Skip to content

Concurrent background-job elicitations are silently dropped — nothing displayed to the user #3584

Description

@aheritier

Summary

When user input (an elicitation request) is triggered from multiple concurrent background jobs, nothing is displayed to the user at all — the request is silently dropped and the requesting handler blocks forever. This is a functional failure experienced frequently in practice, not just a theoretical race.

Root cause

The elicitation path assumes a single, synchronous, LIFO-nested request at a time, which breaks under concurrency (e.g. run_background_agent jobs running RunStream on detached concurrent goroutines):

  • The elicitationBridge is a single global channel slot with a LIFO-nesting assumption (pkg/runtime/elicitation.go:59, pkg/runtime/loop.go:307/217). Concurrent background jobs cause the swaps/restores to interleave arbitrarily.
  • runCollecting silently discards ElicitationRequestEvents — it only handles content/tokens/errors (pkg/runtime/agent_delegation.go:366-383). So a background job's elicitation event lands in a collector that drops it while the handler parks forever.
  • Interleaved teardown leaves the channel slot nil or closed (foreground turn ends first → errNoElicitationChannel / recover path) → silent failure; foreground elicitations can also bleed into a background collector.
  • Display side: the tab supervisor's PendingEvent is a single overwritable slot (pkg/tui/.../supervisor.go:28/202).
  • There is also a TOCTOU: ResumeElicitation's default: branch returns "no elicitation request in progress" if the response lands in the gap before the handler parks. ElicitationID exists on the request event but is unused for routing.

Proposed fix

  • Per-request response channels keyed by ElicitationID (register-before-emit, eliminating the TOCTOU default: branch).
  • A stable OnElicitationRequest UI sink mirroring the existing OnBackgroundEvent sink so background-job elicitations are surfaced to the foreground UI.
  • Additive elicitation_id field on api.ResumeElicitationRequest (it does not carry one today).
  • Headless/non-interactive fast-decline with a model-readable note.
  • A supervisor pending-event queue instead of a single overwritable slot.

Acceptance criteria

  • Elicitation requests raised from concurrent background jobs are displayed to the user and can be answered, with responses routed back to the correct waiter.
  • No silent drops when multiple elicitations are pending simultaneously.
  • Regression tests covering concurrent/background-job elicitation (with -race).

Metadata

Metadata

Assignees

Labels

area/apiFor features/issues/fixes related to the usage of the cagent APIarea/runtimeRuntime engine, agent loop execution, tool dispatch, loop detectionarea/tuiFor features/issues/fixes related to the TUIkind/fixPR fixes a bug (maps to fix:). Use on PRs only.

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions