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).
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_agentjobs runningRunStreamon detached concurrent goroutines):elicitationBridgeis 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.runCollectingsilently discardsElicitationRequestEvents — 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.errNoElicitationChannel/ recover path) → silent failure; foreground elicitations can also bleed into a background collector.PendingEventis a single overwritable slot (pkg/tui/.../supervisor.go:28/202).ResumeElicitation'sdefault:branch returns "no elicitation request in progress" if the response lands in the gap before the handler parks.ElicitationIDexists on the request event but is unused for routing.Proposed fix
ElicitationID(register-before-emit, eliminating the TOCTOUdefault:branch).OnElicitationRequestUI sink mirroring the existingOnBackgroundEventsink so background-job elicitations are surfaced to the foreground UI.elicitation_idfield onapi.ResumeElicitationRequest(it does not carry one today).Acceptance criteria
-race).