0054 — A fleet session is never daemon-hosted, and managed-ness is derived from the process, never from a file on disk
- Status: Accepted (the rule). Nothing of the mechanism has merged, and
this ADR is written at decision time per
ADR-0004, not on merge. The detect-and-name half
is in flight as
PropFlow-Technologies/ccswitch#157(open); the relaunch half is not built and is not to be built from an unattended session — see The scope split. ⚠️ The ruling is a model's and some of the specification is the authoring lane's; no founder has read either, and the overrule channel is open — a founder overruling this is a normal outcome, not an exception. The Deciders line below says which half is which, and it is load-bearing: read it and Provenance before implementing anything here. - Date: 2026-10-14
- Deciders — split on purpose, because attributing a specification to the
model that did not write it is the same defect class this ADR is about.
ADR-0051 is the precedent, and it
is followed here rather than merely cited.
- The decision is Fable's (
claude-fable-5, receiptfb44a1ff4): that a fleet session must never be daemon-hosted; that the defect is detected and refused/relaunched rather than re-adopted; that the detector is state-keyed and needs no update signal; that fleet-ness keys ontask.jsondriverSid, the roster or a board claim and never a self-report; and that the relaunch rides the existing reviver rung by signal. That is decisions 1, 2, 4 and 5's first paragraph, plus both arms of Cost if wrong. The ask was raised2026-10-14T06:55:00Z(the receipt'sat, which equals itsstartedAt— so it timestamps the raise, not the answer) and the verdict was written2026-10-14T07:04:10Z(the receipt file's birth and mtime; the JSON carries no finished timestamp). Question raised by thedock-found-rowsDriver. Not a human decision. - The specification pins are the authoring lane's, made 2026-10-14 under this ADR's own review and nowhere in the receipt: decision 3's unreadable-is-UNKNOWN rule, the exact composition of decision 3's triple, decision 3's decision-6 fallthrough for a session that is unmanaged but not this defect, and decision 5's 1-per-24h cap and its separation from ADR-0035's death budget. Fable ruled that a misread must not relaunch a healthy session and that fleet-ness must not come from a self-report; it did not say which process facts constitute "unmanaged", nor what an unmanaged relaunch's restart budget is. Those answers were derived here from ADR-0024 §1/§5, ADR-0035 decisions 1 and 6, and the measurements in What was measured, then checked by the reviewer against those files. Read them as a lane's inference under review, not as a model's ruling, and overrule them on the same open channel as the rest.
- The decision is Fable's (
- Systems:
ccswitch, the Supervisor and the Temporal task watcher (the reviver rung, ADR-0035),agent-smith's watcher revive path. - Scope. How a fleet session is hosted, and how a lane decides whether a
live session is managed. It does not change which account a session gets
(ADR-0024's
pick_best), and it does not touch interactive sessions a person started.
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 |
- The default-root transcript is a hardlink, not a second file: subject
inode
1603347143is the same inode in both roots, and the control shows the identical pattern (inode1364243759in both). Every managed session looks like this. session-cfgdirs are keyed by tmux session name, not by session id, so a sid-keyed search returns empty for a healthy session. The subject's dir exists —~/.claude/ccswitch/session-cfg/op-dock-found-fleet/, with a sid-namedsession-env/directory and the transcript hardlinked under it. The brief's "nosession-cfgdir under any spelling" is false.
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:
- 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.
- 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 wasclaude --resume <sid> --bg -n <name>run from the smith worker, and on 2026-10-13 it revived board Drivers8d699f3band281c6038as daemon-hosted sessions under the DEFAULT config ("UNMANAGED — no CLAUDE_CONFIG_DIR", soccswitch moverefused 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
- The verdict is Fable's own (
claude-fable-5), not an ADR-0016 fall-through: the receipt carries one ladder rung (fable,status: verdict), no fallback block and no "Fable did not answer" banner. - Both passes ran with the options reversed and both chose B, confidence
high/high; the lower is reported, per the receipt's own note. Under
ADR-0019
§4 two seats answered by one model is a
single_runganswer and never reads as agreement — the reversal is an order control, not concurrence. - The receipt rules less than this ADR specifies, and the Deciders line is
where that is reconciled. Fable's
whyandcost_if_wrongare the source for decisions 1, 2, 4 and 5's first paragraph; decision 3's triple and UNKNOWN rule and decision 5's cap are the authoring lane's inference under review. Anyone weighing this ADR should weigh those two halves differently, which is why they are attributed separately rather than merged under one name. (Amended 2026-10-14, shortly after the original merge: the first revision of this file attributed the whole document to Fable. That was the same defect class as citing a receipt's question field as its ruling — corrected in place, and recorded here rather than silently rewritten.) - No founder has read this. Recorded Accepted on ADR-0051's precedent — a non-human ruling written as the operating decision with the overrule channel named — because ADR-0004 puts the record at decision time. Gera or Fede overruling this is a normal outcome, not an exception.
Options considered
- A.
ccswitchre-adopts — a periodic reconciler claims any live default-config session into asession-cfgdir. Ruled impossible as written, and this is the load-bearing line. A running process cannot adopt a config dir:ccswitchsays so itself at line 12811 — "Everything above changed the ENVIRONMENT. A running session cannot adopt any of it" — and at line 1718, "A running process cannot adopt it (§1) — this session stays on {email} until it is re-exec'd." An adopted dir the process never reads is a registration that lies, which is worse than the gap. Worse still, a default-config session's credential file is the shared default one, so moving it moves every default-config session at once — the structure of the measured 2026-08-29 herd failure recorded at line 12795 (50 of 50 panes on one account, 5h window 14% → 100% overnight). So a working A is B's relaunch minus scoping, and A's "any live default-config session" reaches interactive sessions nobody asked it to touch. - B (this decision). Fleet sessions are never daemon-hosted — detect and refuse/relaunch an unmanaged fleet session, treating daemon hosting as the defect.
- C. Make the gap loud only (ccswitch#157 names unreachable sessions) and accept hand rescue. Rejected: it silently voids the standing doctrine that a wall is not a stop condition. Its loud half ships regardless — see decision 5.
The decision
A fleet session is hosted through
ccswitch execin 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 bareclaude --bg"), and it enforces ADR-0050 §2 — on the minipf/mpfstart throughccswitch exec"because swap-in-place and subagent safety depend on that".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?
Managed-ness is derived from the process, by
CLAUDE_CONFIG_DIRin its own environment (_proc_env_var, walking pane →claudechild). Never from asession-cfgdir'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 ewwcan fail, the pid can race away between the walk and the read, and the pane→child walk can find noclaudeat 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_DIRALONE. A session is the defect this ADR names only when all three hold, each read from the process:CLAUDE_CONFIG_DIRabsent, 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 pairccswitch moverefuses 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_DIRabsent is not this defect. It is respawnable in place, so the remedy atccswitch:17288–17293applies 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_DIRis not this defect either. With no bearer in its environment it is swappable by its file under ADR-0024 §1, soccswitch moveis 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.
- A pane-hosted session with
Fleet-ness is derived from the board, not from the session. The key is
task.json'sdriverSid, the roster, or a board row claim — verified present:dock-found-fleet'stask.jsoncarriesdriverSid: 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.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).
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
- (a) A detector that misreads a healthy or non-fleet default-config session as an unmanaged fleet session will relaunch it and kill every live subagent lane. This is the same absence-read-as-success family ccswitch#157's four review passes found eleven times, and the brief that commissioned this ADR reproduced it twice in one paragraph (the table above). Decisions 3, 4 and 5's separate cap are the whole mitigation — which is why 3 names the forbidden instruments individually, makes an unreadable one UNKNOWN and requires the full triple rather than one variable, 4 keys fleet-ness off the board, and 5 keeps a misdetection from spending the budget a real death needs. Subagents cannot be resumed, only re-dispatched (ADR-0024), so this cost is unrecoverable.
- (b) If the updater can re-host pane-hosted sessions too, "never daemon-hosted" is unachievable as prevention and B degrades to relaunch-per-update — a reconciler in all but name, costing one restart plus its in-flight lanes per fleet session per update. The honest position is that this is not settled: the cause was never established, so whether prevention is even reachable is unknown. Decision 6 is the hedge.
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
- The detect-and-name half is in flight: ccswitch#157, open.
- The relaunch half is destructive by construction — it ends another
session and kills its lanes. It was therefore not built from the
unattended Driver pane that commissioned this ADR, and it needs an attended
seat. Its first test is the misread case, not the happy path. Four
refusals must be demonstrated before a single real relaunch is attempted: a
healthy managed session, a non-fleet default-config session, a pane-hosted
session with
CLAUDE_CONFIG_DIRabsent (decision 3's triple), and a session whose environment could not be read at all (decision 3's UNKNOWN). - Two specification items are UNOWNED, named here rather than given an
implied owner. Both were raised in this ADR's review, both belong to the
relaunch half above, and that half does not exist and may not be built from
an unattended seat — so saying "follow-up PR" would name an owner that is not
there. They are requirements on the relaunch half whenever someone builds it:
- The report must distinguish "never tried" from "tried once, still unmanaged." Decision 5's cap of 1 is only safe if an exhausted cap is visible; decision 6 says such a session is reported but does not say the report carries the failed relaunch. A reader cannot otherwise tell a detector that never fired from one that fired and did not fix it.
- The UNKNOWN count must be surfaced. Decision 3 makes an unreadable environment report-only, and report-only is worthless if nothing reads the report. If most pane-less sessions turn out to be unreadable, the detector does nothing for them while they stay walled — silently, which is the failure mode ccswitch#157 exists to end. That count belongs on #157's surface.
Consequences
- A lane may no longer call a session unmanaged from a file listing. The one
sanctioned read is the process's own
CLAUDE_CONFIG_DIR, with a positive control from a known-managed session in the same breath. ccswitch move's refusal on an env-pinned pane-less session stops being a wall to work around and becomes the detector's definition of the defect.- Nothing here changes interactive sessions, and nothing runs on the MacBook (ADR-0050 decision 1).
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. |
Related
- ADR-0003 — rungs integrate by signals, never calls. Decision 5 is bound by it.
- ADR-0004 — why this exists before the mechanism.
- ADR-0016, ADR-0019 — how the provenance above is classified.
- ADR-0024 — the file/env seam this whole decision rests on.
- ADR-0035 — the rung the relaunch rides.
- ADR-0050
—
ccswitch execis how mini sessions start. - Open PR #220 (ADR-0031/0032), "fleet sessions are hosted outside the GUI
login" — same noun, different axis, and not superseded by this. That pair
governs the launchd domain a fleet session runs in (keychain reachable or
not; ccswitch#147 makes
execrefuse a keychain-reachable context) and, in 0032, releasing a daemon twin of a tmux-held session. This ADR governs the launcher and host of a session that has no tmux pane at all. A session can satisfy ADR-0031's domain rule and still be the defect named here.