feat: accept input while a session's foreground turn waits on background work#1668
feat: accept input while a session's foreground turn waits on background work#1668point-source wants to merge 9 commits into
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe 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. ChangesForeground activity signaling
Estimated code review effort: 5 (Critical) | ~90 minutes Possibly related PRs
Suggested reviewers: 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
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
|
| 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
%%{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
Reviews (10): Last reviewed commit: "docs: renumber busy-signal ADR reference..." | Re-trigger Greptile
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/backend/internal/orchestrator/event_handlers_streaming.go (1)
393-483: 🎯 Functional Correctness | 🔵 TrivialBackground-tracking is unconditional in
handleToolCallEventbut gated behindmessageCreatorhere.
handleToolCallEvent's new registration (lines 259-268) intentionally sits outside themessageCreator != nilblock.trackBackgroundToolUpdatehere is called at line 442, nested inside both theif s.messageCreator == nil { return }guard (404-406) and the statusswitch. IfmessageCreatoris ever nil when tool-update events are processed, a background task registered byhandleToolCallEventcan 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
📒 Files selected for processing (42)
apps/backend/cmd/mock-agent/background_test.goapps/backend/cmd/mock-agent/handler.goapps/backend/cmd/mock-agent/main.goapps/backend/internal/agentctl/server/adapter/transport/acp/monitor.goapps/backend/internal/agentctl/server/adapter/transport/acp/normalize.goapps/backend/internal/agentctl/server/adapter/transport/acp/normalize_background_test.goapps/backend/internal/agentctl/types/streams/monitor_view.goapps/backend/internal/agentctl/types/streams/monitor_view_test.goapps/backend/internal/backendapp/boot_state.goapps/backend/internal/events/types.goapps/backend/internal/gateway/websocket/task_notifications.goapps/backend/internal/gateway/websocket/task_notifications_test.goapps/backend/internal/orchestrator/event_handlers_streaming.goapps/backend/internal/orchestrator/foreground_activity_signal_test.goapps/backend/internal/orchestrator/foreground_busy_signal_test.goapps/backend/internal/orchestrator/prompt_background_acceptance_test.goapps/backend/internal/orchestrator/service.goapps/backend/internal/orchestrator/task_operations.goapps/backend/internal/orchestrator/turn_activity.goapps/backend/internal/task/dto/dto.goapps/backend/internal/task/dto/foreground_activity_test.goapps/backend/internal/task/handlers/foreground_activity_test.goapps/backend/internal/task/handlers/task_handlers.goapps/backend/internal/task/handlers/task_http_handlers.goapps/backend/internal/task/handlers/task_ws_handlers.goapps/backend/pkg/api/v1/task.goapps/backend/pkg/websocket/actions.goapps/web/components/task/session-reopen-menu.tsxapps/web/components/task/sessions-dropdown.tsxapps/web/e2e/tests/chat/busy-signal.spec.tsapps/web/e2e/tests/chat/mobile-busy-signal.spec.tsapps/web/hooks/domains/session/use-session-state.test.tsapps/web/hooks/domains/session/use-session-state.tsapps/web/lib/state/slices/session/session-slice.upsert.test.tsapps/web/lib/types/backend.tsapps/web/lib/types/http.tsapps/web/lib/ui/state-icons.test.tsxapps/web/lib/ui/state-icons.tsxapps/web/lib/ws/handlers/agent-session.test.tsapps/web/lib/ws/handlers/agent-session.tsdocs/decisions/0035-fine-grained-foreground-idle-busy-signal.mddocs/decisions/INDEX.md
There was a problem hiding this comment.
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
|
Thanks for the thorough reviews. Addressed in Fixed
Acknowledged — follow-ups, not regressions
|
FindingsSuggestion (recommended, doesn't block)
Summary
Verdict: Ready to merge with suggestions. The core state machine ( |
|
Thanks for the contribution. The direction makes sense 💪, but I think the prompt-admission path needs a little more work before merging:
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. |
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
apps/backend/internal/agentctl/server/adapter/transport/acp/monitor.goapps/backend/internal/agentctl/server/adapter/transport/acp/monitor_contract_test.goapps/backend/internal/agentctl/types/streams/monitor_view.goapps/backend/internal/agentctl/types/streams/monitor_view_test.goapps/backend/internal/orchestrator/foreground_claim_test.goapps/backend/internal/orchestrator/task_operations.goapps/backend/internal/orchestrator/turn_activity.goapps/backend/internal/task/handlers/message_handlers.godocs/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
There was a problem hiding this comment.
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
FindingsSuggestion (recommended, doesn't block)
Summary
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. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/backend/internal/agent/runtime/lifecycle/session_test.go (1)
894-915: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRegister test resources with
t.Cleanup.Replace the new
defer mock.Close()anddefer client.Close()calls with cleanup callbacks registered immediately after creation.As per coding guidelines, lifecycle tests must register
t.Cleanupimmediately 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
📒 Files selected for processing (13)
apps/backend/internal/agent/runtime/lifecycle/manager_interaction.goapps/backend/internal/agent/runtime/lifecycle/session.goapps/backend/internal/agent/runtime/lifecycle/session_test.goapps/backend/internal/agent/runtime/lifecycle/types.goapps/backend/internal/backendapp/adapters.goapps/backend/internal/orchestrator/event_handlers_streaming.goapps/backend/internal/orchestrator/executor/executor_interaction.goapps/backend/internal/orchestrator/foreground_activity_signal_test.goapps/backend/internal/orchestrator/foreground_busy_signal_test.goapps/backend/internal/orchestrator/foreground_claim_test.goapps/backend/internal/orchestrator/task_operations.goapps/backend/internal/orchestrator/turn_activity.godocs/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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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.
1a923b8 to
3869db9
Compare
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
|
@point-source can you test and confirm the latest changes work as you intended in your setup? |
3869db9 to
5918c47
Compare
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.
…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.
5918c47 to
3774bd1
Compare
What & why
A session whose durable state reads
RUNNINGused 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 subagentTask, arun_in_backgroundshell, 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-
RUNNINGbehavior.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
checkSessionPromptableto the foreground turn via an in-memory per-session activity tracker; default stays busy.run_in_background:trueshell shape (a genuine bug fix regardless of the feature).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 asession.activity_changedWS 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
/background 20s(spawns a subagent and holds the turn open with the foreground idle).RUNNINGwith 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.WAITING_FOR_INPUT(done check).CI: backend
go test -race ./..., changed-filegolangci-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.
Preview Environment
73ec934