Repository navigation
Conversation
Multiplexer panes re-composite pane output at the multiplexer's own cadence, so the TUI's animated repaint stream (tab pulse, marquee, shimmer, tween ticks) turns into continuous full-region recomposites on the multiplexer client, visible as flicker and shaking on remote or slow links (anomalyco#57462, anomalyco#57676). Resolve animations in resolve() the same way tabs.enabled is already environment-aware: default off when HERDR_ENV=1, on otherwise; an explicit animations setting always wins. The mini footer queue test pinned its expected statusline glyph via the test harness coercing unresolved animations to false; resolve() now always defines the field, so the test declares animations: false explicitly for its still-glyph assertion.
Interrupting a session only cancelled the job keyed by the session's own id, which covers a session running as a subagent job. Background shell and image jobs are keyed by their shell/image id and only carry the owning session in job metadata, so jobs.cancel(sessionID) missed them and the work kept running; the completion notice then woke the interrupted session for a new turn (anomalyco#60107). Add Job.cancelAll({ sessionID, type? }) that cancels running jobs owned by the session (metadata.sessionID match, mirroring backgroundAll's shape) and call it next to the existing cancel when the run coordinator settles on a non-shutdown interrupt.
ad19971 to
b1a0615
Compare
There was a problem hiding this comment.
The interrupt now kills the session's background shell, but the interrupted session still starts a new turn, so the user-visible part of #60107 isn't fixed yet.
I checked this with a simulated-model drive script that follows the #60107 reproduction (background sleep 8 && touch marker, then interrupt during the next turn):
- Base: the shell keeps running, the marker is written, and the
state="completed"notice starts a new model turn. - This PR: the shell is cancelled and no marker is written. But a
<shell … state="cancelled">Cancelled</shell>notice is delivered right away and immediately starts a new model turn on the interrupted session.
The description says cancelled jobs follow a "not-admitted" path and won't wake the session, but nothing in the shell notice path does that (see the inline comment). job.test.ts and session-execution.test.ts pass, and tsc -b passes.
Public API: adds cancelAll to the internal Job.Interface in core only. The HTTP API, SDK, plugin API and events are unchanged. The bundled herdr change (below) adds an environment-dependent default for the TUI animations setting.
Other concerns:
- Unrelated commit: this PR still includes
fix(tui): default animations off inside herdr panes(packages/tui/src/config/index.tsxand two TUI tests), which is the same change as #60111. Please drop it from this PR. - Behaviour change for maintainers: the TUI's double-Esc stop and the web composer both call interrupt with
resume: true. With this change, every stop also kills background shells the model started, including long-running ones like dev servers. Background subagents the session started are still left running, because their job metadata has nosessionID. Interrupting an idle session (no active run) also cancels nothing, since this runs only when an active run settles.
| // A session running as a subagent job is keyed by its own id; owned | ||
| // background work (shell, image) carries the session in job metadata. | ||
| yield* jobs.cancel(sessionID) | ||
| yield* jobs.cancelAll({ sessionID }) |
There was a problem hiding this comment.
This cancels the shell, but notifyWhenDone in tool/plugin/shell.ts then posts the state="cancelled" notice through sessions.synthetic(...) without resume: false. Session.synthetic calls execution.wake unless resume === false, so the session starts a new turn as soon as the interrupt settles. I reproduced this with the #60107 script: model request #3 carries <shell … state="cancelled">Cancelled</shell>. The image job's notifier probably behaves the same way. For the interrupt to actually stop the session, the cancelled notice should be recorded without waking it, and a test should cover interrupt → no new turn.
There was a problem hiding this comment.
This push only rebases the PR onto newer dev. The two commits are unchanged, so the open point about the cancelled shell notice waking the session still stands. I re-ran the #60107 reproduction on the new head with the simulated model. The interrupt kills the shell, but a new model turn still starts right away carrying <shell … state="cancelled">Cancelled</shell>.
The unrelated herdr animations commit (the same change as #60111) is still included. The public API is unchanged since the last review.
Issue for this PR
#60107 (fixes the interrupt half; see below)
Type of change
What does this PR do?
Interrupting a session only cancelled the job keyed by the session's own id, which covers a session running as a subagent job. Background shell and image jobs are keyed by their shell/image id and only carry the owning session in
job metadata, sojobs.cancel(sessionID)missed them: the work kept running and its completion notice later woke the interrupted session for a new turn.This adds
Job.cancelAll({ sessionID, type? }), which cancels running jobs whosemetadata.sessionIDmatches (same shape asbackgroundAll, but keyed on ownership rather than blocking sessions, which are cleared once work is backgrounded), and calls it next to the existingjobs.cancel(sessionID)when the run coordinator settles on a non-shutdown interrupt.Scope note: this fixes expected behavior 1 from the report in full. For expected behavior 2, jobs cancelled through this path end
cancelledrather thanerror, so they follow the existing not-admitted notification handling and should not wake the session; theDELETE /api/shell/:id→Shell.NotFoundErrornotice path is separate and not changed here.How did you verify your code works?
packages/core/test/job.test.ts: owned-by-session cancellation leaves foreign and unowned running jobs alone (and lets them complete normally after), and the optionaltypefilter. Red before the change, green after.job.test.ts17/17; related suites (session-execution,tool-shell,subagent-lifetime,tool-subagent,session-shell) 127 pass / 0 fail.packages/coresuite: 6,993 pass; the only remaining failures are four root-in-container artifacts (two flock "unwritable lock root" tests, global-home isolation, one external-plugin spawn) reproduced identically without this change.bun run typecheckclean (1,738 files).Checklist
Added by the opencode bot (AI-generated).
Related: #60107, #36424, #34366, #54658, #60111