feat: show a waiting state when background tasks outlive the turn#4255
feat: show a waiting state when background tasks outlive the turn#4255t3dotgg wants to merge 3 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| // replayed one mid-turn would null the active turn and settle a | ||
| // genuinely running turn. The turn lifecycle already clears | ||
| // pendingBackgroundTasks when a turn starts. | ||
| return event.payload.backgroundTasks === undefined || activeTurnId === null; |
There was a problem hiding this comment.
🟡 Medium Layers/ProviderRuntimeIngestion.ts:1348
A stale or replayed non-empty backgroundTasks roster event arriving after a newer empty roster restores pendingBackgroundTasks, falsely showing the thread as waiting on background work. The session.state.changed guard accepts any roster event when activeTurnId is null, so a late or duplicate non-empty roster re-populates pendingBackgroundTasks after it was already cleared, and the thread remains stuck in that state until another lifecycle event arrives. Consider tracking a monotonic sequence or watermark on roster events so out-of-order replays are rejected.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 1348:
A stale or replayed non-empty `backgroundTasks` roster event arriving after a newer empty roster restores `pendingBackgroundTasks`, falsely showing the thread as waiting on background work. The `session.state.changed` guard accepts any roster event when `activeTurnId` is `null`, so a late or duplicate non-empty roster re-populates `pendingBackgroundTasks` after it was already cleared, and the thread remains stuck in that state until another lifecycle event arrives. Consider tracking a monotonic sequence or watermark on roster events so out-of-order replays are rejected.
| runtimeMode: thread.session?.runtimeMode ?? "full-access", | ||
| activeTurnId: nextActiveTurnId, | ||
| lastError, | ||
| ...(pendingBackgroundTasks !== undefined ? { pendingBackgroundTasks } : {}), |
There was a problem hiding this comment.
Waiting roster cleared by idle events
High Severity
Non-roster lifecycle events, such as session.state.changed from Claude, inadvertently clear pendingBackgroundTasks from the session state. This causes the UI to incorrectly show the session as idle or finished while background work is still active.
Reviewed by Cursor Bugbot for commit 16c1506. Configure here.
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. New feature introducing background task waiting states with complex state management. Multiple unresolved review comments identify HIGH severity bugs in roster event handling and error state management that need to be addressed. You can customize Macroscope's approvability policy. Learn more. |
When Claude ends its turn with background work still running (background shells like codex reviews, subagents), the thread showed 'Worked for Ns' and looked done even though the SDK wakes the session when the task finishes. Track the SDK's background-task roster in the Claude adapter, surface it through session.state.changed → session.pendingBackgroundTasks (additive field, no new status literal, so stale clients degrade to today's behavior), and render a pulsing 'Waiting on background task: …' row in the web timeline while the thread waits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From gpt-5.6-sol review: (1) ignore roster-bearing session.state.changed events while a turn is active so a delayed/replayed roster can't settle a running turn; (2) drop the adapter's roster when a turn fails or is interrupted so stale entries can't resurface as a bogus waiting state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… lifecycle Rebased onto main's sidebar v2 (#4026): migration renumbered 033→034 (settled lifecycle took 033), and the pending-turn-start status resolution in ingestion merged with the roster branch. A thread waiting on background tasks is in motion, not done: - resolveSidebarV2Status shows it as 'Working' instead of the receded ready state - canSettle/effectiveSettled block settling (and override a stale settled pin), matching the running-session blockers - the thread.settle decider rejects it server-side, mirroring the active-session invariant Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
16c1506 to
88adca5
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 88adca5. Configure here.
| yield* emitBackgroundWaitState(context, { | ||
| rawMethod: "claude/system/task_notification", | ||
| rawPayload: message, | ||
| }); |
There was a problem hiding this comment.
Drain after failure clears error
High Severity
After a failed turn clears the background-task map, a later task_notification still calls emitBackgroundWaitState, which emits session.state.changed with state: "ready" and an empty roster. Ingestion then overwrites the session error status from turn.completed and clears lastError.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 88adca5. Configure here.


Problem
When Claude ends its turn while a background process is still running (a backgrounded `codex` review, a subagent), the thread folds to "Worked for Ns" and looks done — even though the Claude SDK will wake the session with a follow-up turn when the task finishes. Repro from tonight's session logs (thread `7210fae4`): Claude launched `codex review` with `run_in_background: true`, said "I'll report back when it completes", and the UI sat idle-looking for ~3 minutes until the task notification triggered a synthetic turn.
The SDK already tells us everything we need: the undeclared `background_tasks_changed` system message is a roster snapshot of live background tasks — the adapter was deliberately swallowing it (it used to only cause spurious warning rows).
Fix
Notes
Testing
🤖 Generated with Claude Code
Note
Medium Risk
Changes thread session lifecycle, settling invariants, and persistence schema, but uses additive optional fields and includes race/stale-roster guards with regression tests.
Overview
Threads no longer look idle when a provider background task (e.g. a Claude background shell) keeps running after the assistant turn ends. The wire session status stays
ready; clients learn about the wait via optionalpendingBackgroundTasksonOrchestrationSession.Claude adapter tracks the SDK
background_tasks_changedroster (withtask_started/task_notificationfallbacks) and emits between-turnsession.state.changedevents carrying abackgroundTasksroster. Failed or interrupted turns clear the roster so stale waits cannot reappear.Ingestion maps roster events to
pendingBackgroundTasks, keeps statusready, ignores roster updates while a turn is active, and clears the list on turn start and other lifecycle transitions. Projection + DB migration 034 persistpending_background_tasks_jsonand expose it in snapshot queries.Settling is blocked server- and client-side while
pendingBackgroundTasksis non-empty. The web UI treats that state as working in the sidebar and shows a waiting-on-background-task timeline row when the turn is settled but tasks remain.Reviewed by Cursor Bugbot for commit 88adca5. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Show a waiting state in chat UI when background tasks outlive the current turn
pending_background_tasks_jsoncolumn toprojection_thread_sessionsvia migration 034, and persists/retrieves the background task roster through the repository and snapshot query layers.ClaudeAdaptertracks background tasks per session and emitssession.state.changedevents with a waiting/ready state and task roster between turns; tasks are cleared on turn failure.ProviderRuntimeIngestionapplies background task roster events only between turns, sets wire status toready, and propagatespendingBackgroundTaskson the session.waiting-backgroundrow variant that renders a pulsing indicator when tasks are pending and no turn is active; the sidebar reportsworkingfor such sessions.canSettleandeffectiveSettledblock settling whilependingBackgroundTasksis non-empty; the server-side decider also rejects settle commands in this state.Macroscope summarized 88adca5.