A member whose MCP contact lands before registry.put is marked present but never leaves SPAWNING, so it holds no seat for reclaim accounting #722

Open
opened 2026-10-04 08:16:15 +02:00 by ltms · 0 comments
Owner

Found independently by both architects consulting on #705 option 1, neither of whom had seen the
other's position. I then verified the path myself in the main clone at 9a64d42. Filing it
separately because it is pre-existing and must not be bundled into #705's role addition.

The window

Both session-acquisition paths launch the pane before registering it:

  • SessionManager.spawn calls launcher.spawn(req) at :235 and only reaches
    registry.put(handle.id(), session) at :244.
  • The worktree path has the same ordering at :715 → :727.

Between those lines the pane exists and has no registry entry.

What happens if the agent's MCP contact lands in that window

The resolver's roster rung finds nothing, so the caller falls to the herdr-pane floor and resolves
WORKER. That marks presence:

// FleetMcp.java:832-837
static void markSpawnedMemberPresent(Principal caller, MemberPresence presence) {
    if (caller.isSpawnedMember()) {          // WORKER || ARCHITECT
        presence.markPresent(caller.terminal());
    }
}
// SessionManager.java:1256-1261 — PresenceFleet.markPresent
super.markPresent(terminal);
sessions.onReady(terminal);

and onReady is then a silent no-op, because there is still no registry entry:

// SessionManager.java:898-899
void onReady(String t) { transitionByTerminal(t, SPAWNING, READY); }

// SessionManager.java:1232-1235
private void transitionByTerminal(String terminalId, State from, State to) {
    MemberSession current = findByTerminal(terminalId);
    if (current == null || current.state() != from) return;   // <-- here

The presence mark persists — the set is additive. Registration then happens and leaves the
session in SPAWNING, and nothing retries the transition. So the member is deliverable but never
recorded as READY.

Why that matters

READY is not cosmetic. It is read in three places, and one is seat accounting:

FleetMcp.java:2204        boolean holdsSeat = session.state() == MemberSession.State.READY ...
SessionManager.java:911   if (current.state() != READY && current.state() != DONE) {
SessionManager.java:1020  if (s.state() != READY && s.state() != DONE) {

So a session stuck in SPAWNING is skipped by reclaimable()'s seat test. The practical effect is
on reclaim and capacity accounting, not on delivery — delivery only needs presence, which was marked.

Reachability — honestly stated

Neither architect reproduced the timing, and nor did I. The ordering permits it; only in-memory
work sits between the two lines, and an agent needs seconds to boot before it sends MCP initialize.
One architect judged it "unreachable in practice" while explicitly adding that it is "not provably
zero"; the other said "the ordering permits it" without claiming an instance. Treat this as a defect
on paper until someone names a path in — a delayed registry.put under load, or a post-spawn
exception before registration, which is the failure form of the same class and would leave an orphan
pane.

Why this is filed apart from #705

#705 option 1 replaces the herdr-pane floor with a new OBSERVER role. Once the observer is added to
the presence gate — which that ticket's decision requires — this path is byte-for-byte what it is
today
: presence marked, onReady no-op, session left in SPAWNING.

One architect wanted "registration reconciliation" inside #705's commit and would have rejected a
role-only PR without it. I ruled against that, because bundling it would hide a pre-existing defect
inside a role addition and make the role's diff look like the cause. The fix belongs here.

Suggested shape

Have registration notice an already-marked presence and complete the transition, rather than relying
on the contact arriving second. Two notes:

  • Do not fix it by reordering registry.put ahead of launcher.spawn. #702 shows this area's
    ordering is load-bearing in the other direction, and a spawn that fails after registration would
    leave a registry entry for a pane that never existed.
  • Whatever the fix, the test must assert the ordering is tolerated in both directions:
    contact-then-register and register-then-contact must both end at READY. A test that only drives
    the normal order passes today and proves nothing.
Found independently by **both** architects consulting on #705 option 1, neither of whom had seen the other's position. I then verified the path myself in the main clone at `9a64d42`. Filing it separately because it is **pre-existing** and must not be bundled into #705's role addition. ## The window Both session-acquisition paths launch the pane before registering it: - `SessionManager.spawn` calls `launcher.spawn(req)` at `:235` and only reaches `registry.put(handle.id(), session)` at `:244`. - The worktree path has the same ordering at `:715` → `:727`. Between those lines the pane exists and has no registry entry. ## What happens if the agent's MCP contact lands in that window The resolver's roster rung finds nothing, so the caller falls to the herdr-pane floor and resolves `WORKER`. That marks presence: ```java // FleetMcp.java:832-837 static void markSpawnedMemberPresent(Principal caller, MemberPresence presence) { if (caller.isSpawnedMember()) { // WORKER || ARCHITECT presence.markPresent(caller.terminal()); } } ``` ```java // SessionManager.java:1256-1261 — PresenceFleet.markPresent super.markPresent(terminal); sessions.onReady(terminal); ``` and `onReady` is then a **silent no-op**, because there is still no registry entry: ```java // SessionManager.java:898-899 void onReady(String t) { transitionByTerminal(t, SPAWNING, READY); } // SessionManager.java:1232-1235 private void transitionByTerminal(String terminalId, State from, State to) { MemberSession current = findByTerminal(terminalId); if (current == null || current.state() != from) return; // <-- here ``` The presence mark **persists** — the set is additive. Registration then happens and leaves the session in `SPAWNING`, and nothing retries the transition. So the member is deliverable but never recorded as `READY`. ## Why that matters `READY` is not cosmetic. It is read in three places, and one is seat accounting: ``` FleetMcp.java:2204 boolean holdsSeat = session.state() == MemberSession.State.READY ... SessionManager.java:911 if (current.state() != READY && current.state() != DONE) { SessionManager.java:1020 if (s.state() != READY && s.state() != DONE) { ``` So a session stuck in `SPAWNING` is skipped by `reclaimable()`'s seat test. The practical effect is on reclaim and capacity accounting, not on delivery — delivery only needs presence, which was marked. ## Reachability — honestly stated **Neither architect reproduced the timing, and nor did I.** The ordering permits it; only in-memory work sits between the two lines, and an agent needs seconds to boot before it sends MCP `initialize`. One architect judged it "unreachable in practice" while explicitly adding that it is "not provably zero"; the other said "the ordering permits it" without claiming an instance. Treat this as a defect on paper until someone names a path in — a delayed `registry.put` under load, or a post-spawn exception before registration, which is the failure form of the same class and would leave an orphan pane. ## Why this is filed apart from #705 #705 option 1 replaces the herdr-pane floor with a new `OBSERVER` role. Once the observer is added to the presence gate — which that ticket's decision requires — this path is **byte-for-byte what it is today**: presence marked, `onReady` no-op, session left in `SPAWNING`. One architect wanted "registration reconciliation" inside #705's commit and would have rejected a role-only PR without it. I ruled against that, because bundling it would hide a pre-existing defect inside a role addition and make the role's diff look like the cause. The fix belongs here. ## Suggested shape Have registration notice an already-marked presence and complete the transition, rather than relying on the contact arriving second. Two notes: - Do **not** fix it by reordering `registry.put` ahead of `launcher.spawn`. #702 shows this area's ordering is load-bearing in the other direction, and a spawn that fails after registration would leave a registry entry for a pane that never existed. - Whatever the fix, the test must assert the **ordering** is tolerated in both directions: contact-then-register and register-then-contact must both end at `READY`. A test that only drives the normal order passes today and proves nothing.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#722