Skip to content

fix(runtime): avoid false on_user_input notifications - #4010

Merged
aheritier merged 1 commit into
mainfrom
fix/4004-on-user-input-notifications
Aug 21, 2026
Merged

fix(runtime): avoid false on_user_input notifications#4010
aheritier merged 1 commit into
mainfrom
fix/4004-on-user-input-notifications

Conversation

@Sayt-0

@Sayt-0 Sayt-0 commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

  • fire on_user_input at stream teardown only when an interactive root session is waiting for the next prompt
  • skip the hook for sub-sessions, background agents, non-interactive runs, and canceled max-iteration waits
  • preserve genuine notifications for tool confirmations, elicitations, and interactive max_iterations
  • attribute notifications to the resolved session agent and include the conversation ID for elicitations
  • add regression coverage for background-agent completion and input-wait behavior

Fixes #4004

Issue expectations

Expectation Implementation
Background-agent completion must not emit a false input notification Sub-session and non-interactive teardowns no longer fire on_user_input
Genuine input waits must still notify Root interactive completion, tool confirmation, elicitation, and interactive max_iterations remain covered
max_iterations behavior must be explicit Interactive sessions notify before waiting; non-interactive and already-canceled sessions stop without notifying
Consumers currently cannot distinguish teardown reasons Gating was selected instead of extending the hook payload, avoiding unnecessary false events
Session-pinned agents must use the correct hook configuration The resolved agent is propagated to the hook dispatcher

Validation

  • task build
  • task lint
  • go vet ./...
  • task test
  • targeted runtime and tool-dispatch tests with the race detector

Residual consideration

toolexec.HookDispatcher.NotifyUserInput now receives the resolved agent explicitly. This is technically an exported interface signature change, but repository-wide search found only the runtime implementation and its test stub.

@Sayt-0
Sayt-0 requested a review from a team as a code owner August 20, 2026 13:36
@aheritier aheritier added area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/feat PR adds a new feature (maps to feat:). Use on PRs only. labels Aug 20, 2026

@aheritier aheritier left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified locally on 6d5ad03: go build ./..., go test ./pkg/runtime/..., and the new tests under -race all pass. CI is green on this SHA.

The core reasoning holds up. on_user_input means "control is being handed back to a human", and the three gates match that: loop.go:221 skips sub-session and non-interactive teardowns, loop_steps.go:91-105 moves the hook after the non-interactive auto-stop and behind a ctx.Err() guard so a cancelled run can't announce a wait it will never perform, and the elicitation callsite now uses genai.ConversationIDFromContext(ctx) — consistent with elicitation.go:524, which already derived its session id the same way, so the previously-empty session_id is now correct.

Test quality is the strongest part: I applied pkg/runtime/on_user_input_test.go unchanged to origin/main, and 7 of the 8 tests fail there (only RootInteractiveStreamEnd_FiresOnce, which pins pre-existing semantics, passes). That is real regression coverage rather than restated implementation.

The exported signature change is contained: git grep NotifyUserInput finds only hookDispatcher (tool_dispatch.go:129) and the test stub (toolexec/helpers_test.go:62), matching what the description claims.

[should-fix] One-shot --exec runs still emit the false notification

Not a regression from this PR, and not a blocker — but the description says non-interactive runs are now skipped, and that is only true for A2A, MCP serve, chatserver, and evals, which set session.NonInteractive explicitly. Nothing under cmd/ ever sets it (git grep NonInteractive -- cmd returns nothing), so a CLI run keeps NonInteractive == false:

  • pkg/cli/runner.go:291-298len(userMessages) > 0: runs oneLoop per message, then returns and the process exits
  • pkg/cli/runner.go:279-289 / :299-312 — the - and non-tty-stdin branches: single oneLoop, then exit

Each of those streams tears down through loop.go:221 as a root, "interactive" session, so the hook fires — the exact shape asserted by TestOnUserInputHooks_RootInteractiveStreamEnd_FiresOnce. Concretely, docker agent run --exec agent.yaml "do X" in CI still triggers the desktop notification documented at docs/guides/tips/index.md:389, with no user to notify. Only pkg/cli/runner.go:314-336 (the bare interactive prompt loop) genuinely waits. Worth a follow-up: mark the session non-interactive in the one-shot branches, or have cli.Run signal which mode it is in.

[optional] Third copy of the recording-builtin harness

runtimeWithRecordedUserInput (on_user_input_test.go:31-58) is now the third near-identical copy alongside runtimeWithRecordedAgentSwitch (on_agent_switch_test.go:55-83) and runtimeWithRecordedSessionResume (on_session_resume_test.go:19-41). Following the established pattern is defensible; at three copies a shared helper parameterized by event + builtin name would pay for itself.

[optional] Docs could state the narrowed contract

docs/configuration/hooks/index.md:53 ("When the agent is waiting for user input") is not stale — the fix makes behavior match it more closely. Since consumers deliberately cannot distinguish teardown reasons, a clause noting that sub-sessions, background agents, and non-interactive runs never fire it would save readers a source dive.

[optional] Label

Labelled kind/feat while the commit is fix(runtime):; that will place this under "What's New" instead of "Bug Fixes" in the generated changelog.

Nice touches: every non-obvious gate carries a short WHY comment with a repository-qualified #4004 reference rather than a bare assertion, and the cancelled-context test also asserts MaxIterationsReached is still emitted, pinning that the guard narrows only the input wait.

@aheritier aheritier added kind/fix PR fixes a bug (maps to fix:). Use on PRs only. and removed kind/feat PR adds a new feature (maps to feat:). Use on PRs only. labels Aug 21, 2026
@aheritier
aheritier merged commit 23f3c88 into main Aug 21, 2026
15 checks passed
@aheritier
aheritier deleted the fix/4004-on-user-input-notifications branch August 21, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

on_user_input hook fires on every stream teardown, not just real elicitation (false 'needs input' notifications for background agents)

2 participants