Skip to content

feat(sessions): pause a session — take the agent's memory, keep its conversation (v0.446.0) - #826

Merged
vikasprogrammer merged 1 commit into
mainfrom
feat/session-pause
Sep 17, 2026
Merged

vikasprogrammer merged 1 commit into
mainfrom
feat/session-pause

Conversation

@vikasprogrammer

Copy link
Copy Markdown
Owner

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 a paused row — that covers every path that types into a live pane (inject, deliverToResident, a pasted file, the wake-up inject lanes).
  • Every path that resurrects checks the status itself, because those read a false from reachable() as "the pane is gone, relaunch it" — the exact opposite of a pause. Those are reviveResident (the cold path behind Slack/Discord/ClickUp/Telegram thread continuity and DM continuity), chatSend, takeoverRun, takeoverToTerminal, reloadSession, and the wake-up resume lane in wakeups.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: sharedTerminalAuthz refuses 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 /resume route — 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. blockResume drops the same sentinel a stop does, so ttyd's auto-reconnect can't revive it either.

Why a status and not a flag over stopped

A paused run has not finished, and three roll-ups would have scored it as one:

Roll-up What stopped would have done
outcome.ts stamped it incomplete / stopped-midway
agent-stats.ts counted it against the maturity tier that gates unattended cross-agent edits
stale-session tidy archived a conversation somebody meant to come back to after 14 days

All 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. stopSession still 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

resident is preserved (a warm chat resumes warm), but an unattended run comes back attended and claimed by whoever resumed it — mirroring takeoverRun, and for the same reason: resume seeds no prompt, so left headless it 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

paused gets its own status role (a Pause glyph, sky — suspended, not halted) rather than borrowing halted'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 through TenantRegistry + createHttpServer: the routes are wired (401, not 404), an owner can pause, /attach 409s, /resume and /interactive are refused, the conversation stays readable, and unpause brings it back.
    • One trap the test itself had to dodge: launchAgentRuntime defers the spawn to a setImmediate, so counting backend.spawn calls made every "nothing relaunched" assertion pass for the wrong reason. It asserts on tm.launching, which is stamped synchronously.
  • scripts/session-revive-gates-test.cjs (extended) — it caught the first cut of this change: the ended expression referenced a local the test's lift couldn't resolve. Now lifts isPaused too 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 full npm run test:governance all 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

…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
vikasprogrammer merged commit 368c0fa into main Sep 17, 2026
1 check passed
@vikasprogrammer
vikasprogrammer deleted the feat/session-pause branch September 17, 2026 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant