Skip to content

Plugin API: stop a session until it is given work again (session.stop) #63650

Open

Description

@cevr

The fork carries it as branch subagent-stop
(cevr/opencode, commit 4cb2aea off prime-look eb39956), and the dotfiles subagents tool's stop uses it.

What a plugin can do today

ctx.session.interrupt({ sessionID }) ends the session's current turn. For a subagent session the host then settles
it as cancelled: SubagentReport.prepare resolves an interrupted outcome to { status: "cancelled" }, cancels the
subagent's own Job, and delivers one <subagent state="cancelled"> notice to the parent. That part works.

What it cannot do

  • The child's background work keeps running. Job.cancelSession(sessionID) runs only inside Session.remove,
    and it also deletes the child's own subagent job row (its report). A stopped child's background shell, when it
    ends, admits its notice to the child and wakes it. The child runs a new turn, and when it goes idle its parent gets
    a second report, state="completed": the parent is woken as if the child finished after it was stopped.
  • Queued input stays parked. interrupt without resume leaves queued prompts in the inbox, and the plugin
    SessionDomain has no inbox; the next wake runs them.
  • Any later wake reports again. A notice (a shell's end, a grandchild's report) or the owner prompting the child
    starts a period whose end reports to the parent.

The fork's change (proposed upstream)

  • Job.cancelOwned(sessionID): cancels, as cancel does one, each running job the session owns (its shells,
    images, its own subagents' jobs), not its own subagent job.
  • SessionExecution.stop(sessionID): marks the session stopped (KV session.stopped:<id>, durable across a
    restart), cancels its owned jobs, interrupts its turn and waits for it to settle. SessionExecution.stopped(id).
    A resume (the subagent tool giving the child a new task) clears the mark.
  • While a session is stopped: Session.synthetic, a subagent's report to it, and the restart sweep do not wake it;
    and SubagentReport.prepare reports nothing to its parent for a period no Job promised a report for (the stop's
    own cancelled settlement is promised by the subagent job, so it is the one notice).
  • Session.stop(sessionID): the execution stop, then cancels its inbox rows.
  • Plugin (effect) SessionDomain.stop({ sessionID }) => { interrupted }. The promise API is not extended.

Tests: packages/core/test/subagent-report.test.ts "a stop cancels the child's background work, reports cancelled
once, and silences the child until a resume"; dotfiles tests/kernel.test.ts "a cell's subagent handle stops a real
child mid-turn…" (a real child with a running background shell and a queued prompt).

Alternatives considered

Exposing the pieces (session.inbox, a job cancel by session, a report-suppress hook, a wake gate) leaves the
policy in each plugin and needs four hooks where one operation keeps it in the host.

Activity

  1. opencodejr commented on Nov 13, 2026

    @opencodejr
    Contributor

    Thanks for the detailed proposal and for linking the fork's implementation and tests.

    To restate it: you're asking for a host-side session.stop({ sessionID }) in the effect plugin API. It would end a session's turn, cancel the background jobs it owns, drop its queued input, and keep it from waking or reporting to its parent again until a resume gives it new work. Today interrupt only ends the current turn.

    I checked the current plugin API, and the effect SessionDomain has interrupt but no stop or inbox access. So a plugin can't do this yet. Maintainers will consider the proposal.

    Related:

  2. cevr commented on Nov 13, 2026

    @cevr
    Author

    Update from our fork: a stop interrupted by a server restart was undone, because the restart's resume of a turn a dead server left running cleared the stop mark and ran the session. We added a separate recovery path (SessionExecution.recover) that never clears the stop mark and never runs a stopped session; on restart a stopped session is closed as a user interrupt and keeps its mark (cevr/opencode branch subagent-stop-restart, commits 926373e, 63478c1). Whatever shape session.stop takes upstream, the restart sweep needs the same guarantee.

  3. trin4ik commented on Nov 14, 2026

    @trin4ik

    Very close to what we need. We wrote up the pause side here: #63875. Your stop is basically our kill; soft/hard keep background jobs running and queue their results.

    Both come down to one durable gate where turns get started, which every wake path goes through. The restart race you found shows why it has to be in one place. Might be worth looking at them together: one gate, with the policy (cancel vs keep and queue) as a parameter.

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/pluginsPlugins, skills, custom agentstype/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