Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Summary
OpenCode v2 has no way to pause a session. interrupt stops the current turn, but the session (or any subagent in its tree) is started again by the next event: a background shell finishing, a subagent report, a retry timer, a queued prompt. We'd like a first-class paused state on the session tree: while paused, nothing can start a turn. Everything that arrives is queued, and an explicit resume releases the queue in one go.
We propose three levels, by analogy with signals:
| Mode | Current turns | Running tool calls | Background shells / jobs | New input while paused |
|---|---|---|---|---|
soft |
finish the current step (model output or tool call), no next step | finish | keep running, results queued | queued |
hard |
interrupted now, across the whole tree | detached to background, results queued | keep running, results queued | queued |
kill |
interrupted now, across the whole tree | cancelled | cancelled | queued |
soft and hard are pause: no work is lost and the session continues after resume. kill is closer to the stop proposed in #63650, but it keeps the session in the same paused state, so the user can still resume it.
Resume after kill (open question). After soft/hard, resume delivers the results of background work that kept running. After kill that work was cancelled, although the agent started it and expects its results. So on resume, cancelled background shells and jobs should possibly be restarted (same command, same owning session), or at least offered for restart. If they aren't restarted, the resume message must explicitly list what was cancelled by kill, so the model doesn't wait for results that will never arrive. We're not sure which default is right; it could be a resume option (restartKilled: true).
Why
- Users expect a pause button: "stop everything now, I want to look / think / fix something, then continue". Today a client can't honestly offer it. After the user presses "pause", a background shell finishing a minute later wakes the model, which spends tokens and edits files while the user believes everything is stopped.
- Products that embed OpenCode as a backend (custom UIs, orchestrators, bots) need this most. Their users are often not developers and won't reason about background jobs and wake semantics. "Paused" has to mean paused.
- Background work is valuable. A 2-hour test run shouldn't be thrown away just because the user wants the agent to stop thinking. So pause must not equal cancel; that's why
soft/hardkeep background jobs andkillis separate.
Current behavior (v2.3.0)
interrupttakes onlyresumeand is not a durable gate: protocol/src/groups/session.ts#L679-L694.- A background shell completion sends a synthetic message (tool/plugin/shell.ts#L141-L173), which by default wakes execution (session/session.ts#L286-L324). This happens after
interrupttoo. - Subagent reports wake the parent (session/execution/subagent-report.ts#L74-L95). Since 2.3.0 subagents always run in the background (tool/plugin/subagent.ts#L194-L224), so interrupting a parent doesn't stop its subtree.
resume: falseon an individual synthetic message helps, but the built-in completion paths don't use it, and it's not a session-wide guarantee.- There is no atomic way to cancel all background jobs of a session tree. Listing jobs with
descendants=trueand cancelling them one by one races with completions that wake the session.
Proposed behavior
1. One paused state, one gate. The session (and its whole tree) gets a durable paused status. The check lives in the single place where turns are started (run coordinator / execution wake), not in each event handler. Every entry point goes through it:
- user prompts (
queueandsteer); - synthetic messages (background shell completion, subagent reports, cancellation notices);
- provider retry timers;
- automatic compaction;
- recovery after server restart;
- anything a plugin triggers.
While paused, all of these are admitted to the owning session's inbox (the session where the shell or subagent was started) and nothing is executed. Pausing at the exact moment a turn is starting must resolve deterministically: either the turn didn't start, or it's treated as a current turn and handled by the selected mode.
2. The whole tree. Pause applies to the session, its subagents, their subagents, and so on at any depth. No new subagent turn starts while the root is paused. Resume on the root resumes the tree.
3. Running tool calls on hard. A tool call that is executing (e.g. a foreground shell) isn't killed. It's detached to background like /background, and its result goes to the inbox. kill cancels it.
4. Pending human input. Open permission requests and questions stay open while paused and can be answered. The answer goes to the inbox and doesn't start a turn.
5. Resume. resume releases the queue as one wake, not one turn per item. Items are delivered in arrival order and annotated with their source, so the model understands what happened while it was paused, e.g.:
While you were paused, the following arrived (in order): background shell
npm testfinished (exit 0); subagent "reviewer" reported; user message: "...".
An atomic "resume and send this prompt" is also useful, so clients don't race between two calls.
6. Visibility and persistence.
- Pause survives a server restart.
- Session status shows
pausedwith its mode. - Events:
pause.requested,pause.settled(the whole tree is quiescent; forsoftthis can come later than the request),resumed. - The queued count is exposed.
- Clients need
settledto tell the user "everything has actually stopped".
API sketch:
POST /api/session/:id/pause { "mode": "soft" | "hard" | "kill" }
POST /api/session/:id/resume { "prompt"?: ..., "restartKilled"?: boolean } // optional atomic resume + prompt; restart jobs cancelled by kill
GET /api/session/:id -> status: { paused?: { mode, requestedAt, settledAt?, queued } }
Escalation should work: soft -> hard -> kill while already paused (e.g. a soft pause is waiting on a long tool call).
Why not a plugin
We tried to see how this could be built on top of OpenCode. A plugin can observe a wake and interrupt again, but by then the model or a tool may already have acted: that's a race and gives "phantom" behavior, sometimes stopped, sometimes not. A plugin also can't hold synthetic deliveries, retry timers or restart recovery in a queue. This needs a durable gate in the core execution path.
Related issues
- [FEATURE]:Add Suspend/Resume functionality to pause Agent and Subagent execution #27511 Suspend/Resume - the closest idea, but it's from the v1 era and pauses after the current tool call only. It doesn't cover the session tree, background completions waking the session, queued delivery, or persistence.
- Plugin API: stop a session until it is given work again (
session.stop) #63650session.stop- a durable gate too, but it cancels owned jobs and clears the inbox. That matches ourkill, not pause. A shared gate could serve both. - v2: shell: interrupting a session doesn't cancel its background shell jobs #60107 - background shells survive
interruptand their completion wakes the session. This is the core reasoninterruptcan't be used as pause. - v2: subagent: no cancellation support for background subagents #36423 - no cascading cancellation of background subagents, and the cancellation notice wakes the parent again.
- PR fix(core): cancel a session's background shell jobs on interrupt #60119 - cancels shells on
interrupt. That fixes v2: shell: interrupting a session doesn't cancel its background shell jobs #60107 for "stop" semantics, but it would makeinterrupteven less suitable as a pause that keeps background work. We'd suggest keepinginterruptas is and adding pause as a separate state.
Context
We build a server on top of OpenCode v2: OpenCode is the agent backend, and we write our own frontend for non-developer users (chat UI, bots embedded in business apps). For them "pause / play" is a basic control, and we can't implement it reliably from outside the core. We're happy to help test or contribute if the maintainers agree on the shape.
Activity
opencodejr commentedon Nov 14, 2026
Thanks for the detailed proposal, @trin4ik, and for offering to help test and contribute.
To restate it: you're asking for a lasting paused state on a session and all its subagents. While paused, nothing can start a turn: prompts, background shell completions, subagent reports, retry timers, compaction and restart recovery all go to the inbox. An explicit resume releases them as one wake. There would be three levels (
soft/hard/kill), and the paused state would survive restarts, with its own status and events.Today
session.interruptonly stops the current execution, so this is a feature request rather than a bug. In the plugin API I read, the session domain hasinterruptbut no way to hold a session's wakes. So I agree a plugin can't do this reliably; it needs to be done in the core. Maintainers will consider it.Related issues:
session.stop) #63650: a lastingsession.stopgate that cancels the session's background jobs and clears its inbox. It's close to yourkill, and a shared gate could serve both.interrupt, and their completion wakes the session.