feat(server,web): show background work that is still running, pinned above the composer (#604) - #807
Merged
Merged
Conversation
…above the composer (#604) A sub-agent already got a live row above the composer while it worked. Nothing else did. A background `Bash`, a `Monitor` or a workflow could run for minutes behind a card scrolled far up the transcript, and the only hint was a static `running` chip that meant "no completion notification was found in the transcript" — not "we checked". A killed task kept that chip forever. And the chat claimed to be idle while it happened. `chat:active` reports one hub turn, and a background task outlives the turn that launched it, so the moment the reply landed every consumer of that signal — the sidebar dot, the Home in-flight badge, the running-only filter, the composer — was told the session had stopped while the work continued. That is the defect this closes. The signal already existed and we were dropping it. The SDK publishes `background_tasks_changed` (the complete live set on every membership change) plus `task_started` / `task_progress` / `task_notification` / `task_updated` for per-task detail, and `BackgroundTaskSummary` already enumerates shell | subagent | monitor | workflow. herdctl taps that stream to decide reaping; Paddock's consumption was four comments and no code. - `BackgroundRegistry` folds those messages into a per-session live set, fed from all five turn paths. The level signal is the sole authority on membership and edges may only enrich, so a missed edge cannot wedge a stale row — the #528 failure mode. Terminal edges evict ahead of the confirming level. - `chat:background` broadcasts the set and is replayed on connect, so a remount is populated on the first paint rather than after a poll. - `chat:active.running` now accounts for background work. - `RunningWork` replaces `RunningSubagents`, rendering shells, monitors and workflows alongside sub-agents. A sub-agent the transcript path already shows is not duplicated; `skip_transcript` work is hidden. `chat:active` also gains `turnRunning`. Making `running` true whenever background work exists is what the issue asks for, but QA showed the cost: after the turn finished, the working indicator still animated and the composer stayed locked to "Queue a message to send next…". An hour-long Monitor would make the chat unusable while claiming the model was thinking. Status readouts now read `running`; the composer lock and working indicator read `turnRunning`. Optional on the wire, so an older client falls back and behaves as before. Session drive mode only — the CLI runtime reads the transcript file, which these stream-only control messages never reach. A new `[[BGTASK]]` fake-claude directive drives the whole path with no API spend. Co-Authored-By: Claude <noreply@anthropic.com>
Deploying paddock with
|
| Latest commit: |
d8a3c72
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://150a6027.paddock-7u2.pages.dev |
| Branch Preview URL: | https://feat-604-background-work-reg.paddock-7u2.pages.dev |
Merged
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #604. Design and rejected alternatives: #604 (comment)
The problem
A sub-agent gets a live row above the composer while it works. Nothing else does. A background
Bash, aMonitor, or a workflow can run for minutes behind a card scrolled far up the transcript, and the only hint is a staticrunningchip that means "no completion notification was found in the transcript" — not "we checked". A killed task keeps that chip forever.And the chat claims to be idle while it happens.
chat:activereports one hub turn; a background task outlives the turn that launched it. So the moment the reply lands, every consumer of that signal — sidebar streaming dot, Home in-flight badge, running-only filter, the composer's own state — is told the session stopped while minutes of work continue. That is #604.Where the signal was
The Agent SDK already publishes exactly this, and we were throwing it away.
background_tasks_changedcarries the complete live set on every membership change, withtask_started/task_progress/task_notification/task_updatedadding per-task detail;BackgroundTaskSummaryalready enumeratesshell | subagent | monitor | workflow. herdctl taps that stream to decide session reaping (session-hooks.js:137). Paddock's entire consumption was four comments and no code.Meanwhile #750 spent real effort reconstructing one field of it from disk — scraping each sub-agent transcript for an
end_turnmarker with a ten-minute stale backstop — whichtask_notificationreports directly, and which covers only sub-agents.What this does
BackgroundRegistry(background-live.ts) — a per-session live set fed from raw SDK messages.background_tasks_changedreplaces the set. This is the SDK's own guidance ("a missed bookend cannot wedge a stale running indicator") and it is load-bearing: A chat is permanently wedged "running" when a background task never exits — Stop is a structural no-op and nothing self-heals #528 was precisely such a wedge. Edge-pairing would reintroduce it.task_started/task_progressenrich in place. An edge may never create a row, so a leaked or late edge cannot resurrect finished work.chat:backgroundframe — broadcast on change, replayed on connect. That replay is the whole rehydration story: no polling, no arming heuristic. A pane that mounts mid-run is populated on the first paint.chat:activenow tells the truth —runningisturn.running || background.isBusy(session).RunningWorkreplacesRunningSubagentsin the same slot. Sub-agents keep coming from the existing transcript-derived path (richer per-step detail, already tested); shells, monitors and workflows come from the registry. A registrysubagentrow already shown by the transcript path is dropped so the two sources cannot double-render; one the transcript path has not found still appears. Ambient work flaggedskip_transcriptis hidden.The signal split — please review this bit
Making
runningtrue whenever background work exists is what the issue asks for, and my first working build did exactly that. QA showed the cost immediately: after the turn had finished, the working indicator still animated and the composer was locked to "Queue a message to send next…". An hour-longMonitorwould have made the chat unusable while telling the user the model was thinking.That is the overloaded-
runningproblem — one boolean answering "is a turn in flight?", "is this chat busy?" and "should the composer lock?".So the frame now carries
turnRunningalongsiderunning:runningturnRunningturnRunningis optional on the wire; an older client falls back torunningand behaves as before. This is a partial, deliberately minimal move toward the separation — it does not restructure the state machine.Verification
32 new tests, plus a new
[[BGTASK]]fake-claude directive so all of this is exercised with no API spend.background-live.test.ts) — replace semantics, edge-cannot-create, enrichment surviving the next level, terminaltask_updated,startedAtnot restamped, unknown task types kept.RunningWork.test.tsx) — all four prior sub-agent behaviours preserved, plus the new rows, dedup,skip_transcript, elapsed, and inertness without atoolUseId.ws-background.test.ts) — the frame, edge enrichment, REPLACE shrinking the set,chat:activestaying true pastchat:complete, and connect-time replay to a socket that joins mid-run.The #604 guard was verified to fail without the fix. Reverting the
chat:activejoin makeskeeps chat:active running:true while background work is in flightfail while the other two still pass.Manually QA'd on a
pminstance driven through Playwright (batch drive mode, fake claude, tokens unset, isolatedHOME— no API spend, no access to real transcripts). States confirmed in the live UI:2 THINGS RUNNING—shell wait for CI 0:06,monitor errors in deploy.log 0:06chat:complete1 THING RUNNING— shell gone via REPLACE, monitor still ticking; fleet strip1 RUNNING; composer usable0:52— elapsed continuous, sostartedAtsurvivesinforeads distinctly fromaccentScope and caveats
waitForJobs: false, so those tasks are dead. The old chip went on claiming a killed task was alive.reapChatSession). WiringTaskStopper row would let you kill one runaway monitor while sub-agents keep working — a good follow-up.RunningSubagentsis deleted, and the E2E testidrunning-subagentsis renamed torunning-work.