Follow-up to #271, which was a good fix — this is the consequence it left open.
What changed
Before #271, coding-session.ts ended the session after every Pilot run. That was the bug (it made
delegation single-use), and the fix is right: a run now closes only the session it opened
(shouldEndSessionAfterRun({ openedByRun })).
But ending-every-run was also, accidentally, the only thing that reaped sessions. Nothing replaced
it with a deliberate policy.
What now ends a coding session
| Path |
Trigger |
routes/coding.ts:1384 |
the user clicks Kill |
coding-session.ts:468 |
a run closes a session it opened |
reconcileOrphanedSessions |
an active session the runner is not tracking, after a 3-min grace |
STALE_DRIVER_MS (15 min) expires the driver claim, not the session — different thing.
There is no idle timeout. A session opened by a delegated run and then left alone stays active
indefinitely, with a live claude --dangerously-skip-permissions child process on the user's
machine.
And the one reaper is opportunistic, not scheduled
reconcileOrphanedSessions has exactly one caller — routes/coding.ts:1506, inside the sessions
listing. It runs when somebody loads the console. The cron (index.ts:206-217) runs
runDueTriggers, runDueDeliveries and runStaleRunSweep; sessions are not swept.
So for a user who delegates via chat/MCP and never opens the Coding tab — exactly the workflow #271
was built to enable — nothing reaps anything.
Why it matters
- A persistent LLM CLI process per repo. Four repo coders under one Lead = four long-lived
Claude Code processes, each holding a session and accumulating context, indefinitely.
- Stale
active rows. After a runner restart the rows stay active until someone opens the
console; until then ensureActiveSession re-attaches to them (idempotent, so it mostly works) and
connectivity reporting shows sessions that are not really there.
- It is bounded but not small. One-active-session-per-repo caps the count at one per repo, so
this is not unbounded growth — it is a permanent resident per repo.
Suggested shape
Decide the policy explicitly rather than inheriting the absence of one:
- An idle timeout on sessions (last driver activity / last capture older than N), reaped by the
cron alongside runStaleRunSweep — which already exists for precisely this class ("a row stuck at
running forever tells every supervisor its subordinate is still working").
- Move
reconcileOrphanedSessions onto that cron too, so orphan detection does not depend on a
human opening a tab.
- If long-lived sessions are wanted (warm engine = faster delegation, which is a real benefit),
then say so and make the idle window generous — but make it a decision, with the process cost
visible in the session view.
Also: the two new modules from #271 have no tests
lib/coding-session-open.ts — ensureActiveSession + startSessionOnRunner: the create/reuse/
race path, and the one that decides opened. No test file.
lib/instance-connectivity.ts — runtimeConnectivity / runtimeConnectivityMany, now shared by
the driver and supervision. No test file.
The pure classifiers are covered (subordinate-connectivity 7, coding-session-lifecycle 7). The
untested part is the orchestration, and specifically opened — get that wrong in either direction
and you either leak every session or destroy a session a human opened, which is #271 again from the
other side. The lost-race branch (catch → reuse winner → opened: false) is exactly the kind of
path that regresses silently.
Follow-up to #271, which was a good fix — this is the consequence it left open.
What changed
Before #271,
coding-session.tsended the session after every Pilot run. That was the bug (it madedelegation single-use), and the fix is right: a run now closes only the session it opened
(
shouldEndSessionAfterRun({ openedByRun })).But ending-every-run was also, accidentally, the only thing that reaped sessions. Nothing replaced
it with a deliberate policy.
What now ends a coding session
routes/coding.ts:1384coding-session.ts:468reconcileOrphanedSessionsactivesession the runner is not tracking, after a 3-min graceSTALE_DRIVER_MS(15 min) expires the driver claim, not the session — different thing.There is no idle timeout. A session opened by a delegated run and then left alone stays
activeindefinitely, with a live
claude --dangerously-skip-permissionschild process on the user'smachine.
And the one reaper is opportunistic, not scheduled
reconcileOrphanedSessionshas exactly one caller —routes/coding.ts:1506, inside the sessionslisting. It runs when somebody loads the console. The cron (
index.ts:206-217) runsrunDueTriggers,runDueDeliveriesandrunStaleRunSweep; sessions are not swept.So for a user who delegates via chat/MCP and never opens the Coding tab — exactly the workflow #271
was built to enable — nothing reaps anything.
Why it matters
Claude Code processes, each holding a session and accumulating context, indefinitely.
activerows. After a runner restart the rows stayactiveuntil someone opens theconsole; until then
ensureActiveSessionre-attaches to them (idempotent, so it mostly works) andconnectivity reporting shows sessions that are not really there.
this is not unbounded growth — it is a permanent resident per repo.
Suggested shape
Decide the policy explicitly rather than inheriting the absence of one:
cron alongside
runStaleRunSweep— which already exists for precisely this class ("a row stuck atrunningforever tells every supervisor its subordinate is still working").reconcileOrphanedSessionsonto that cron too, so orphan detection does not depend on ahuman opening a tab.
then say so and make the idle window generous — but make it a decision, with the process cost
visible in the session view.
Also: the two new modules from #271 have no tests
lib/coding-session-open.ts—ensureActiveSession+startSessionOnRunner: the create/reuse/race path, and the one that decides
opened. No test file.lib/instance-connectivity.ts—runtimeConnectivity/runtimeConnectivityMany, now shared bythe driver and supervision. No test file.
The pure classifiers are covered (
subordinate-connectivity7,coding-session-lifecycle7). Theuntested part is the orchestration, and specifically
opened— get that wrong in either directionand you either leak every session or destroy a session a human opened, which is #271 again from the
other side. The lost-race branch (
catch→ reuse winner →opened: false) is exactly the kind ofpath that regresses silently.