0028 — The session ceiling counts what is actually running, and 70 is the number
- Status: Accepted.
- Date: 2026-09-20
- Deciders: Gera, 2026-09-20, choosing "70, plus work the drift" from four options put to him with 254 paired readings attached.
- Supersedes: the ceiling of 35 in
session-cap(b88294643 set the cap; this changes the quantity it counts and the number it compares against).
Why this was not a tuning change
session-cap refuses a start past a ceiling. It counted with
host_headroom.claude_processes(), which cannot see a daemon-hosted session
at all. A background session runs as one process carrying both the wrapper
and the real binary on a single command line:
claude bg-pty-host --bg-pty-host /tmp/cc-daemon-501/<d>/spare/<id>.pty.sock \
200 50 -- .../share/claude/versions/2.1.276 --session-id <sid> ...
Its comm is the version path, so the claude test matched. Its cmd contained
bg-pty-host, so the helper test dropped it. The second won, and the session
was counted as scaffolding. Measured 2026-09-19: seven of eight version-named
processes carried a helper marker.
That is why the ceiling did not hold on 2026-09-17, when a spawn bug
(nudge:1568, local-bin #231) took the fleet from 16 to 134 sessions in 2h30m
and the machine kernel-panicked on VM compressor segment exhaustion. The cap
was not lagging. It was structurally blind to the only population that was
growing, and its counted number sat pinned at 35-36 throughout.
The measurement the decision rests on
254 paired readings, 2026-09-20, 00:21 → 21:29:
min median p90 MAX
GATED (old) 29 31 35 39
ARGV (true) 38 40 44 51 ratio 1.29
The true count exceeded the old ceiling of 35 in 254 of 254 samples, on a
box that was healthy all day: swap 0.0G on every reading, compressor flat at
9-15% and not rising with the count, state OK on 252 of 254.
The decision
- The ceiling counts by argv.
claude_processes_argv()classifies on argv position: the effective program is whatever follows a standalone--, and a process is a helper only when that program's own first argument is a helper subcommand. Substring matching against the whole command line is what caused the blindness and is not used. - The ceiling is 70. It clears the observed peak of 51 with about a day of headroom, and still stops a 09-17-shaped burst far below 134. It is NOT "35 adjusted" — the two numbers count different populations and are not comparable.
- The count ceiling and the pressure verdict both stay, because they answer
different questions.
pressure_verdictreads swap, compressor and free — "the box is dying now" — and already covers memory, including the ~2.8 GB of idle spares no session count can see. The ceiling answers "too many things are starting". They are not redundant: on 09-17 pressure did not reach DANGER until 05:45, by which time the fleet was already ~100. Pressure is the backstop; the count is the only control that can fire early. - A count is an acknowledged proxy, not the hazard. Sessions measured 427 MB median on 2026-09-20; at the panic they averaged ~1.6 GB. The hazard is count × size. SYSTEMS.md already says "session age is the variable, not session count". The ceiling is kept because a count is what a spawn site can be refused against — and for no stronger reason than that.
⚠️ What this decision does NOT fix, and Gera named it when he took it
The same 254 readings show the fleet growing +0.62/hour, +15/day, with the hourly minimum rising too — accumulation, not churn. Of the 47 sessions live at 21:30, 34 were over a day old and 14 over two days.
A ceiling alone converts "crash in N days" into "wedge in N days." At this drift, any fixed number is reached and then refuses everything. The decision taken was explicitly "70, plus work the drift". The ceiling buys about a day; it is not the fix. Sessions have to leave.
The lead: tests/session-expire.py is red on local-bin main at 82 passed /
15 failed (measured 2026-09-20, identical before and after this change). The
failing cases cover status holds, the per-run cap, and whether a run writes its
log line at all — the behaviour of the one tool whose job is to remove sessions.
The failure mode is a wedge, so the rollback is not "remove the brake"
SESSION_CAP_COUNT=legacy returns the gate to the old number while keeping
refusal, rather than SESSION_CAP_OFF=1 which removes it. An unreadable count
still yields ALLOW-UNKNOWN: a gate that fails closed on its own bug stops every
spawn path on the machine, including the session someone would open to fix it.
Controls live in tests/session-cap-gates-on-the-true-count.py, where the
load-bearing cases are the ALLOWs — a guard that refuses everything hides
behind looking careful.
Consequences
- local-bin #240 added the counter (measured only); #241 moved the gate onto it.
- The
hostentry in SYSTEMS.md gainsargvSessions/argvRssGb/argvHelpersinsnapshot.jsonandargv=N/NGin the sampler's log line. - Anything reading
claudeSessionsas "how many sessions are running" is reading the blind number and should move toargvSessions.