12 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/platform/background-work-liveness.md | 2026-07-21 | in_progress |
Implementation Plan: Background work liveness
Overview
Correct the busy signal by separating foreground ownership, launch-tool completion, and launched-workload completion. The adapter first exposes the detached workload lifecycle, the orchestrator then retains it across foreground turn completion, and the existing frontend indicators consume the corrected session/task aggregate. The final E2E scenario must end the foreground turn before the background workload ends; the current mock holds one turn open and therefore cannot catch the reported false-done regression.
The ACP evidence for the design is reproducible with acpdbg: Claude ACP 0.60
returns a terminal Bash tool update containing
_meta.claudeCode.toolResponse.backgroundTaskId and an output-file path, while
the provider explicitly promises a later task notification. Async subagents use
the parallel agentId / outputFile launch metadata. The human-origin
usage_update arrives after the foreground reply even when session/prompt
remains held open for that subagent; the eventual autonomous completion cycle
ends with _meta._claude/origin.kind=task-notification. A terminal launch-tool
frame therefore cannot be treated as terminal evidence for either foreground
idleness or detached-work completion.
Lifecycle hardening amendment (2026-07-25)
The next pass replaces global normalized-shape recognition with typed adapter-issued background-work attestation. Claude retains its covered subagent, background-shell, and Monitor paths; Codex subagents opt in after terminal child activities are correlated by child thread ID across ACP tool-call IDs. Other adapters remain conservatively foreground-busy.
The orchestrator extends each live registration with workload kind and derives
active_subagent_count from the registration map. Session REST/boot/WS payloads
carry the per-session count, task payloads sum it across sessions, and count-only
changes publish even if foreground_activity stays generating or background.
The count is runtime-only and does not include background shells or Monitor
watches. This pass does not add a count badge or subagent list to the UI.
Backend
ACP detached-work lifecycle
- Extend
apps/backend/internal/agentctl/types/streams/agent.gowith protocol-neutral foreground-yield and detached-background completion events. Keep tool-card status unchanged: a Bash or Task launch can be complete while its workload remains live. - In
apps/backend/internal/agentctl/server/adapter/transport/acp/, recognize Claude's human-origin and task-notification-origin usage boundaries. Retain the originating top-level tool-call registration across prompt completion. - Convert human-origin usage completion into an explicit foreground-yield event and task-notification-origin usage completion into one workload-completion event. Claude ACP does not expose the completed task ID on the latter frame, so the orchestrator retires one outstanding registration per notification; an ambiguous remainder stays live rather than falsely showing done. Monitor notification routing continues updating its own correlated tool card.
- Prompt-end active-tool sweeps continue closing launch cards but do not invent detached-work completion. Agent execution teardown drains any remaining registrations and publishes terminal cleanup.
Session activity state machine
- Refactor
apps/backend/internal/orchestrator/turn_activity.goso foreground ownership and background registrations are independent. The read order is: prompt claim/top-level foreground activity → generating; otherwise any background registration → background; otherwise idle/default. - Replace the turn-close reset in
apps/backend/internal/orchestrator/service.go::completeTurnForSessionwith a foreground-idle transition. Reserve full activity deletion for execution stop, cancellation/teardown, and session removal paths. - Handle detached-background lifecycle events separately from tool-card terminal updates. A launch-tool completion must not remove its workload; an ID-bearing workload completion removes its registration, while an uncorrelated provider completion retires one session registration and fails closed when ambiguity remains.
- Preserve atomic prompt admission: a foreground claim always wins over background liveness, concurrent claims admit exactly one prompt, and a failed claim restores background-idle only when its generation/epoch is still current.
DTO, aggregate, and live updates
- Update session DTO enrichment so
foreground_activity=backgroundmay be emitted for a settled coarse session when detached work remains. Unknown activity on aRUNNINGsession still defaults to generating. - Update task most-active-wins aggregation to rank generating
RUNNINGsessions first, then every session with live background work, then existing settled-state behavior. - Publish
session.activity_changedand task refreshes for the foreground-turn close → background and final background → idle transitions, including after the coarse session has leftRUNNING.
Frontend
Shared session flags and store transitions
- In
apps/web/hooks/domains/session/use-session-state.ts, derive background work fromforeground_activityindependently of coarseRUNNINGstate.isAgentBusyremains foreground-only;isWorkingincludes background work. - Audit
session.state_changedandsession.activity_changedreducers so a coarse settled transition does not erase a concurrently supplied background value, and final liveness completion clears the working affordance. - Keep the existing indicators and layout. This is shared state/data normalization, so desktop and mobile use the same view model with no new composition, touch, navigation, or responsive behavior.
Sidebar background spinner
- In
apps/web/components/task/task-item.tsx, render background work with the sameIconCircleDashedspinner used for foreground running, using violet instead of yellow. - Preserve
task-state-background-runningand add the distinct “Background work is running” tooltip/accessibility name. Do not change the board, graph, header, or chat status icon vocabulary in this pass. - Treat this as a shared compact-row styling change. The nearest mobile exemplar is the existing mobile task switcher/sidebar row; verify the shared component at mobile width without introducing desktop-only composition.
Tests
- Foreground precedence: background + foreground output/claim reads
generating and queues a second simultaneous prompt. File:
apps/backend/internal/orchestrator/foreground_busy_signal_test.go. - Turn outlived: turn completion with a registered detached workload reads
background and stays promptable; only workload completion reads idle. Files:
foreground_busy_signal_test.go,foreground_activity_signal_test.go. - Launch versus workload terminal: terminal Bash/async-Task launch updates
preserve detached liveness; human-origin usage yields the foreground;
task-notification-origin usage retires one workload; unknown or duplicate
notifications are harmless. Files under
apps/backend/internal/agentctl/server/adapter/transport/acp/andforeground_busy_signal_test.go. - Task aggregate: a non-
RUNNINGsession with live background work outranks done, while any generating session outranks background. Files:apps/backend/internal/task/service/task_activity_test.goand DTO tests. - Frontend flags/store: settled+background is working but not busy;
RUNNING+generating is busy; foreground always wins; final completion is idle. Files:use-session-state.test.tsand relevant session WS slice tests. - Adapter trust: presentation-only subagent/shell payloads do not relax the
gate; only Claude/Codex adapter fixtures stamp trusted lifecycle provenance.
Files: stream payload tests, ACP normalizer/dialect tests, and
foreground_busy_signal_test.go. - Codex cross-ID terminal correlation: start under one tool-call ID followed
by completed/interrupted/cancelled/error/shutdown/not-found child activity
under another retires the original registration exactly once; controls do not
create a second card. Files:
dialect_codex_test.go,subagent_test.go, and focused orchestrator lifecycle tests. - Active subagent count: one typed registration per live subagent; duplicate
terminal events are harmless; shells/Monitor do not count; execution teardown
clears registrations; session and task DTOs expose exact count on fresh load;
count-only WS/task updates publish. Files: stream payload tests,
background_work_accounting_test.go, foreground activity fresh-load/signal tests, and task DTO/service tests. - Sidebar spinner: background uses
IconCircleDashed, violet spinning classes, a distinct test ID, and an accessible background label while generating stays yellow. File:apps/web/components/task/task-item.test.tsx.
E2E Tests
- Add a mock-agent command that launches background work, completes its foreground turn immediately, emits a later terminal notification, and does not hold the prompt RPC open.
- Update
apps/web/e2e/tests/chat/busy-signal.spec.tsto assert, after the foreground turn settles, that the green background spinner remains, the composer sends immediately rather than queues, a new foreground prompt shows orange for its duration, and the indicator returns to background until the detached workload ends. - Update
apps/web/e2e/tests/chat/mobile-busy-signal.spec.tswith the same foreground-ended/background-live outcome at Pixel 5 width. No mobile layout contract changes; this covers the shared state normalization already rendered by the existing mobile surface. - Re-run desktop and Pixel 5 busy-signal scenarios to prove the composer accepts input only for attested background work and the shared sidebar/task-switcher renders the violet dashed spinner. Provider coverage itself stays at captured ACP fixture level; CI does not need live credentials for every agent.
Implementation Waves
Wave 1:
Wave 2:
Wave 3:
Wave 4:
Wave 5:
Wave 6:
Wave 7:
Wave 8:
Wave 9:
Wave 10:
Wave 11:
Wave 12:
Wave 13:
Wave 14:
Wave 15:
Wave 16:
Wave 17: