0054 — A fleet session is never daemon-hosted, and managed-ness is derived from the process, never from a file on disk

The problem

A background fleet session can end up hosted by Claude Code's own daemon under the default config dir, outside ccswitch. Such a session is env-pinned and pane-less, which is precisely the pair ccswitch move refuses. When its account then hits a weekly wall, nothing can move it, and it sits walled for days holding its board rows.

What was measured, and with which instrument

All readings below are read-only, taken 2026-10-14 ~07:14Z on the mini. The subject is session 19d51f87 (the dock-found-fleet Driver, driverSid in its task.json). The positive control is e6ad2688 (the dock-found-rows Driver), a session independently known to be ccswitch-managed.

Two signals that look like proof of "unmanaged" and are not

The brief that commissioned this ADR called the subject unmanaged on two readings. Both fail against the control, and they are recorded here as the design constraint, not as a correction of a person — because a detector built on either one is the misread this decision's cost-if-wrong is about:

Reading Subject Control (known managed) Discriminates?
Transcript present in the default root ~/.claude/projects/ yes yes No
A session-cfg dir found by searching for the session id not found not found No

A dir on disk is also not the answer in the other direction. ADR-0024 §5 already settled this: "Which kind a session is must be derived — read the process's actual environment — never a remembered flag or a maintained list. The standing rule: if a fact has to be remembered to stay true, it will be false." The dir persists whether or not the process still uses it.

The instrument that does discriminate, with a control that passes

ccswitch's own _proc_env_var(pid, name) (lines 15069–15085; the call site the ruling cites is 15134) reads one variable out of a pid's own environment via ps eww. Walking from the pane to the actual claude child — _swap_pid_candidates' walk, because a pane created under a shell has claude as a child — gives:

CLAUDE_CONFIG_DIR bearer in env tmux pane
Control e6ad2688 (pid 59294) …/session-cfg/op-dock-found-rows absent yes (pid 59247)
Subject 19d51f87 (pid 6798) absent CLAUDE_CODE_OAUTH_TOKEN present none

Only presence was read for the bearer, never a value. The control passes, so the subject's None is a reading and not a broken probe.

Those are exactly ADR-0024 §1's two shapes: the control carries no bearer in its environment and reads $CLAUDE_CONFIG_DIR/.credentials.json, so it is swappable in place; the subject holds the token in its environment, so it "ignores the credential file entirely" and is unswappable. And the subject is daemon-hosted in the literal sense — its parent, pid 6775, is

claude --bg-pty-host /tmp/cc-daemon-501/…/18f9570b.pty.sock 220 50 -- \
  …/versions/2.1.296 --resume …/19d51f87….jsonl -n driver-…found-fleet

reparented to init (PPID 1), with no tmux pane anywhere for its slug while the control has one. So both halves of ccswitch move's refusal hold, and ccswitch itself names the only remedy (line 17295–17298): a session that is env-pinned and has no live hosting pane "can only be moved by ending it and starting a fresh session beside it (ccswitch exec -- claude …)".

The consequence is verified. The cause is NOT the auto-update the row names

The subject is a verified instance of unmanaged ⇒ unmovable at a wall ⇒ stuck for days. The row that commissioned this ADR blames a Claude Code auto-update. That is the wrong cause, and the right one is already fixed going forward.

A mid-life re-host did happen. Under the managed config dir, the subject's own session-env/19d51f87…/sessionstart-hook-8.sh is dated 2026-10-10 15:28 local — a session start through ccswitch — while the process running today started 2026-10-12T21:28:56Z with no CLAUDE_CONFIG_DIR at all. The session was managed earlier in its life and its current process is not.

Two measurements point away from an auto-update:

  1. The re-host runs version 2.1.296, installed 2026-10-09T19:35:11Z — three days and two hours before the re-host. No version was installed near it.
  2. Version 2.1.299 was installed 2026-10-14T02:49:38Z, a real update, and the subject was not re-hosted by it: its process still dates from Oct 12 and still runs 2.1.296.

And the actual route has a fingerprint match. The subject's argv is

--resume …/19d51f87….jsonl -n driver-portfolio-architecture-phases.found-fleet

under a --bg-pty-host daemon parent. That -n value is exactly the Temporal workflowId in its own watcher.json, and the shape is exactly the revive command agent-smith#999 deleted: argv = [claude, "--resume", input.sid, "--bg", "-n", input.name]. #999's replacement comment names the same failure in the same words, for two other sessions:

⚠️ NEVER BARE claude, NEVER --bg. This was claude --resume <sid> --bg -n <name> run from the smith worker, and on 2026-10-13 it revived board Drivers 8d699f3b and 281c6038 as daemon-hosted sessions under the DEFAULT config ("UNMANAGED — no CLAUDE_CONFIG_DIR", so ccswitch move refused them at the usage wall)

So the population is at least three sessions, two of them named by #999, produced by the pre-#999 watcher revive — not by an updater. It is the same mechanism ADR-0035 decision 4 documents as claude --resume <sid> --bg.

This is not offered as proof, and the instrument that would have settled it is silent for a reason worth recording. events.jsonl does carry a revived event kind — 27 of them across 57 task files — so "a revive is not logged" would be false. But all 27 are Supervisor replacements ("The Driver watching this task had died; the Supervisor started a replacement"), and none names the subject. The Temporal watcher's own driver_revive_activity — the path whose command the subject's argv matches — writes no such event. So the one rung that logs is not the rung indicated, and the subject's log additionally stops at 2026-10-12T01:08:18Z, twenty hours before the re-host: this instrument has no coverage of the window in either direction.

This sharpens the decision rather than dissolving it. #999 fixed the launcher; it did not and cannot re-host the sessions already launched wrong — the subject's process predates #999's merge (2026-10-12T21:28:56Z vs 2026-10-13T22:46:52Z) and is still running unmanaged today. A state-keyed detector is exactly what covers an already-broken population, which is why decision 2 keys on current state and not on any launch-time or update-time event. Fable reached the same place from the other side in its reversed pass: "Its unmanaged CAUSE was not verified as an auto-update re-host; if another route unmanages sessions, B's update-focused framing under-scopes the defect." It does, and the row's title should be read as naming a symptom.

Why no reviver rescued it

The watcher is alive and ticking — watcher.json tickAt 2026-10-14T07:11:56Z, rosterOk: true, found: true. It is correct: the session is alive (pid 6798). Every rung of the supervision ladder keys revival on death, so a session that is alive, walled and unmanaged is invisible to all of them — and the reason is simpler than the watcher's grace: under ADR-0035 decision 4 as amended 2026-09-29, the Supervisor's stand-down is a bounded grace counted from when it has itself seen the Driver dead for 15 minutes (WATCHER_REVIVE_GRACE_MS). Here the Driver is alive, so that dead-timer never starts at all and the grace never becomes the operative reason. Nothing is broken; the state has no owner.

Provenance, stated plainly rather than softened

Options considered

The decision

  1. A fleet session is hosted through ccswitch exec in a tmux pane, never by the daemon under the default config dir. Daemon hosting of a fleet session is the defect, not the symptom. This extends to sessions the fleet did not launch the ruling already made where it did: PropFlow-Technologies/agent-smith#999 (merged 2026-10-13T22:46:52Z, "watcher revive: start the Driver through ccswitch exec in the task's repo, never bare claude --bg"), and it enforces ADR-0050 §2 — on the mini pf/mpf start through ccswitch exec "because swap-in-place and subagent safety depend on that".

  2. The detector is state-keyed and periodic. It is never keyed on an update, and never on a launch. An auto-update is not an event the fleet is notified of, and as measured it is not even the route here. The deeper reason is that a launcher fix cannot reach a session already launched: #999 corrected the revive command, yet the subject — re-hosted the day before #999 merged — is still running unmanaged. The detector reads current state on a schedule and asks only one question: is this session managed, now?

  3. Managed-ness is derived from the process, by CLAUDE_CONFIG_DIR in its own environment (_proc_env_var, walking pane → claude child). Never from a session-cfg dir's existence, never from a transcript's location, never from a sid-keyed directory search, and never from the session's own self-report. This is ADR-0024 §5 applied, and the table in What was measured is the proof that the easy signals do not discriminate.

    An unreadable instrument is UNKNOWN, and UNKNOWN is report-only — never a relaunch. ps eww can fail, the pid can race away between the walk and the read, and the pane→child walk can find no claude at all. None of those is "unmanaged". This is the repo's own unknown-is-never-negative rule (ADR-0035 decision 1 applies it to liveness: "an undetermined reading does nothing"), and it is binding here precisely because cost-if-wrong (a) is unrecoverable. A detector that cannot read a session says so and stops.

    THE FULL TRIPLE, NOT CLAUDE_CONFIG_DIR ALONE. A session is the defect this ADR names only when all three hold, each read from the process: CLAUDE_CONFIG_DIR absent, a bearer carrier present in its environment (so it is env-pinned and unswappable per ADR-0024 §1), and no live hosting pane. That is exactly the pair ccswitch move refuses plus the reason it cannot be swapped instead, and it is the measured subject's shape. Narrowing to one variable would relaunch sessions this decision does not cover.

    The two near-misses are decision 6's case, and neither is an unhandled gap — spelled out because a case that lives only in a review thread is a case the implementer will not see, and the cost of getting it wrong here is killing every lane a healthy session holds:

    • A pane-hosted session with CLAUDE_CONFIG_DIR absent is not this defect. It is respawnable in place, so the remedy at ccswitch:17288–17293 applies instead of the pane-less one, and this detector must not relaunch it.
    • A session with no bearer carrier, no pane and no CLAUDE_CONFIG_DIR is not this defect either. With no bearer in its environment it is swappable by its file under ADR-0024 §1, so ccswitch move is not refused for it — which is precisely why it is not the thing this ADR names.

    Both are reported under decision 6 and never relaunched by this detector, like any other unmanaged session. "Not this defect" means reported, never silent.

  4. Fleet-ness is derived from the board, not from the session. The key is task.json's driverSid, the roster, or a board row claim — verified present: dock-found-fleet's task.json carries driverSid: 19d51f87-…. Never the session's own claim about itself. A session that is unmanaged but not fleet is out of scope and is left alone.

  5. The relaunch rides the existing reviver rung, by signal. The reviver of ADR-0035 decision 4 already owns "end a Driver and start a fresh one", already arbitrates by a file, and already caps restarts (5 per rolling 24h, decision 6). "Unmanaged fleet session" becomes a signal that rung reads, not a new rung and not a call into it (ADR-0003). Its file-decides-which-reviver rule applies unchanged. Note the rung must learn a liveness-independent trigger: as measured, every existing reviver keys on death and correctly does nothing here.

    Whose budget it spends, pinned, because sharing one silently is the failure: an unmanaged relaunch gets its own cap of 1 per session per rolling 24 hours, counted separately from ADR-0035 decision 6's 5-per-24h death budget, and it never decrements that death budget. Two reasons, both one-directional. A misdetection must not consume restarts a real death needs — cost-if-wrong (a) is unrecoverable, and a detector bug that also starves the death path turns one defect into two. And the condition is not like a death: being unmanaged is a persistent state, not an event, so a relaunch that fails to fix it would re-fire on the next tick forever; a cap of 1 makes the second detection an escalation instead of a loop. A session still unmanaged after its one relaunch is reported, not relaunched again (decision 6).

  6. The loud half ships regardless of the relaunch half. ccswitch#157 ("a live session it cannot reach is reported UNREACHABLE, never absent") and local-bin#551 (merged 2026-10-14T06:29:09Z) stand on their own, so a missed relaunch is at least named rather than silent. This is C's only good part, kept.

Cost if wrong — both arms, from the receipt

This ADR does not rescue 19d51f87. That seat needs a hand --resume relaunch under ccswitch exec, which is a person's move; its lanes already died at the wall, so most of that kill cost is paid.

The scope split — a decision, not a disclaimer

Consequences

Citations — which held

Every line below was opened and read, not grepped. ccswitch line numbers are against ~/.local/bin/ccswitch → deploy/ccswitch-78a5c0e0b732, md5 11163f7e86026a6de98bb5a3cdc9dd9a, 19125 lines, byte-identical to the repo copy ~/code/ccswitch/ccswitch (same md5). Line cites rot on the next deploy; the hash is what makes them checkable.

Citation Verdict
ccswitch:12811 — a running session cannot adopt a changed env Holds. Context is bearer/shard rotation, i.e. the general principle, not CLAUDE_CONFIG_DIR specifically; 1718 states it for the credential link.
ccswitch:17295–17298 — pane-less env-pinned ⇒ end it, relaunch via ccswitch exec Holds verbatim.
ccswitch:17288–17293 — env-pinned with a pane ⇒ "The only way to move it is a RESPAWN, which kills the pane's whole process group" Holds verbatim. Added 2026-10-14 with decision 3's pane-hosted near-miss, which it is the authority for.
ccswitch:1718, :12795, :15134 (reversed pass) All hold.
agent-smith#999 merged, ruled "never bare claude --bg" Holds, and carries more than the ruling claimed: MERGED 2026-10-13T22:46:52Z, title verbatim, and its diff shows the deleted argv plus two further sessions (8d699f3b, 281c6038) with this exact signature.
ADR-0035 is "one waker, one reviver, chosen by a file"; riding it is coherent Holds, with one caveat now in decision 5: its revivers key on death.
ADR-0024 "a session changes accounts by its file, not by dying" supports the per-session-store point Holds (§1, §5). ⚠️ Two files on main carry the number 0024; cite it by filename.
ADR-0050 §2, local-bin#551, ccswitch#157 state All hold.
Brief: "no session-cfg dir under any spelling" Does not hold. The dir exists; see the table.
Brief: ruling landed 07:04Z Holds. The receipt file's birth is 07:04:10Z. Its at/startedAt (06:55:00Z) is the raise, not the answer — an earlier revision of this ADR "corrected" the brief from that field and was wrong; both times are now stated on the Deciders line.
Row title: an auto-update is the cause Does not hold — see the cause is NOT the auto-update. Two measurements against it, and a fingerprint match to the pre-#999 watcher revive instead.
Brief: "15 of 70 open rows" Unverified here. Measured instead: 23 items in the subject's row-fence.json, 36 cumulative lines in claimed-items.txt. The 15/70 figure is the Driver's count, carried as its claim, not as fact.