Skip to content

[FEATURE]: Session pause (soft / hard / kill) - a durable state that nothing can wake until resume #63875

Open

Description

@trin4ik

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/hard keep background jobs and kill is separate.

Current behavior (v2.3.0)

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 (queue and steer);
  • 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 test finished (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 paused with its mode.
  • Events: pause.requested, pause.settled (the whole tree is quiescent; for soft this can come later than the request), resumed.
  • The queued count is exposed.
  • Clients need settled to 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

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

  1. opencodejr commented on Nov 14, 2026

    @opencodejr
    Contributor

    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.interrupt only stops the current execution, so this is a feature request rather than a bug. In the plugin API I read, the session domain has interrupt but 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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/coreAgent loop, tools and configtype/featureNew behaviour or an improvementversion/v2OpenCode 2.x

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions