feat(sessions): pause a session — take the agent's memory, keep its conversation (v0.446.0) - #826
Merged
Merged
Conversation
…onversation (v0.446.0) A new session status, `paused`: neither live nor terminal. Pausing kills the agent's claude — which is what actually frees the ~500 MB of context, the runtime-account slot and the concurrency-cap slot — and leaves the transcript on disk, so Resume relaunches on the SAME conversation with the full context back (`claude --resume`). While paused the session is readable and nothing else. `reachable()` refuses a paused row, which covers every path that types into a live pane; every path that RESURRECTS checks the status itself, because those read a false from `reachable()` as "the pane is gone, relaunch it" — the opposite of a pause. Those are `reviveResident` (Slack/Discord/ClickUp/Telegram thread + DM continuity), `chatSend`, `takeoverRun`, `takeoverToTerminal`, `reloadSession`, and the wake-up resume lane, where a finishing delegate's news is kept QUEUED for the resume rather than dropped. The ttyd WebSocket authz refuses a paused id (a tab left open never passes through /api/sessions/:id/attach), and the stop-block /resume route refuses one too — attaching would resurrect the agent by the back door and leave the status lying. A pause is deliberately not an ending: no episode, no completion card, and a status no roll-up scores — `outcome.ts` would have read it `incomplete`/`stopped-midway`, `agent-stats` would have counted it against the maturity tier that gates unattended cross-agent edits, and the 14-day tidy would have archived a conversation somebody meant to come back to. Stop stays available on a paused run. Resume preserves `resident` but brings an unattended run back attended and claimed, mirroring `takeoverRun`: it seeds no prompt, so left headless it would be idle-reaped within the hour and the person who pressed Resume would watch it vanish. Pinned by scripts/session-pause-test.cjs (62 checks incl. a real-HTTP round trip) and an extended scripts/session-revive-gates-test.cjs (a paused session offers neither attach-Resume nor Take over, and always renders read-only). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vikasprogrammer
force-pushed
the
feat/session-pause
branch
from
September 17, 2026 10:54
edf8ffa to
1511bc9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A new session status,
paused— the only one that is neither live nor terminal.Pausing kills the agent's claude. That is the point: killing the process is what frees the ~500 MB of context, the runtime-account slot and the concurrency-cap slot. The conversation is untouched, because claude's transcript is a file on disk — so Resume relaunches on the SAME conversation (
claude --resume <pinned id>) with the full context back.While paused, the session is readable and nothing else: its conversation timeline scrolls as usual, and nothing can hand the agent a process back except a deliberate Resume.
The invariant, and why it needs two layers
reachable()refuses apausedrow — that covers every path that types into a live pane (inject,deliverToResident, a pasted file, the wake-up inject lanes).reachable()as "the pane is gone, relaunch it" — the exact opposite of a pause. Those arereviveResident(the cold path behind Slack/Discord/ClickUp/Telegram thread continuity and DM continuity),chatSend,takeoverRun,takeoverToTerminal,reloadSession, and the wake-up resume lane inwakeups.ts, where a finishing delegate would otherwise start a fresh claude on the paused transcript. That last one keeps the wake-up pending rather than dropping it, so the news is delivered by the resume.Two edges outside the manager:
sharedTerminalAuthzrefuses the ttyd WebSocket for a paused id (a tab left open or a pasted terminal URL never passes through/api/sessions/:id/attach), and the generic/resumeroute — which only lifts the stay-stopped sentinel — refuses a paused row, since letting the terminal attach would resurrect the agent by the back door and leave the status lying.blockResumedrops the same sentinel a stop does, so ttyd's auto-reconnect can't revive it either.Why a status and not a flag over
stoppedA paused run has not finished, and three roll-ups would have scored it as one:
stoppedwould have doneoutcome.tsincomplete/stopped-midwayagent-stats.tsAll three now exclude it. Pause also writes no episode and posts no completion card — an episode would tell Dreaming and the consolidator the run is over.
stopSessionstill works on a paused row: that is how you say "I'm not coming back", and it writes the episode and clears the paused stamp.Resume semantics
residentis preserved (a warm chat resumes warm), but an unattended run comes back attended and claimed by whoever resumed it — mirroringtakeoverRun, and for the same reason: resume seeds no prompt, so leftheadlessit would be idle-reaped within the hour and the person who pressed Resume would watch the session they just brought back disappear.Pausing is refused where it would be a one-way trip: a run with no live pane ("nothing to pause") or no resumable conversation (told to stop instead, and its pane is left alone).
Console
pausedgets its own status role (aPauseglyph, sky — suspended, not halted) rather than borrowinghalted's stop sign, plus a filter, a sort slot directly under the live rows, and Pause/Resume in the terminal's Operations menu. A paused session always renders its read-only transcript with a Resume button in the header — an attended, resumable one would otherwise attach to a terminal that never opens.Testing
scripts/session-pause-test.cjs(new, in the gate) — 62 checks over an isolated home with a stubbed backend, including a real-HTTP round trip throughTenantRegistry+createHttpServer: the routes are wired (401, not 404), an owner can pause,/attach409s,/resumeand/interactiveare refused, the conversation stays readable, and unpause brings it back.launchAgentRuntimedefers the spawn to asetImmediate, so countingbackend.spawncalls made every "nothing relaunched" assertion pass for the wrong reason. It asserts ontm.launching, which is stamped synchronously.scripts/session-revive-gates-test.cjs(extended) — it caught the first cut of this change: theendedexpression referenced a local the test's lift couldn't resolve. Now liftsisPausedtoo and pins that a paused session offers neither attach-Resume nor Take over and always renders read-only.npm run typecheck,cd web && npm run build, and the fullnpm run test:governanceall green.Not covered
Under
AOS_UID_ISOLATION(off by default; Linux only) the per-member ttyd proxy resolves by space, not per session, so per-session authz there is nginx's job. A paused session in that lane is still protected by the stay-paused sentinel — attach.sh refuses to resurrect it — but the refusal surfaces as tmux's own error rather than a clean 409.🤖 Generated with Claude Code