Skip to content

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

Open
point-source wants to merge 9 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 9 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 73ec934
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
Description check ⚠️ Warning The body covers the problem, behavior, and validation, but it omits the required Checklist section and doesn't follow the template headings. Add the required ## Checklist section unchanged, and keep the summary and validation in the template's required sections.
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.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: allowing input while a session's foreground turn waits on background work.
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.
✨ 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
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements fine-grained foreground-idle detection for RUNNING sessions, allowing operators to send messages while the foreground agent is waiting on spawned background work (subagent Task, run_in_background shell, active Monitor). It also fixes a genuine bug in the ACP normalizer where Claude's run_in_background:true shell shape wasn't recognized.

  • Backend: A new in-memory turnActivity tracker per session, with atomic claim/release mechanics (claimForegroundTurn / completeForegroundClaim / releaseForegroundClaim) to prevent double-admission across the check-then-act window; the checkSessionPromptable gate narrows from "any RUNNING session" to "foreground-generating RUNNING session", and a new session.activity_changed WS event plus foreground_activity field on DTO/boot payloads propagates the substate to clients.
  • Frontend: A tri-state status icon (spinner vs. filled dot vs. check) distinguishes generating / background-idle / done; the composer gate (isAgentBusy) narrows to foreground-generating; the new WS event handler guards against stale out-of-order deliveries by requiring state === "RUNNING" before applying the flip.
  • Normalizer fix: isBackgroundExecInput now recognizes both wait:false (other agents) and run_in_background:true (Claude Bash tool) as background shell shapes; the fix is applied on merge so a later update frame carrying the flag is honoured even if the initial tool_call frame lacked it.

Confidence Score: 4/5

The change is conservative and defaults to the existing reject-while-RUNNING behavior for unrecognized agents; the new acceptance window is tightly gated by atomic claim/release mechanics that prevent double-admission.

The turn-activity tracker, foreground claim lifecycle, and event-driven state transitions are internally consistent and well-tested. The two unresolved dispatch-failure paths noted in prior threads mean a failed prompt during a background-idle turn leaves the session appearing foreground-generating until the turn closes.

apps/backend/internal/orchestrator/task_operations.go — the PromptWithDispatchCallback error handling path (handlePromptDispatchFailure) does not call releaseForegroundClaimOnFailure, leaving promptInFlight=true for the rest of the turn.

Important Files Changed

Filename Overview
apps/backend/internal/orchestrator/turn_activity.go New file: implements per-session in-memory activity tracker with atomic claim/release mechanics; logic is internally consistent with careful epoch and promptInFlight guards, but dispatch-failure cleanup relies on callers (not all paths covered — see existing threads)
apps/backend/internal/orchestrator/task_operations.go Replaces monolithic session check with loadPromptableSession + claimForegroundForPrompt + trySwitchModelForPrompt helpers; failure-release is wired on ensureSessionRunning, claimSessionRunningForPrompt and model-switch paths but not on PromptWithDispatchCallback failure (pre-existing thread)
apps/backend/internal/orchestrator/event_handlers_streaming.go Adds registerBackgroundTask on tool_call, markForegroundGenerating on message/thinking chunks, and trackBackgroundToolUpdate for tool_call_update terminal clearing; background-task classification by ID membership (not status re-classification) avoids permanent leaks from adapters that drop the Background flag on terminal frames
apps/backend/internal/agentctl/server/adapter/transport/acp/normalize.go Bug fix: isBackgroundExecInput now recognizes both wait:false and run_in_background:true shapes; applied on merge (updateShellExecInput) so later update frames carrying the flag are honoured, and the flag is one-way (never cleared by a subsequent frame)
apps/backend/internal/agent/runtime/lifecycle/session.go Refactors sendPrompt into preparePrompt+triggerPrompt+finishAcceptedPrompt; adds SendPromptWithDispatchCallback which calls onDispatched after agentctl acceptance; serializes with promptMu and waitForPendingDispatchedPrompt to prevent buffer resets racing across dispatch-only and regular prompts; fixes dispatchPrompt returning err instead of retryErr
apps/backend/internal/orchestrator/executor/executor_interaction.go Adds PromptWithDispatchCallback; uses optional interface type assertion for promptAgentWithDispatchCallback; passthrough path invokes onDispatched after a successful PTY write; well-tested with TestExecutor_PromptWithDispatchCallback_RequiresCapableManager
apps/web/lib/ws/handlers/agent-session.ts New session.activity_changed handler with correct guards (state=RUNNING, task_id binding); buildSessionUpdate propagates foreground_activity on every coarse state transition to reset stale values
apps/web/hooks/domains/session/use-session-state.ts deriveSessionFlags now takes the full session object; isAgentBusy narrows to foreground-generating only; isWorking stays true for all RUNNING states including background-idle; absent/unknown foreground_activity safely defaults to generating
apps/web/lib/ui/state-icons.tsx Adds SESSION_BACKGROUND_ICON (spinning emerald loader) for background-idle state; getSessionStateIcon gains an optional foregroundActivity parameter; established RUNNING dot is unchanged
apps/backend/internal/task/handlers/message_handlers.go errorForBlockedMessageSession now calls ForegroundActivity as a hard dependency (compile error if absent) to gate RUNNING sessions; the asymmetry with TaskHandlers' soft dependency is intentional and documented

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Op as Operator
    participant WS as WS Gate (MessageHandlers)
    participant Orch as Orchestrator (promptTask)
    participant TA as turnActivity
    participant Exec as Executor
    participant Agent as ACP Agent

    Note over TA: Default: yielded=false (generating)

    Agent->>Orch: tool_call (run_in_background)
    Orch->>TA: registerBackgroundTask(sessionID, toolCallID)
    TA-->>Orch: "changed=true (was generating to background)"
    Orch->>WS: publish session.activity_changed (background)

    Op->>WS: send message
    WS->>Orch: ForegroundActivity(sessionID)
    Orch->>TA: isForegroundTurnGenerating returns false
    WS-->>Op: accepted (not blocked)

    WS->>Orch: promptTask(sessionID)
    Orch->>TA: claimForegroundTurn returns claim
    TA-->>Orch: "claim with claimGen=N, epoch=E"
    Orch->>Orch: ensureSessionRunning
    Orch->>TA: markForegroundGenerating, foregroundEpoch++
    Orch->>Orch: publish activity_changed (generating)
    Orch->>Exec: PromptWithDispatchCallback(onDispatched)
    Exec->>Agent: dispatch prompt
    Agent-->>Exec: accepted
    Exec->>Orch: onDispatched()
    Orch->>TA: "completeForegroundClaim, promptInFlight=false"

    Agent->>Orch: tool_update terminal (toolCallID)
    Orch->>TA: "completeBackgroundTask, yielded=false, changed=true"
    Orch->>WS: publish session.activity_changed (generating)

    Agent->>Orch: turn complete
    Orch->>TA: clearTurnActivity
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 WS as WS Gate (MessageHandlers)
    participant Orch as Orchestrator (promptTask)
    participant TA as turnActivity
    participant Exec as Executor
    participant Agent as ACP Agent

    Note over TA: Default: yielded=false (generating)

    Agent->>Orch: tool_call (run_in_background)
    Orch->>TA: registerBackgroundTask(sessionID, toolCallID)
    TA-->>Orch: "changed=true (was generating to background)"
    Orch->>WS: publish session.activity_changed (background)

    Op->>WS: send message
    WS->>Orch: ForegroundActivity(sessionID)
    Orch->>TA: isForegroundTurnGenerating returns false
    WS-->>Op: accepted (not blocked)

    WS->>Orch: promptTask(sessionID)
    Orch->>TA: claimForegroundTurn returns claim
    TA-->>Orch: "claim with claimGen=N, epoch=E"
    Orch->>Orch: ensureSessionRunning
    Orch->>TA: markForegroundGenerating, foregroundEpoch++
    Orch->>Orch: publish activity_changed (generating)
    Orch->>Exec: PromptWithDispatchCallback(onDispatched)
    Exec->>Agent: dispatch prompt
    Agent-->>Exec: accepted
    Exec->>Orch: onDispatched()
    Orch->>TA: "completeForegroundClaim, promptInFlight=false"

    Agent->>Orch: tool_update terminal (toolCallID)
    Orch->>TA: "completeBackgroundTask, yielded=false, changed=true"
    Orch->>WS: publish session.activity_changed (generating)

    Agent->>Orch: turn complete
    Orch->>TA: clearTurnActivity
Loading

Reviews (10): Last reviewed commit: "docs: renumber busy-signal ADR reference..." | Re-trigger Greptile

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

@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
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@carlosflorencio

Copy link
Copy Markdown
Member

@point-source can you test and confirm the latest changes work as you intended in your setup?

@carlosflorencio carlosflorencio force-pushed the feat/fine-grained-busy-signal branch from 3869db9 to 5918c47 Compare July 14, 2026 22:21
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.
Comment thread apps/backend/internal/orchestrator/task_operations.go
point-source and others added 9 commits July 15, 2026 07:59
…und work

A RUNNING session whose foreground turn is idle — held open only by spawned background work (a subagent Task, a run_in_background shell, or an active Monitor) — now accepts a new operator message instead of rejecting it as "agent is already running". The busy gate is narrowed to the foreground turn via an in-memory per-session activity tracker; recognition keys on normalized payload shape and defaults to busy, so non-Claude agents are unchanged. The substate is surfaced to the composer and a tri-state status indicator, delivered live over a session.activity_changed WS event and read into the boot/REST/WS DTOs (RUNNING-only, not persisted) so a fresh page-load is correct. Also fixes the ACP normalizer to recognize run_in_background:true shells.

Decision recorded in ADR-0035. Mid-turn steering (delivery into an actively-generating turn) is explicitly out of scope.
- message.add now honors the foreground-idle gate: a RUNNING background-idle session's message is accepted (flows to PromptTask) instead of rejected as busy, so the feature works through the real composer path, not just direct PromptTask (Codex).
- PromptTask publishes activity_changed when it clears the background hold, so the client's stale foreground_activity=background is reset (Greptile/cubic).
- streaming handlers only flip to generating on genuine output, so empty/discarded frames can't spuriously reclose the prompt gate (cubic).
- background tracking runs before the messageCreator nil-guard and skips already-terminal tool_calls, so run-in-background/Monitor still drive the gate and finished calls don't leak (cubic).
- frontend activity_changed guards on RUNNING state + matching task_id; dead foreground_activity arg dropped from the reopen menu (cubic).
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.
…claim durable

Addresses the review round on 73ec934.

- IsActiveMonitor now classifies on adapter attestation, not payload shape. The
  previous round tightened the shape check (demanding task_id), but cubic is right
  that this was never sufficient: Generic.Output is assigned the agent's raw tool
  result verbatim, so *nothing inside it* can vouch for its own origin, however
  many fields we demand. The ACP adapter now stamps a typed MonitorPayload as a
  sibling of the Generic payload, on the path already gated by ACP
  _meta.claudeCode.toolName — metadata the claude-agent-acp wrapper sets and model
  tool output cannot reach. Output keeps carrying the view the frontend card
  renders; it is a presentation contract, not a trust one. An agent can no longer
  relax its own busy gate by emitting a monitor-shaped tool result (cubic P1).

- The foreground claim is now held rather than merely taken, and tracked
  independently of background-idle activity. A background tool_call landing
  mid-admission called registerBackgroundTask, which re-set `yielded` and reopened
  the gate underneath the in-flight prompt — admitting a second one. An in-flight
  claim now keeps the session un-promptable until the prompt is dispatched (cubic
  P1).

- Releasing a claim no longer stomps a live foreground. If the agent's foreground
  began generating for real during preflight, handing the turn back to
  background-idle would let a second prompt overlap a live turn; the release
  carries the epoch it claimed at and declines when it is stale (cubic P1).

- Publish the substate on the two paths that returned without one: a release
  (a client that loaded the page mid-admission was stranded with a disabled
  composer) and a successful non-in-place model switch (clients sat at
  "background" with an enabled composer while the server had already flipped to
  generating and would reject their next send) (coderabbit + cubic P2).

Note the admission window must close *before* executor.Prompt, which blocks for the
whole turn — a claim held across it would keep the session un-promptable for the
entire turn and defeat the feature outright.

Tests: forged-Output rejection (byte-identical to the adapter's own map, minus the
attestation), seed-stamps-attestation guard, background-registration-cannot-reopen,
and stale-release-over-live-foreground.
@carlosflorencio carlosflorencio force-pushed the feat/fine-grained-busy-signal branch from 5918c47 to 3774bd1 Compare July 15, 2026 07:05
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.

2 participants