Skip to content

feat: accept input while a session's foreground turn waits on background work#1668

Open
point-source wants to merge 78 commits into
kdlbs:mainfrom
point-source:feat/fine-grained-busy-signal
Open

feat: accept input while a session's foreground turn waits on background work#1668
point-source wants to merge 78 commits into
kdlbs:mainfrom
point-source:feat/fine-grained-busy-signal

Conversation

@point-source

@point-source point-source commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What & why

A session whose durable state reads RUNNING used to reject every new operator message as "agent is already running" — even when the foreground turn was actually idle, held open only by spawned background work (a subagent Task, a run_in_background shell, or an active Monitor watch). For long background jobs this locked the operator out of the conversation for the entire duration.

This PR narrows the busy signal to the foreground turn: when the foreground agent has yielded to registered background work, the session accepts "are you still working?" instead of dropping it. The change is conservative and capability-gated — any agent whose in-flight frames aren't recognized as background work (Codex, OpenCode, and anything else) keeps today's exact reject-while-RUNNING behavior.

Explicitly out of scope: mid-turn steering while the model is actively generating (a separate future concern — needs ACP concurrent-prompt support and per-agent capability gating). This PR corrects only whether input is accepted, not delivery into a live generating turn.

Full rationale, alternatives, and consequences are recorded in ADR-0035.

Behavior delivered

  • Backend gate narrows checkSessionPromptable to the foreground turn via an in-memory per-session activity tracker; default stays busy.
  • Normalizer fix recognizes Claude's run_in_background:true shell shape (a genuine bug fix regardless of the feature).
  • Operator-visible signal: the composer gates on foreground-generating rather than coarse RUNNING; a tri-state status icon distinguishes generating / working-in-background / done (the established running indicator is unchanged — this only adds a background indicator); and the substate is delivered live over a session.activity_changed WS event and surfaced on the boot payload / REST / WS session DTOs (RUNNING-only, not persisted) so a fresh page-load or second tab is immediately correct, with a backend restart resetting to the safe "generating" default.

Validated against upstream idle-turn completion

A falsifiable acceptance test drives a chatty Monitor (repeated event bursts that keep the idle-completion debounce alive) and asserts a prompt sent during a burst window is accepted and forwarded — proving the gate is still needed. A non-Claude regression test asserts Codex/OpenCode-shaped frames leave the gate unchanged.

End-to-end verification for the reviewer

  1. Drive a Claude session; run the mock agent's /background 20s (spawns a subagent and holds the turn open with the foreground idle).
  2. The session stays RUNNING with a working-in-background status (spinner, not the done check). Send "are you still working?" — it is accepted (composer shows send/accept, not "Queue more instructions…") and forwarded, not rejected.
  3. Reload the browser mid-background-window → the accept + working affordance is immediate, no wait for a WS event.
  4. When the background work finishes, the turn completes and the session settles to WAITING_FOR_INPUT (done check).
  5. Repeat with a non-Claude agent whose frames aren't recognized as background → input stays gated, unchanged.

CI: backend go test -race ./..., changed-file golangci-lint, web typecheck/lint/vitest, plus desktop + mobile Playwright specs (including a reload assertion).

Note

This is the upstream-targeted version of the change (the fork carries the same code under its own governance docs). It deliberately documents the decision as an ADR rather than the fork's SPEC/REQUIREMENTS files.

Review in cubic

Preview Environment

URL https://kandev-pr-1668-bwo7.sprites.app
Commit 8310c18
Agent Mock agent

Updates automatically on each push. Destroyed when the PR is closed.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds fine-grained foreground activity tracking for background work, updates prompt gating and session event delivery, standardizes Monitor and ACP background payload handling, enriches backend session DTOs, and updates web state, icons, and end-to-end coverage.

Changes

Foreground activity signaling

Layer / File(s) Summary
Background-work recognition
apps/backend/cmd/mock-agent/*, apps/backend/internal/agentctl/server/adapter/transport/acp/*, apps/backend/internal/agentctl/types/streams/*
Adds the /background mock command, recognizes ACP background execution, and introduces an adapter-attested Monitor payload contract.
Orchestrator activity state
apps/backend/internal/orchestrator/*, apps/backend/internal/agent/runtime/lifecycle/*
Tracks background tool calls, serializes prompt dispatch, transitions between generating and background-idle states, gates concurrent prompts, and publishes activity changes.
Backend session delivery
apps/backend/internal/events/*, apps/backend/internal/gateway/websocket/*, apps/backend/internal/task/*, apps/backend/pkg/*
Adds foreground activity types, events, WebSocket wiring, DTO enrichment, boot-state stamping, and message acceptance for background-idle sessions.
WebSocket and UI integration
apps/web/lib/ws/*, apps/web/lib/types/*, apps/web/hooks/*, apps/web/lib/ui/*, apps/web/components/*, apps/web/e2e/*
Consumes activity events, preserves enriched session state, derives busy flags, renders background indicators, and adds desktop/mobile tests.
Decision record
docs/decisions/*
Documents the foreground-idle busy-signal protocol and records ADR 0035 as accepted.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

  • kdlbs/kandev#413: Depends on PromptTask returning ErrAgentPromptInProgress for session admission behavior.
  • kdlbs/kandev#1244: Both changes interact with asynchronous subagent background-tool lifecycle data.
  • kdlbs/kandev#1600: Both changes modify ACP Monitor completion and activity handling.

Suggested reviewers: carlosflorencio, jcfs

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant ACP
  participant Orchestrator
  participant EventBus
  participant WebSocket
  participant WebUI
  Agent->>ACP: Emit background tool call or update
  ACP->>Orchestrator: Provide normalized background payload
  Orchestrator->>Orchestrator: Register background activity
  Orchestrator->>EventBus: Publish foreground activity
  EventBus->>WebSocket: Send session.activity_changed
  WebSocket->>WebUI: Update session foreground_activity
  WebUI->>WebUI: Show working status and accept input
Loading

Poem

I’m a rabbit with background hops,
While foreground typing never stops.
Signals bloom from stream to screen,
Spinner bright where work has been.
One prompt wins, the rest wait neat.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers the goal and validation, but it does not follow the required template and is missing the Checklist section. Rewrite the body using the repository template, include the required Validation and Checklist sections, and add RELATED ISSUES if applicable.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: accepting input while a running session waits on background work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56ada34f00

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/web/hooks/domains/session/use-session-state.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR narrows the session-level "busy" signal from coarse RUNNING to the finer-grained distinction between a foreground turn actively generating and one that has yielded to spawned background work (subagent task, run_in_background shell, or active Monitor watch). When the foreground is idle, the operator can send a new message even though the session state still reads RUNNING; non-Claude agents (Codex, OpenCode) keep the historical reject-while-RUNNING behavior.

  • Backend gate — new turnActivity in-memory tracker per session, with a claim/release lifecycle to close the check-then-act race window; the PromptWithDispatchCallback extension surfaces the dispatch boundary to the tracker; the run_in_background:true normalization bug is fixed as a side-effect.
  • Live signalsession.activity_changed WS event and foreground_activity field on the session/task DTOs (boot payload + REST) carry the substate to the client, with the task-level MOST-ACTIVE-WINS aggregate gating board card and list affordances; promptMu added to SessionManager.sendPrompt to prevent concurrent callers from racing on shared execution buffers.
  • FrontendderiveSessionFlags adds isAgentBusy = RUNNING && !background-idle; a tri-state icon set (solid dot / spinning arc / violet segmented spinner) distinguishes generating, background-running, and done by shape+motion+hue.

Confidence Score: 5/5

Safe to merge; both previous-thread issues are bounded to rare timing windows and the PR's own test suite exercises the core concurrency paths with a 32-goroutine race regression.

The two new observations are a misleading error message on a short-lived timing race (user retries and succeeds) and a forward-compatibility gap in the dispatch-callback fallback path. Neither touches correctness for any currently-deployed agent type: Claude sessions go through lifecycle.Manager which implements the callback, and passthrough agents are handled on a separate branch. The promptMu serialization change is intentional and consistent with the ACP adapter's existing gate.

apps/backend/internal/agent/runtime/lifecycle/session.go deserves a second look to confirm that no code path inside the agent-event handler (the sender to promptDoneCh) acquires promptMu, which would be the only route to a deadlock in the new serialization scheme.

Important Files Changed

Filename Overview
apps/backend/internal/orchestrator/turn_activity.go New file implementing the per-session foreground activity tracker. Well-structured with careful locking; the claim/release/complete lifecycle is sound, though the stale-claim edge case (turn ends between claimForegroundTurn and sendPrompt) surfaces a confusing error to the user.
apps/backend/internal/orchestrator/task_operations.go PromptTask refactored to support background-idle acceptance; foreground claim/release guard paths cover ensureSessionRunning and claimSessionRunningForPrompt failures, though the known leaked promptInFlight on PromptWithDispatchCallback failure is already tracked in a previous thread.
apps/backend/internal/agent/runtime/lifecycle/session.go SendPrompt refactored into sendPrompt/preparePrompt/triggerPrompt/finishAcceptedPrompt; new promptMu serializes concurrent callers per-execution. No deadlock risk since the agent-event handler that sends to promptDoneCh does not acquire promptMu. dispatchedPromptPending correctly handles the dispatch-only completion race.
apps/backend/internal/orchestrator/executor/executor_interaction.go PromptWithDispatchCallback added; passthrough path calls onDispatched on success; non-passthrough path type-asserts to promptAgentWithDispatchCallback, returning ErrPromptDispatchCallbackUnsupported for agents that don't implement it — propagates as a session error without special handling.
apps/backend/internal/orchestrator/event_handlers_streaming.go Background task registration/completion wired into tool_call and tool_update handlers; child tool calls correctly excluded; terminal status check consistent with isTerminalToolStatus helper.
apps/backend/internal/agentctl/server/adapter/transport/acp/normalize.go Bug fix: isBackgroundExecInput now recognizes both wait:false and run_in_background:true shapes; update path sets Background only ever to true.
apps/backend/internal/agentctl/types/streams/monitor_view.go MonitorPayload attestation cleanly separates adapter-attested identity from agent-shaped output; IsActiveMonitor reads only the adapter-set field, preventing impersonation by non-Claude tools.
apps/backend/internal/task/service/task_activity.go Task-level MOST-ACTIVE-WINS aggregate with dedup tracking; safe-fallback when session list fails to load; forgetTaskActivity called on task delete.
apps/backend/internal/backendapp/boot_state.go Boot state enrichment stamps foreground_activity onto session and task DTOs for fresh page-loads; no-op when orchestrator is not wired.
apps/web/hooks/domains/session/use-session-state.ts deriveSessionFlags takes the full session; isAgentBusy gates on foreground-generating only; isWorking stays true for all RUNNING states.
apps/web/lib/ws/handlers/agent-session.ts applyForegroundActivity guards against missing session, non-RUNNING state, and task-ID mismatch; foreground_activity reset wired into buildSessionUpdate for every coarse transition.
apps/web/lib/ws/handlers/tasks.ts mergeTaskUpdate preserves existing foregroundActivity when payload omits the field; correctly applies explicit null; hasPayloadField distinguishes absence from explicit null.
apps/web/lib/ui/state-icons.tsx Tri-state icon system with shape+motion+hue separation satisfying the not-color-alone accessibility requirement.
apps/backend/pkg/api/v1/task.go ForegroundActivity type and AggregateForegroundActivity MOST-ACTIVE-WINS reduction correctly short-circuits on generating with a sawBackground sentinel for background.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Op as Operator
    participant BE as promptTask (Orchestrator)
    participant TA as turnActivity (in-memory)
    participant Ag as lifecycle.Manager
    participant WS as WebSocket (client)

    Note over Op,WS: Background-idle session (turn open, foreground yielded)

    Ag->>TA: registerBackgroundTask(sessionID, toolCallID)
    TA-->>WS: publishForegroundActivityChanged - background
    WS-->>Op: "session.activity_changed foreground_activity=background"

    Op->>BE: PromptTask
    BE->>TA: "checkSessionPromptable - isForegroundTurnGenerating=false - pass"
    BE->>TA: "claimForegroundTurn - promptInFlight=true yielded=false"
    BE->>BE: ensureSessionRunning effectivePrompt
    BE->>TA: markForegroundGenerating - publish generating
    WS-->>Op: "session.activity_changed foreground_activity=generating"
    BE->>Ag: PromptWithDispatchCallback(onDispatched)
    Ag->>Ag: sendPrompt dispatch prompt to ACP
    Ag->>BE: onDispatched() - completeForegroundClaim
    Op-->>WS: prompt accepted

    Note over Ag,TA: Background tool_call completes
    Ag->>TA: completeBackgroundTask(sessionID, toolCallID)
    TA-->>WS: publishForegroundActivityChanged if last task

    Ag->>BE: waitForPromptDone - turn completes
    BE->>TA: clearTurnActivity(sessionID)
    BE-->>WS: "state_changed WAITING_FOR_INPUT foreground_activity=generating"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Op as Operator
    participant BE as promptTask (Orchestrator)
    participant TA as turnActivity (in-memory)
    participant Ag as lifecycle.Manager
    participant WS as WebSocket (client)

    Note over Op,WS: Background-idle session (turn open, foreground yielded)

    Ag->>TA: registerBackgroundTask(sessionID, toolCallID)
    TA-->>WS: publishForegroundActivityChanged - background
    WS-->>Op: "session.activity_changed foreground_activity=background"

    Op->>BE: PromptTask
    BE->>TA: "checkSessionPromptable - isForegroundTurnGenerating=false - pass"
    BE->>TA: "claimForegroundTurn - promptInFlight=true yielded=false"
    BE->>BE: ensureSessionRunning effectivePrompt
    BE->>TA: markForegroundGenerating - publish generating
    WS-->>Op: "session.activity_changed foreground_activity=generating"
    BE->>Ag: PromptWithDispatchCallback(onDispatched)
    Ag->>Ag: sendPrompt dispatch prompt to ACP
    Ag->>BE: onDispatched() - completeForegroundClaim
    Op-->>WS: prompt accepted

    Note over Ag,TA: Background tool_call completes
    Ag->>TA: completeBackgroundTask(sessionID, toolCallID)
    TA-->>WS: publishForegroundActivityChanged if last task

    Ag->>BE: waitForPromptDone - turn completes
    BE->>TA: clearTurnActivity(sessionID)
    BE-->>WS: "state_changed WAITING_FOR_INPUT foreground_activity=generating"
Loading

Reviews (12): Last reviewed commit: "Merge pull request #6 from point-source/..." | Re-trigger Greptile

Comment thread apps/backend/internal/orchestrator/task_operations.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/backend/internal/orchestrator/event_handlers_streaming.go (1)

393-483: 🎯 Functional Correctness | 🔵 Trivial

Background-tracking is unconditional in handleToolCallEvent but gated behind messageCreator here.

handleToolCallEvent's new registration (lines 259-268) intentionally sits outside the messageCreator != nil block. trackBackgroundToolUpdate here is called at line 442, nested inside both the if s.messageCreator == nil { return } guard (404-406) and the status switch. If messageCreator is ever nil when tool-update events are processed, a background task registered by handleToolCallEvent can never be completed via this path, parking the session in the "background" substate for the rest of the turn. Moving the call ahead of the guard mirrors the call-side symmetry and removes the dependency.

♻️ Proposed fix to decouple background tracking from messageCreator
 	if s.shouldDropCompletedExecutionStreamEvent(payload) {
 		return
 	}

+	// Background-work bookkeeping must run even when messageCreator is unset,
+	// mirroring handleToolCallEvent's unconditional registration.
+	s.trackBackgroundToolUpdate(ctx, payload)
+
 	if s.messageCreator == nil {
 		return
 	}
 			s.setSessionRunningForExecution(ctx, payload.TaskID, payload.SessionID, payload.ExecutionID)
 		}
-
-		// Background-work bookkeeping for the finer-grained busy signal.
-		s.trackBackgroundToolUpdate(ctx, payload)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/internal/orchestrator/event_handlers_streaming.go` around lines
393 - 483, Move the trackBackgroundToolUpdate call in handleToolUpdateEvent
before the messageCreator nil guard and outside the status/message-update
handling, so background completion tracking always runs even when messageCreator
is unavailable. Preserve the existing payload validation and avoid changing the
tool message update behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/backend/internal/orchestrator/event_handlers_streaming.go`:
- Around line 393-483: Move the trackBackgroundToolUpdate call in
handleToolUpdateEvent before the messageCreator nil guard and outside the
status/message-update handling, so background completion tracking always runs
even when messageCreator is unavailable. Preserve the existing payload
validation and avoid changing the tool message update behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d3aaebd2-f212-4f29-9a7d-0eb2c6ce30c6

📥 Commits

Reviewing files that changed from the base of the PR and between 9094659 and 56ada34.

📒 Files selected for processing (42)
  • apps/backend/cmd/mock-agent/background_test.go
  • apps/backend/cmd/mock-agent/handler.go
  • apps/backend/cmd/mock-agent/main.go
  • apps/backend/internal/agentctl/server/adapter/transport/acp/monitor.go
  • apps/backend/internal/agentctl/server/adapter/transport/acp/normalize.go
  • apps/backend/internal/agentctl/server/adapter/transport/acp/normalize_background_test.go
  • apps/backend/internal/agentctl/types/streams/monitor_view.go
  • apps/backend/internal/agentctl/types/streams/monitor_view_test.go
  • apps/backend/internal/backendapp/boot_state.go
  • apps/backend/internal/events/types.go
  • apps/backend/internal/gateway/websocket/task_notifications.go
  • apps/backend/internal/gateway/websocket/task_notifications_test.go
  • apps/backend/internal/orchestrator/event_handlers_streaming.go
  • apps/backend/internal/orchestrator/foreground_activity_signal_test.go
  • apps/backend/internal/orchestrator/foreground_busy_signal_test.go
  • apps/backend/internal/orchestrator/prompt_background_acceptance_test.go
  • apps/backend/internal/orchestrator/service.go
  • apps/backend/internal/orchestrator/task_operations.go
  • apps/backend/internal/orchestrator/turn_activity.go
  • apps/backend/internal/task/dto/dto.go
  • apps/backend/internal/task/dto/foreground_activity_test.go
  • apps/backend/internal/task/handlers/foreground_activity_test.go
  • apps/backend/internal/task/handlers/task_handlers.go
  • apps/backend/internal/task/handlers/task_http_handlers.go
  • apps/backend/internal/task/handlers/task_ws_handlers.go
  • apps/backend/pkg/api/v1/task.go
  • apps/backend/pkg/websocket/actions.go
  • apps/web/components/task/session-reopen-menu.tsx
  • apps/web/components/task/sessions-dropdown.tsx
  • apps/web/e2e/tests/chat/busy-signal.spec.ts
  • apps/web/e2e/tests/chat/mobile-busy-signal.spec.ts
  • apps/web/hooks/domains/session/use-session-state.test.ts
  • apps/web/hooks/domains/session/use-session-state.ts
  • apps/web/lib/state/slices/session/session-slice.upsert.test.ts
  • apps/web/lib/types/backend.ts
  • apps/web/lib/types/http.ts
  • apps/web/lib/ui/state-icons.test.tsx
  • apps/web/lib/ui/state-icons.tsx
  • apps/web/lib/ws/handlers/agent-session.test.ts
  • apps/web/lib/ws/handlers/agent-session.ts
  • docs/decisions/0035-fine-grained-foreground-idle-busy-signal.md
  • docs/decisions/INDEX.md

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 42 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread apps/backend/internal/orchestrator/event_handlers_streaming.go Outdated
Comment thread apps/backend/internal/orchestrator/task_operations.go
Comment thread apps/backend/internal/agentctl/types/streams/monitor_view.go Outdated
Comment thread apps/web/lib/ws/handlers/agent-session.ts
Comment thread apps/backend/internal/orchestrator/event_handlers_streaming.go Outdated
Comment thread apps/backend/internal/orchestrator/event_handlers_streaming.go
Comment thread apps/backend/internal/orchestrator/event_handlers_streaming.go
Comment thread apps/web/components/task/session-reopen-menu.tsx Outdated
Comment thread apps/backend/internal/agentctl/types/streams/monitor_view.go
Comment thread apps/backend/internal/orchestrator/task_operations.go Outdated
@point-source

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough reviews. Addressed in be730db2. Summary of each finding:

Fixed

  • Codex — message.add rejects background-idle RUNNING (P1). Confirmed real: the composer's non-busy send goes through message.adderrorForBlockedMessageSession, which rejected every RUNNING session before reaching PromptTask's narrowed gate. errorForBlockedMessageSession now consults ForegroundActivity and lets a background-idle session through to PromptTask (which owns the same gate). Added TestErrorForBlockedMessageSession_BackgroundIdleAccepts.
  • Greptile / cubic — no activity_changed publish in PromptTask (P1/P2). markForegroundGenerating's return is now checked and the flip published, so the client's stale foreground_activity=background is reset when a prompt is accepted into an already-RUNNING session.
  • cubic — markForegroundGenerating before payload validation (P1, streaming.go:375). Now gated on payload.Data.Text != "", so empty/discarded frames can't spuriously reclose the prompt gate.
  • cubic — background tracking unreachable when messageCreator == nil (P1, streaming.go:453). Moved trackBackgroundToolUpdate ahead of the nil-guard (mirrors handleToolCallEvent).
  • cubic — terminal tool_call registered but never cleared (P1, streaming.go:264). Added an isTerminalToolStatus guard on registration.
  • cubic — applyForegroundActivity lacks stale/mismatch guards (P1, agent-session.ts:475). Now ignores the flip unless the row is still RUNNING and task_id matches. (Deferred the timestamp/sequence guard — it needs a new payload field; the RUNNING guard already blocks the reopen-during-generating case.)
  • cubic — dead foreground_activity arg in reopen menu (P2). Dropped; that branch only renders non-RUNNING sessions.

Acknowledged — follow-ups, not regressions

  • cubic — tracker can stick in generating after a mid-background foreground chunk (P0, streaming.go:441). The spurious-mark fix removes the main trigger (empty frames). The residual case — genuine foreground output mid-background, then silence — is the documented best-effort limitation in ADR-0035's Tradeoffs: a reliable re-yield needs a foreground-idle signal ACP doesn't emit (the upstream idle-turn debounce is the closest). It degrades safely to the conservative "busy", so it's a coverage gap, not a wrong-accept.
  • cubic — check-then-act race on concurrent prompts (P1, task_operations.go:2373). Real but low-probability (two concurrent sends to the same session inside the background-idle window). A proper fix is an atomic check-and-claim on the foreground turn — an architectural change tracked as a follow-up.
  • cubic — IsActiveMonitor provenance (P1) / exported key constants (P2). The {monitor:{kind:"Monitor",ended:false}} wrapper is stamped only by the ACP adapter for Claude Monitor, so misclassification needs a non-Claude agent to emit our exact internal shape — low risk. Hardening (a provenance discriminator + shared exported keys / an ACP↔streams integration test) is a reasonable follow-up.
  • CodeRabbit — description template / docstring coverage. Noted; not code defects.

Comment thread apps/web/components/task/session-reopen-menu.tsx Outdated
Comment thread apps/backend/internal/task/handlers/message_handlers.go
@github-actions

Copy link
Copy Markdown
Contributor

Findings

Suggestion (recommended, doesn't block)

  1. AI slop comment in session-reopen-menu.tsxapps/web/components/task/session-reopen-menu.tsx:128-129

    • Issue: the inline JSX comment restates the obvious — the surrounding session.state !== "RUNNING" condition at line 124 already shows exactly why foreground_activity is irrelevant in that branch.
    • Fix: delete the comment. See inline suggestion.
  2. Undocumented interface asymmetry for ForegroundActivityapps/backend/internal/task/handlers/message_handlers.go:33-36

    • Issue: OrchestratorService (used by MessageHandlers) requires ForegroundActivity as a hard interface method, while TaskHandlers acquires the same capability via an optional type-assertion on OrchestratorStarter. Both approaches are coherent, but someone adding a new MessageHandlers mock must implement the method or it won't compile, whereas a new TaskHandlers orchestrator silently gets no enrichment and there's nothing in the code to flag the missing capability. A short comment on the interface (or NewTaskHandlers) noting that the asymmetry is deliberate — gate vs. best-effort enrichment — would prevent future authors from "fixing" it the wrong way.

Summary

Severity Count
Blocker 0
Suggestion 2

Verdict: Ready to merge with suggestions.

The core state machine (turn_activity.go) is well-designed: the absent/zero default is "foreground generating" so nothing changes for unrecognized agents; registerBackgroundTask is idempotent across concurrent calls via LoadOrStore; and completeBackgroundTask correctly clears by tool-call ID membership rather than re-classifying the terminal payload (the TestForegroundBusySignal_TerminalToolUpdateReclosesGateByIDNotKind test pins this invariant explicitly). The ACP normalizer fix (isBackgroundExecInput extracted and wired into the merge path) is a genuine bug fix on its own. Test coverage — unit, integration, E2E desktop + mobile — is comprehensive. The boot-payload enrichment and WS reset on every coarse state transition close the page-reload gap cleanly.

@carlosflorencio

Copy link
Copy Markdown
Member

Thanks for the contribution. The direction makes sense 💪, but I think the prompt-admission path needs a little more work before merging:

  • A background-shaped tool call does not necessarily mean the current prompt RPC has finished. ACP serializes calls through promptGate, while lifecycle waits for completion through the execution’s shared promptDoneCh. I suggest tracking two independent facts per session: backgroundWorkOutstanding and promptRPCInFlight. Set the latter when dispatching a prompt and clear it only when the corresponding complete event arrives. Direct sending should require background work to be outstanding and no prompt RPC to be in flight; otherwise the message should use the existing queue. This avoids concurrent SendPrompt waiters and shared-buffer resets.

  • PromptTask has a check-and-claim race. Two submissions can both observe yielded=true before either reaches markForegroundGenerating. A small atomic method on turnActivity, such as tryClaimForegroundPrompt(), could lock the activity record, verify that it is background-idle and unclaimed, then mark it generating/claimed in the same critical section. The WS message handler should claim synchronously before persisting the direct-send message, pass that reservation into PromptTask, and release it if persistence or dispatch fails. A two-goroutine regression test should assert that exactly one submission is admitted.

  • After background work opens the gate, a subsequent top-level foreground tool call does not close it again. In handleToolCallEvent / trackBackgroundToolUpdate, a non-terminal top-level tool event could follow this rule: register it when it is recognized background work; otherwise call markForegroundGenerating. Child events should remain ignored, and updates for an already registered background tool ID should not re-yield the foreground. An initially incomplete tool payload can conservatively mark generating until a later update proves it is background work.

  • Since this behavior is Claude-specific today, I suggest an explicit server-owned capability such as AcceptsPromptWhileBackground. Enable it only for the configured Claude agent type, copy it onto the execution/session activity record, and require it before registering any background-idle state. Unknown agents should default to false. This can start as a static agent registry capability and later move into the existing agent_capabilities contract if another provider supports it; it should not be inferred solely from tool output.

The current E2E test proves that the message appears and avoids the visible queue, but it would be useful to extend the mock so the second prompt produces a unique response. The test could then assert that the response arrives after the correct prompt, that each completion wakes the correct request, and that the user message and response belong to the same turn.

point-source added a commit to point-source/kandev that referenced this pull request Jul 13, 2026
Addresses the second round of automated review on kdlbs#1668.

- PromptTask now atomically claims the foreground turn instead of acting on a
  bare read. checkSessionPromptable only reads the substate, and the window
  between that read and the point the turn is marked generating spans a session
  reload, ensureSessionRunning, and a possibly network-bound model switch — so
  two prompts landing in the background-idle window together (a double-send, two
  tabs) both passed the gate and both reached executor.Prompt, starting
  overlapping turns on one ACP session. Exactly one prompt now wins; the losers
  are rejected with ErrAgentPromptInProgress as before. A claim whose prompt then
  fails before reaching the agent is released, so a failed send can't leave the
  session advertising a foreground it doesn't have (cubic P1).

- IsActiveMonitor gains a provenance check. A Generic payload's Output is
  otherwise the agent's own raw tool result, so the predicate now demands the
  full view the adapter writes, including the task_id only a real Monitor
  registration produces. Note GenericPayload.Name is NOT usable as the
  discriminator as suggested — it carries the ACP tool kind, which is "other" for
  Monitor, so asserting Name == "Monitor" would have disabled Monitor
  recognition outright (cubic P1).

- The Monitor view map keys are exported from streams and consumed by
  acp/monitor.go, replacing the duplicated string literals on the producer side,
  and a contract test drives real monitorOutputWrapper output through
  IsActiveMonitor (incl. across the agentctl→orchestrator JSON boundary) so the
  two sides can't drift apart silently (cubic P2).

- Document the deliberate hard-vs-soft ForegroundActivity dependency asymmetry
  between MessageHandlers (admission gate — must not silently degrade) and
  TaskHandlers (DTO enrichment — safe to omit), and drop a comment that restated
  its own state guard (github-actions).

Tests: concurrent-PromptTask regression proving exactly one turn opens (red
without the claim: all 8 prompts were accepted), claim/release unit coverage, and
the producer→consumer Monitor contract tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/internal/orchestrator/task_operations.go`:
- Around line 2343-2348: Update the early-return branch handling trySwitchModel
in the surrounding task operation to publish the foreground activity change when
the model switch succeeds, before returning. Preserve the existing
releaseForegroundClaimOnFailure behavior for errors and avoid publishing the
success update when trySwitchModel does not switch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c9571ba-464a-4b35-8400-0ff22334d384

📥 Commits

Reviewing files that changed from the base of the PR and between be730db and 73ec934.

📒 Files selected for processing (9)
  • apps/backend/internal/agentctl/server/adapter/transport/acp/monitor.go
  • apps/backend/internal/agentctl/server/adapter/transport/acp/monitor_contract_test.go
  • apps/backend/internal/agentctl/types/streams/monitor_view.go
  • apps/backend/internal/agentctl/types/streams/monitor_view_test.go
  • apps/backend/internal/orchestrator/foreground_claim_test.go
  • apps/backend/internal/orchestrator/task_operations.go
  • apps/backend/internal/orchestrator/turn_activity.go
  • apps/backend/internal/task/handlers/message_handlers.go
  • docs/decisions/0035-fine-grained-foreground-idle-busy-signal.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/backend/internal/agentctl/types/streams/monitor_view.go
  • apps/backend/internal/task/handlers/message_handlers.go
  • apps/backend/internal/agentctl/types/streams/monitor_view_test.go

Comment thread apps/backend/internal/orchestrator/task_operations.go Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread apps/backend/internal/orchestrator/turn_activity.go Outdated
Comment thread apps/backend/internal/agentctl/types/streams/monitor_view.go Outdated
Comment thread apps/backend/internal/orchestrator/task_operations.go Outdated
Comment thread apps/backend/internal/orchestrator/task_operations.go Outdated
Comment thread apps/backend/internal/orchestrator/task_operations.go Outdated
Comment thread apps/backend/internal/orchestrator/task_operations.go
Comment thread apps/backend/internal/orchestrator/foreground_busy_signal_test.go
@github-actions

Copy link
Copy Markdown
Contributor

Findings

Suggestion (recommended, doesn't block)

  1. releaseForegroundClaimOnFailure doesn't publish the activity flip back to "background"apps/backend/internal/orchestrator/task_operations.go:2295

    • Issue: releaseForegroundClaim re-yields the backend gate (yielded = true) but emits no session.activity_changed event. The client keeps foreground_activity = "generating" (stamped when the claim was taken) and shows "Queue more instructions…" even though the backend would now accept a direct message.add.
    • Why: The WS message gate (errorForBlockedMessageSession) checks the live in-memory value, so a retry is accepted by the backend regardless — but the UI affords queuing rather than sending. The queued message eventually drains when the turn completes, so correctness is preserved; only the UX is degraded in the narrow ensureSessionRunning/trySwitchModel failure path.
    • Fix: Return a bool from releaseForegroundClaim (true when it actually re-yielded), and call publishForegroundActivityChanged in the closure when it does. See inline comment for a code sketch.
  2. monitorGenericPayload test helper hardcodes Monitor view map keysapps/backend/internal/orchestrator/foreground_busy_signal_test.go:362

    • Issue: Uses literal "monitor", "kind", "ended", "task_id", "command" instead of streams.MonitorViewKey, streams.MonitorViewKindKey, etc. It correctly uses streams.MonitorSubkind for the value, but spells the keys out directly.
    • Why: streams.MonitorView*Key constants exist so a rename on either the producer or consumer side causes a compile error, not a silent drift. With hardcoded keys in this helper, if any constant were renamed IsActiveMonitor would stop recognizing Monitors in these tests without a compile error or test failure — the "monitor should be background" assertions would silently flip to incorrect results.
    • Fix: Replace the five string literals with the corresponding streams.MonitorView*Key constants. See inline comment for a one-line-per-key suggestion.

Summary

Severity Count
Blocker 0
Suggestion 2

Verdict: Ready with suggestions — both are narrow, won't block the feature. The release-claim publish gap is the more impactful of the two (UX regression in a rare error path); the constant-usage gap is a test hygiene fix. Neither affects the correctness of the happy path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/backend/internal/agent/runtime/lifecycle/session_test.go (1)

894-915: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Register test resources with t.Cleanup.

Replace the new defer mock.Close() and defer client.Close() calls with cleanup callbacks registered immediately after creation.

As per coding guidelines, lifecycle tests must register t.Cleanup immediately after resource creation.

Also applies to: 968-973, 1065-1087

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/internal/agent/runtime/lifecycle/session_test.go` around lines
894 - 915, Replace the deferred mock and client shutdowns with t.Cleanup
callbacks registered immediately after each resource is created. Apply this
consistently in the shown setup and the corresponding resource-creation blocks
around the additional locations, preserving the existing Close behavior and
cleanup order.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/internal/agent/runtime/lifecycle/session.go`:
- Around line 713-721: Update the reconnect retry handling in the session
lifecycle method around retryPromptAfterReconnect so that a failed retry
propagates its retryErr into the subsequent error-mapping flow instead of
leaving the original err unchanged. Preserve the existing success return and
warning log, while ensuring retry-side ErrCancelEscalated errors are mapped
correctly for callers.

In `@apps/backend/internal/orchestrator/executor/executor_interaction.go`:
- Around line 188-196: Update the dispatch path around
promptAgentWithDispatchCallback so dispatchOnly requests require an agentManager
implementing PromptAgentWithDispatchCallback; when that capability is absent,
fail explicitly instead of calling PromptAgent and invoking onDispatched after
completion. Preserve the existing callback-capable flow and only use the
PromptAgent fallback where the dispatch-callback contract is not required.

---

Nitpick comments:
In `@apps/backend/internal/agent/runtime/lifecycle/session_test.go`:
- Around line 894-915: Replace the deferred mock and client shutdowns with
t.Cleanup callbacks registered immediately after each resource is created. Apply
this consistently in the shown setup and the corresponding resource-creation
blocks around the additional locations, preserving the existing Close behavior
and cleanup order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a68e0b0-ee9e-4f76-b7ee-b4d3b39a03c0

📥 Commits

Reviewing files that changed from the base of the PR and between caaf18d and ddeac87.

📒 Files selected for processing (13)
  • apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go
  • apps/backend/internal/agent/runtime/lifecycle/session.go
  • apps/backend/internal/agent/runtime/lifecycle/session_test.go
  • apps/backend/internal/agent/runtime/lifecycle/types.go
  • apps/backend/internal/backendapp/adapters.go
  • apps/backend/internal/orchestrator/event_handlers_streaming.go
  • apps/backend/internal/orchestrator/executor/executor_interaction.go
  • apps/backend/internal/orchestrator/foreground_activity_signal_test.go
  • apps/backend/internal/orchestrator/foreground_busy_signal_test.go
  • apps/backend/internal/orchestrator/foreground_claim_test.go
  • apps/backend/internal/orchestrator/task_operations.go
  • apps/backend/internal/orchestrator/turn_activity.go
  • docs/decisions/0035-fine-grained-foreground-idle-busy-signal.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • docs/decisions/0035-fine-grained-foreground-idle-busy-signal.md
  • apps/backend/internal/orchestrator/foreground_claim_test.go
  • apps/backend/internal/orchestrator/turn_activity.go
  • apps/backend/internal/orchestrator/event_handlers_streaming.go
  • apps/backend/internal/orchestrator/task_operations.go
  • apps/backend/internal/orchestrator/foreground_busy_signal_test.go

Comment thread apps/backend/internal/agent/runtime/lifecycle/session.go Outdated
Comment thread apps/backend/internal/orchestrator/executor/executor_interaction.go

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread apps/backend/internal/orchestrator/executor/executor_interaction.go Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread apps/backend/internal/orchestrator/event_handlers_test.go Outdated
carlosflorencio pushed a commit to point-source/kandev that referenced this pull request Jul 14, 2026
Addresses the second round of automated review on kdlbs#1668.

- PromptTask now atomically claims the foreground turn instead of acting on a
  bare read. checkSessionPromptable only reads the substate, and the window
  between that read and the point the turn is marked generating spans a session
  reload, ensureSessionRunning, and a possibly network-bound model switch — so
  two prompts landing in the background-idle window together (a double-send, two
  tabs) both passed the gate and both reached executor.Prompt, starting
  overlapping turns on one ACP session. Exactly one prompt now wins; the losers
  are rejected with ErrAgentPromptInProgress as before. A claim whose prompt then
  fails before reaching the agent is released, so a failed send can't leave the
  session advertising a foreground it doesn't have (cubic P1).

- IsActiveMonitor gains a provenance check. A Generic payload's Output is
  otherwise the agent's own raw tool result, so the predicate now demands the
  full view the adapter writes, including the task_id only a real Monitor
  registration produces. Note GenericPayload.Name is NOT usable as the
  discriminator as suggested — it carries the ACP tool kind, which is "other" for
  Monitor, so asserting Name == "Monitor" would have disabled Monitor
  recognition outright (cubic P1).

- The Monitor view map keys are exported from streams and consumed by
  acp/monitor.go, replacing the duplicated string literals on the producer side,
  and a contract test drives real monitorOutputWrapper output through
  IsActiveMonitor (incl. across the agentctl→orchestrator JSON boundary) so the
  two sides can't drift apart silently (cubic P2).

- Document the deliberate hard-vs-soft ForegroundActivity dependency asymmetry
  between MessageHandlers (admission gate — must not silently degrade) and
  TaskHandlers (DTO enrichment — safe to omit), and drop a comment that restated
  its own state guard (github-actions).

Tests: concurrent-PromptTask regression proving exactly one turn opens (red
without the claim: all 8 prompts were accepted), claim/release unit coverage, and
the producer→consumer Monitor contract tests.
@carlosflorencio
carlosflorencio force-pushed the feat/fine-grained-busy-signal branch from 1a923b8 to 3869db9 Compare July 14, 2026 12:42
Copilot AI review requested due to automatic review settings July 25, 2026 05:21
@point-source
point-source force-pushed the feat/fine-grained-busy-signal branch from beb3443 to c935b1c Compare July 25, 2026 05:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 170 out of 170 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

apps/web/lib/types/backend.ts:96

  • There’s a stray semicolon in the comment (//;) which reads like a typo and can confuse readers. Removing it keeps the doc comment clean.

Comment on lines 192 to 195
ActionSessionStateChanged = "session.state_changed"
ActionSessionActivityChanged = "session.activity_changed"
ActionSessionWaitingForInput = "session.waiting_for_input"
ActionSessionAgentctlStarting = "session.agentctl_starting"
Copilot AI review requested due to automatic review settings July 25, 2026 08:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 170 out of 170 changed files in this pull request and generated 1 comment.

Comment on lines +46 to +51
// ForegroundActivity is the fine-grained busy substate of a RUNNING session
// (ADR-0049). It distinguishes a foreground turn that is
// actively generating from one that is idle, held open only by spawned
// background work (a subagent task, a run-in-background shell, an active
// Monitor). It is only meaningful while the session state is RUNNING; for every
// other state the coarse state already tells the whole story.
Copilot AI review requested due to automatic review settings July 25, 2026 09:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 183 out of 183 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

apps/backend/pkg/api/v1/task.go:51

  • The doc comment says ForegroundActivity is only meaningful while session state is RUNNING, but this PR explicitly carries "background" beyond the foreground turn (e.g. WAITING_FOR_INPUT) and both backend/frontend code rely on that. The comment is misleading for maintainers reading the API types.
    apps/web/lib/types/backend.ts:95
  • There’s a stray semicolon in the comment (//; absent/null...), which reads like a typo in the generated backend payload types.
    apps/web/components/task/chat/messages/agent-status.tsx:45
  • resolveAgentStatusConfig only shows the "Background work is running" label for WAITING_FOR_INPUT+isWorking. But this PR also makes RUNNING+foreground_activity="background" directly promptable, and in that state the UI still resolves to the coarse RUNNING label ("Agent is running"), which contradicts the intended tri-state indicator described in the PR (generating vs background vs done).

@carlosflorencio

Copy link
Copy Markdown
Member

I found two cases that should be fixed:

  1. Multiple ID-less background completions never retire any work. completeBackgroundWorkSnapshot returns early whenever more than one registration exists, so two workloads followed by two Claude task-notification completions leave both registered indefinitely. This also contradicts the spec saying each uncorrelated completion retires one registration.

Ideally, carry a stable workload ID through the adapter:

AgentEvent{
    Type:       streams.EventTypeBackgroundComplete,
    ToolCallID: taskID,
}

If the provider cannot expose that identity, the accounting contract needs another deduplication/correlation strategy rather than leaving all registrations live.

  1. The per-task publication queue continues after task.deleted. I reproduced task.updated → task.deleted → stale task.updated; all three events were delivered, and the frontend upserts the final event, making the deleted task reappear. The queue needs a terminal/tombstone state that clears pending work and rejects later non-create events, roughly:
if eventType == events.TaskDeleted {
    queue.deleted = true
    queue.pending = nil
}

Please add deterministic regression tests for both sequences.

Two smaller follow-ups: the chat footer still says “Agent is running” for RUNNING + background, and a few API comments still describe foreground_activity as RUNNING-only even though background activity can survive into settled states.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants