Repository navigation
Description
#257 shipped its history half — loadRepoTimeline, migration 0077, repo-scoped fetch in the
console. Good fix. The session-lifecycle half was raised in a comment after that issue closed,
so it is currently untracked. This is that half.
Delegation is single-use
Two facts, both still true at 5f28af8:
// lib/loop-drivers.ts:117 — the driver REQUIRES a live session
const session = await getActiveSessionForRepo(env, instanceId, userId, repo.id);
if (!session) return { ok: false, status: 409, error: `${repo.name} has no live coding session…` };
// workflows/coding-session.ts:447 — the Pilot ENDS the session when its run completes
await endSession(env, instanceId, userId, sessionId, status);So every delegated job consumes the session it needed:
open a session by hand → ask for work → start_work → Pilot completes → Pilot ends session
→ ask again → 409 → open another by hand
Verified three times today on FAS platform (964594b6), runner online throughout:
| 03:44:50 | session opened (by hand) |
| 03:45:23 | start_work → Pilot ran git pull — worked |
| 03:45:36 | Pilot ended the session |
| ~05:40 | asked again → 409 no live coding session |
| ~06:05 | asked again after confirming → 409 again |
Also why the Coder Lead could not delegate to any of four subordinates (#259).
The error message tells the user to fix the wrong thing
loop-drivers.ts:122:
${repo.name} has no live coding session — start one on its Coding tab (and run \pags up`), then try again.`
The (and run \pags up`)is unconditional. It fires when the runner is **already connected** — verified at the moment of the 409:runnerOnline: true, relayConnected: true, reachable: true,
runnerVersion 0.4.32, lastSeenAt` current.
So a perfectly-behaved agent relays it faithfully and misinforms the user. Both retests above ended
with the agent telling the user to start a runner that was running. That is not the agent's fault —
it is repeating platform text that conflates two independent conditions.
Suggested shape
The design question first: should a completed Pilot run end the session, or only mark the run
done? Ending it is what makes delegation single-use, forces a fresh empty timeline per job, and
means a supervisor cannot supervise without a human opening a session first. Leaving the session
idle-but-live for the next goal removes all three at once.
If ending stays, the driver must be able to start a session itself when none is live and a
runner is connected — otherwise delegate_goal is only ever usable by someone sitting in the
console.
Either way, split the error by actual cause:
- runner not connected → say so, name the machine,
pags up - runner connected, no session → say that, and (once the above lands) just start one
Verification
- Two consecutive delegated jobs succeed without a human opening a session between them.
- With the runner online and no session, the message never mentions
pags up. - With the runner offline, it does — and names the node.
- The Coder Lead can delegate to an idle subordinate whose runner is connected.
Adjacent observation
The #256 run-history injection changed behaviour in a way worth watching: asked "Pull latest
changes" (an imperative), the agent answered about the 2-hour-old pull run and asked for
confirmation rather than acting. Grounded and honest — a clear improvement on denial — but it is
now possible for injected history to satisfy a request for new work. Worth a look if it recurs.
Activity
fix(coder): the Lead can see connectivity, and delegation stops being…
serge-ivo commentedon Aug 6, 2026
Fixed in
03f79b7(fix(coder): the Lead can see connectivity, and delegation stops being single-use (#259, #271)).Verified at HEAD before building — both cited lines still said what the report says.
loop-drivers.ts:117required a live session and returned the 409 with the unconditional "(and runpags up)";coding-session.ts:447ended the session in its terminalfinallystep.The lifecycle decision, and why
Both suggested shapes, because each alone leaves a hole:
1. A run ends only the session it opened. This is the ownership/lease notion, expressed as
sessionOpenedByRunon the workflow params and decided by one pure rule (shouldEndSessionAfterRun,lib/coding-session-lifecycle.ts).Answering your framing directly — least surprising to a human who opened a session by hand: ending someone else's session is the surprising half. Nothing else in the product closes a resource the user created, and the Pilot had no reason to be the first. The outcome is deliberately not an input to the rule: a failed run is the worst possible moment to take a session away, because that is exactly when the user wants to open the terminal and look at it.
Default is
false— "not mine". So an in-flight workflow and every existing caller (/sessions/:id/run, the Overseer'sdrive_claude) keep a session rather than lose one. Note this changes the console path too: hand a hand-opened session to the Pilot from the Coding tab and it is still there afterwards.2. Ownership alone still dead-ends on an agent nobody opened a session for — a supervisor could never supervise without a human sitting in the console first. So the driver opens one itself when the runner is connected, and by rule (1) that session is the run's, so it is cleaned up and the repo is left as it was found.
startSessionOnRunnermoved verbatim fromroutes/coding.tsintolib/coding-session-open.ts: it was private to a route, which is why the autonomous path had to refuse — the capability existed, it was just behind a URL.One consequence worth flagging:
endSessionis what normally frees the single-flight claim (#208). When the session survives, the Pilot now releases the claim explicitly — miss that and "delegation is single-use" becomes "delegation is once every 15 minutes" (STALE_DRIVER_MS).The error message
Split by actual cause, and connectivity is now checked first, from the same resolver delegation routes through (shared with #259's classifier, so a Lead's relayed refusal and the status it reads cannot contradict each other):
pags uppags up --force(plainpags upwould just be rejected 4409 again)<node>, so this is not apags upproblem", plus the runner's own error when/coding/startfailed (clone error, bad engine command)A test asserts the connected branch never emits anything that reads as an instruction to run
pags up. That sentence is what cost you hours.Tests
lib/coding-session-lifecycle.test.ts(7) for both rules.loop-drivers.test.tsgained: opens a session itself when none is live and the runner is connected (sessionOpenedByRun: true); does not claim ownership of a reused session (false); blames the runner only when it is actually unreachable. Its stub anddelegate-instance.test.ts's now model a registered, relay-connected runner — without that they would have silently asserted against the offline path.Full suite green: 245 files / 3553 tests,
pnpm typecheckandbiome checkclean on every touched file.Not verified
I could not re-test this live — no runner session available from here. The verification list in the issue (two consecutive delegated jobs with no human in between; the message never mentioning
pags upwith the runner online; the Lead delegating to an idle subordinate) is covered by unit tests against the real decision functions, but the end-to-end run is still worth doing.Not done
No migration. Ownership rides on a durable workflow param rather than a
coding_sessionscolumn — the only decision point is the Pilot's terminal step, and there was an active migration-number race with two other agents. If ownership ever needs to be visible (a console badge, a reaper that distinguishes abandoned agent sessions from human ones), a column is the right next step.Also untouched: the adjacent observation about #256 run-history injection satisfying a request for new work. Real, but a different bug — worth its own issue if it recurs.
fix(coder): something reaps an idle session, and a supervisor can see…
refactor(console): the Coding tab opens a REPO, and stops asking the …
fix(supervision): the second adapter forwards the pin too, so a pinne…
fix(coding): `getRunnerConn` asks the relay whether the machine is up…
fix(coding): a coding card follows the RUN, so a handoff reaches "Nee…
fix(coder): a relocated engine is briefed AND says so — the banner, t…