Repository navigation
Description
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 insideSession.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.
interruptwithoutresumeleaves queued prompts in the inbox, and the plugin
SessionDomainhas noinbox; 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, ascanceldoes 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 (KVsession.stopped:<id>, durable across a
restart), cancels its owned jobs, interrupts its turn and waits for it to settle.SessionExecution.stopped(id).
Aresume(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;
andSubagentReport.preparereports 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
opencodejr commentedon Nov 13, 2026
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 aresumegives it new work. Todayinterruptonly ends the current turn.I checked the current plugin API, and the effect
SessionDomainhasinterruptbut nostopor inbox access. So a plugin can't do this yet. Maintainers will consider the proposal.Related:
cevr commentedon Nov 13, 2026
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 branchsubagent-stop-restart, commits 926373e, 63478c1). Whatever shapesession.stoptakes upstream, the restart sweep needs the same guarantee.trin4ik commentedon Nov 14, 2026
Very close to what we need. We wrote up the pause side here: #63875. Your stop is basically our
kill;soft/hardkeep 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.