fix(tour): an unanswered tour bridge no longer costs 45s per action - #90112
Merged
Conversation
The renderer's `tour.request` handler ships in the desktop bundle, but the tool is offered by the backend, and the two update on different clocks. A desktop build older than the tour tool receives the event in a renderer with no branch for it, so `tour.respond` never comes and the agent blocks for the full 45s deadline — once per action the model tries. A single "give me a tour" turn (targets, then narrate, then stop) stacked those waits into minutes of dead air, which is what got reported against #89620. Hold a session's first action to a deadline a working renderer cannot miss, and let an unanswered probe mark the bridge unavailable for that session: later calls return immediately with an error naming the actual fix instead of stalling again. Once a client has answered, real actions get the full deadline back, so a preview tour injecting into a live page still works and one slow action no longer condemns a live client. The verdict lives on the session record, so it dies with the session and a new one re-probes. The same five-action sequence goes from ~225s of dead air to a single 10s probe. Toolset gating is unchanged: removing the tool outright needs a client capability declared at session.create, which prompt caching means can only take effect for a new session.
OutThisLife
force-pushed
the
bb/tour-unanswered-bridge
branch
from
August 19, 2026 16:20
ff81c42 to
84d81d2
Compare
OutThisLife
enabled auto-merge
August 19, 2026 16:20
bobaba76
pushed a commit
to bobaba76/hermes-agent
that referenced
this pull request
Aug 27, 2026
…wered-bridge fix(tour): an unanswered tour bridge no longer costs 45s per action
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
…wered-bridge fix(tour): an unanswered tour bridge no longer costs 45s per action
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fast follow on #89620. A user trying to reproduce the tour demo reported the model timing out on
tour.What's wrong
touris a blocking bridge: Python emitstour.requestand waits up to 45s for the renderer to answertour.respond. The handler that answers ships in the desktop bundle; the tool is offered by the backend. Those update on different clocks.Against a desktop build older than the tour tool, the event lands in a renderer with no branch for it. Nothing answers, and the agent blocks the full 45s — once per action the model tries. Because the schema (correctly) tells the model to call
action='targets'first and then narrate, one "give me a tour" turn stacks those waits:Roughly four minutes of dead air, ending in "The tour request timed out, or no GUI window answered." Nothing in the system noticed that the client provably could not answer the first one.
This is not limited to old builds — a renderer that crashed or a window that went away fails the same way.
The fix
Hold a session's first tour action to a deadline a working renderer cannot miss, and let an unanswered probe mark the bridge unavailable for that session. Later calls then return immediately with an error naming the actual fix, instead of stalling again.
Once a client has answered once, real actions get the full deadline back, so a preview tour injecting its engine into a live page still works, and a single slow action no longer condemns a live client. The verdict lives on the session record, so it dies with the session and a new one re-probes.
Measured on the five-action sequence above: ~225s of dead air becomes a single 10s probe, then instant.
What this deliberately does not do
It does not stop the tool being offered to a client that can't run it. Hiding it needs a capability the client declares at
session.create, and since prompt caching fixes the toolset for the life of a conversation, that can only ever take effect for a new session. It would not have helped the reported session, and it cannot help a renderer that dies mid-conversation — which is why the in-session behaviour is the part worth fixing first. Happy to follow up with the handshake if we want the tool gone entirely for older apps.Tests
tourshipped without any, so this adds both halves:tests/tools/test_tour_tool.py— the tool's own contract (toolset placement, action/surface/step validation, payload shape, JSON pass-through, callback failure).tests/tui_gateway/test_tour_bridge_fail_fast.py— the bridge: first action probes, an answering client gets the full deadline back, an unanswered probe explains the real problem, later actions short-circuit, a proven client survives one slow action, a new session re-probes.scripts/run_tests.sh tests/tui_gateway/ tests/tools/—tests/tui_gateway/fully green. Thetests/tools/failures (daytona, voice, web_tools_config, file_tools) reproduce identically on cleanmainwith the same counts; they're missing optional deps in my local env.