The original design for Control Tower’s session state was the obvious one: one JSON file per session, with a status field you read and a status field you edit. Nobody had written the concurrency story, because on paper it looked like there wasn’t one, until the re-architecture question got asked with no constraint on stack, and the obvious question followed it: what happens when a human and an agent both touch the same session at once? The honest answer was “we don’t know, we never wrote that part down.” A tool whose entire reason to exist is coordinating concurrent work had never actually addressed concurrent writes.
The fix nobody had to invent
The fix wasn’t new. A sibling lab, local-agentic-coding-lab, had already built and validated the exact pattern this needed: RunLogger, an append-only JSONL stream as the single source of truth, with every derived fact computed generically from the stream instead of stored as a separately-mutated field. Control Tower’s rewrite is that pattern applied to a session-lifecycle vocabulary instead of a tool-call vocabulary: five event types (session_start, session_block, session_unblock, session_review, session_close), each with a small fixed field set, one append-only file per session, and nothing else. Nobody but a session’s owner ever appends to its file, which removes the concurrent-write problem by construction: there is no shared mutable state left to race over.
What “derived, never stored” actually means
The old design would have had a status field sitting in the JSON, updated by whichever command last touched it. The new design has no such field anywhere on disk. Four small pure functions compute everything a caller could want, every time, from the event list:
status(events)replaysstart → block → unblock → review → closein timestamp order; the last transition wins.stale(session)isstatus == active AND now − last_event.timestamp > threshold, presentation logic, not a status value. A session is never stored as stale; it is shown as stale.blockers(events)is everysession_blockwithout a latersession_unblock.next_action(events)is the most recent note-bearing event, and a bareunblockwith no note must preserve the prior blocker’s reason rather than clearing it, since it isn’t itself a note-bearing event (a clarification the Phase 1 review had to pin down explicitly, because the original vocabulary table didn’t say).
None of these four functions writes anything back to disk. A session’s current view is always replay(read(<id>.jsonl)), computed fresh, every call.
The caveat that didn’t get papered over
One honest gap survives the rewrite, documented rather than hidden: a single session’s own append still depends on the operating system making that write’s underlying syscall atomic. That’s a safe assumption on local POSIX filesystems, and an explicitly unverified one on some networked filesystems: certain NFS configurations don’t guarantee O_APPEND atomicity. The partitioning-by-session-file move eliminates cross-session contention by design; it does not, on its own, prove atomicity of a single file’s own concurrent appends on every possible filesystem. That gap got a real test later in the arc, not a shrug now.
What next
The event log answers “is this safe to write concurrently.” It says nothing yet about what happens when a session stops being a human’s freeform work log and starts being one node in a machine-generated build plan, which is exactly the next pressure this architecture had to survive.
What Next
Giving the Factory a Spine in a Day
The event model gets extended, not forked, to represent nodes in a real multi-task build plan, and the first real DAG it ever ran was VeilGremlin's.