Repository navigation
Description
Reported
"Why do I need to always start the session manually? If it expires, can't the agent just start it
when I have a conversation with it?"
Because the chat path is the only path that cannot. This is not a policy decision — it is a missing
call.
The asymmetry, in two lines of code
lib/coding-session-open.ts:102 exports ensureActiveSession(env, instanceId, userId, repo). It
reuses a live session, re-attaches one whose engine process has gone (runner restarted, laptop
slept), opens a fresh one when there is none, and reports why when it cannot. It is idempotent and
it is already the thing that makes the Loop reliable.
It has exactly one caller: lib/loop-drivers.ts:181 — the Loop / Pilot path.
The chat tools do not use it. lib/storage-tools.ts:637 and :682:
const session = await getActiveSessionForRepo(env, ctx.agentId, ctx.userId, repo.id);
if (!session) return fail(call.name, `No active session for "${repoName}".`);So the same agent, in the same conversation, about the same repo:
- "go fix issue Document platform architecture #12" →
start_work→ loop driver →ensureActiveSession→ a session opens; - "what's the terminal showing?" →
read_terminal→ "No active session", and you are told to
go and press a button.
One of those spawns a process on your laptop. The other refuses to look at one. The path with the
larger consequence is the one that self-serves.
What a session actually is (for the ticket's readers)
A coding_sessions row plus a real child process on the owner's machine — the claude CLI
itself, spawned by the runner (HeadlessSession). Not tmux: tmux sessions survive forever because a
tmux server owns them and nothing reaps them. Here the platform owns a live OS process, and
coding-session-sweeper.ts:63 reaps it after IDLE_SESSION_MS = 6 hours with nothing touching
it — no capture poll, no message, no run, no Co-pilot turn, no timeline read. The header states the
reason plainly: otherwise a claude --dangerously-skip-permissions child stays resident
indefinitely, one per repo, accumulating context forever.
That policy is right and this ticket does not propose changing it. Six idle hours is not the
problem; needing a button afterwards is.
Do
- Chat tools that need a session should ensure one, via the same
ensureActiveSessionthe
Loop uses —read_terminal,send_to_cli, and anything else that currently fails on
getActiveSessionForReporeturning null. - Say so in the reply. "Started a session for
apps/chess-academy" — a process appearing on
someone's laptop should never be silent, and the agent is mid-conversation with them anyway. - Only when the runner is live. If there is no relay socket, the honest answer is the
runner-offline one ([bug] The Terminal tab learns the runner is gone only by failing — and blanks that error every 4s, so a phone shows an empty pane and a flicker #378's treatment), not a session attempt that cannot work. - Tell the user when a session is reaped. The sweeper appends to the coding timeline, which
is visible in the Co-pilot view and nowhere else — so from chat, a session simply stops
existing and the next question fails for a reason nothing stated. Same shape as [bug] Stop on a loop looks like it does nothing — the server records the cancel and no UI ever reads cancelRequested #376/[bug] Hands-free gives up after four failed mic restarts and says nothing — no notice, no error-log row, indistinguishable from a crash #387: the
platform knows and does not say.
Alternative considered and rejected
Keep it manual, because spawning a process on someone's machine deserves an explicit act. That
argument is real, and it is already lost: start_work from the same chat opens one without asking,
and so does the Loop button. The current split does not protect anyone — it only makes the harmless
half (reading a pane) harder than the consequential half (driving the engine). If an explicit act is
genuinely wanted, it belongs on the WRITE path, not on read_terminal.
Acceptance
- Asking a Repo Coder about its terminal with no live session opens one and answers, in one turn.
- The reply names the session it started.
- With the runner offline, the reply is the offline diagnosis, and no session row is created.
- The 6-hour idle reap is unchanged, and a reaped session is visible from the chat surface.
Files: workers/api/src/lib/storage-tools.ts:637,682, workers/api/src/lib/coding-session-open.ts:102,
workers/api/src/lib/loop-drivers.ts:181, workers/api/src/lib/coding-session-sweeper.ts:63.
Activity
fix(coding): chat opens its own coding session instead of sending you…
serge-ivo commentedon Aug 8, 2026
Shipped in
5768194, deployed to the API worker (run 31234351408, success;/healthgreen).What changed.
read_terminalandsend_to_cliinlib/storage-tools.tsno longer give up ongetActiveSessionForReporeturning null — they go through a newensureSessionForChatinlib/coding-session-open.ts, which wraps the sameensureActiveSessionthe Loop uses behind a connectivity gate. The gate is the load-bearing part:ensureActiveSessioncallscreateSessionbefore it discovers there is no runner and then ends the row, which is fine for a Loop that 409s either way and not fine for a read tool called repeatedly. Runner offline now returns the diagnosis and writes nothing.Acceptance, each with a test:
content).ensureSessionForChatand end-to-end through both tools), checkingcreateSessionandcallRunnerwere never called. The realcoding-session-open.tsruns in that test; only D1/relay/engine leaves are mocked, because the criterion is a fact about ordering and stubbing the module would have asserted nothing.IDLE_SESSION_MSis unchanged at 6h and pinned by a test that names the wrong inference ("chat can reopen it, so reap sooner"). The reap sentence was extracted behind a shared marker so a reword cannot silently mute the chat surface — no migration needed, the fact was already durable, just unreadable from the chat side.Verified:
pnpm typecheck(10/10),pnpm vitest run(373 files, 5969 tests, +13), biome, design tokens, migration hygiene — all re-run after rebasing ontomain, then pushed.Not verified live. The end-to-end check needs a machine running
pags up, which I do not start. On a Repo Coder with the runner up and no session, ask "what's the terminal doing?" and confirm the reply names a newly started session; then stoppags upand confirm the reply blames the runner andcoding_sessionsgains no row.Left undone, deliberately:
list_coding_reposdoes not mention a reaped session. It needs a per-repo query, and the reap is already reported at the moment it matters — the nextread_terminal. Say if you want that filed.Note: CI on
mainis red for an unrelated reason (the #384 WebKit Preferences guard, red for 12 commits before this one); the API deploy workflow is independent and passed.serge-ivo commentedon Aug 8, 2026
Verified already delivered — closing.
5768194is onorigin/mainand deployed. Re-checked against the code rather than the commit message:lib/storage-tools.ts:645,703— bothread_terminalandsend_to_clinow go throughensureSessionForChat, not the baregetActiveSessionForReponull-check this issue was filed against.lib/coding-session-open.ts:226—ensureSessionForChatchecksruntimeConnectivitybeforeensureActiveSession, so the runner-offline path returnsnoSessionMessage(...)and creates nocoding_sessionsrow. That ordering is the load-bearing part and it is the ordering in the file.sessionOpenedNoticenames repo, node, engine and session id, and carries the reap sentence whenlastIdleReapForRepofinds one.IDLE_SESSION_MSis untouched at 6h.All four acceptance boxes hold in code. Still not verified live — that needs a machine running
pags up, and both of the owner's machines are on stale CLIs (Mac0.4.40,Mac-mini0.4.22). The live check is unchanged from the shipping comment: with the runner up and no session, ask a Repo Coder "what's the terminal doing?" and confirm the reply names a session it started; stoppags upand confirm the reply blames the runner andcoding_sessionsgains no row.The one deliberate omission stands:
list_coding_reposdoes not mention a reaped session. File it separately if wanted.#408 is where the rest of this lives, and it is open.
feat(coding): a re-opened session continues the repo's conversation f…
refactor(console): the Coding tab opens a REPO, and stops asking the …
fix(coding): an idle repo goes to sleep instead of announcing a close…
fix(coding): one fenced dispatch contract for every engine-writing do…