The roll-up said nothing was running while an executor was mid-turn. That was the first live test of the hub doorway, and the board was confidently wrong.

The doorway is one Claude Code conversation, the hub, that spawns child sessions and shows them by session-ref. It is deliberately not a new web app. The engine that spawns, resumes and streams real claude.exe sessions already existed in our dashboard’s bridge, with spawnFresh, send, a BridgeEvent stream and a MAX_SESSIONS cap. The spec I commissioned for v0 said reuse it and build nothing that duplicates it. What was new is thin.

The pieces

A child starts through the existing handoff path with one added flag:

handoff-spawn --parent <my-short-sid> --seed "<prompt>"

The bridge tags the child’s manifest with parent_sid. The hub reads children back with a subsessions command that GETs the dashboard’s /api/dashboard/sessions route and filters to rows whose parentSid matches the calling session. Each row carries a state (RUNNING, READY, BLOCKED or ERRORED) and the pulse line the child last wrote. The default view shows only live and needs-you children, and --all adds the finished ones.

Addressing reuses the session-ref that our async relay already speaks. Every session footer prints [N] plus a short sid. A child is [2] in the roll-up or any sid prefix, and two flags act on it:

subsessions --answer 2 "go, but keep the migration in its own commit"
subsessions --cancel 2

When a child needs me, it surfaces in the hub’s roll-up instead of in a tab I have to hunt for. The phone path is the same idea. The relay posts to a shared DM tagged with the session ref, and I answer with the number first (“2 go”), so a bare reply can’t be grabbed by the wrong session.

A state column that read the wrong file

The first version of the state column came straight from the session manifest. That looked reasonable, because the manifest already carries pulse, next step and waiting-on-me per turn. I built the roll-up on it, dispatched a real executor, and ran subsessions. The executor was mid-turn and the roll-up reported it as silent.

Headless children spawned through the bridge don’t behave like an interactive session that rewrites its manifest every turn. The manifest is a record of what a session last said. It says nothing about whether the process is alive. So a child could be working hard and look idle, or be dead and look idle, and the roll-up couldn’t tell those apart.

The cost was trust. For the rest of that first session I checked each child by hand against the bridge, which defeats the point of a roll-up. The fix, which got its own ticket, changed deriveState in the dashboard’s sessions module to read the live bridge status first. That is where RUNNING, READY and ERRORED now come from for headless children. The manifest pulse is display text and no longer a source of truth about liveness.

Verifying a child’s claim before it reaches me

The hub’s rules say that every claim from a child gets verified independently before it goes to me. The rule got tested on a small hardening ticket. An executor came back with a hook config that invoked bare bash in a settings file. That is a known Windows gotcha, and the hub flagged it in its answer to the child. The child had to prove the hook fired live before the change was mergeable. The child had also correctly flagged that its change reversed an earlier decision to gitignore that settings file, and the hub held the merge until I weighed in. Reading a diff would not have caught the bash problem. It only shows up when the hook runs.

A few days later I found five of our guardrail hooks had been failing open. When one couldn’t read its input, it allowed the action. We changed all five to refuse when they can’t parse their input. It was the same failure as the roll-up. A status view or a guardrail that goes quiet on error looks identical to one that is fine.

Two hubs, one dispatch authority

I asked the hub I was running from my phone whether multiple hubs could coexist. The answer was one real risk, which is two dispatch authorities. On 7/23 two PM sessions both dispatched the same ticket and collided on clone occupancy. Two hubs that both assign work would repeat that, and each would hold a stale registry that doesn’t know what the other did.

The desktop hub was safe only because it does nothing between turns. The dangerous moment is when I sit down and prompt it, because it acts on a picture from before I touched anything. The current rule is one dispatching hub at a time. If I return to an older one, its first instruction is to re-orient from the roll-up before acting, or I retire it and hand off.

What is still open

The 7/25 run was the hub working a real queue. It triaged two open tickets and left notes on each. It cleared a stale reminder that had fired for a week on finished work. It dispatched a hardening change to the path-handling module, verified it and closed it. It also filed a follow-up for a session-cancel bug, so --cancel still needs work. The doorway works, but it is not finished.

If you build one of these, check the state column first. Dispatch, answers and cancels all depend on believing what the roll-up says about which children are alive, and mine was wrong on its first live run.