Skip to content

Stop an unanswered request for the wheel outliving its run - #145

Merged
davidmckayv merged 4 commits into
mainfrom
fix/a-help-request-does-not-outlive-its-run
Aug 22, 2026
Merged

Stop an unanswered request for the wheel outliving its run#145
davidmckayv merged 4 commits into
mainfrom
fix/a-help-request-does-not-outlive-its-run

Conversation

@davidmckayv

Copy link
Copy Markdown
Contributor

The cross-conversation half of #140.

What it looked like

A brand new channel, on a completely unrelated question, showing:

The assistant needs you. Google Docs is asking for sign-in before I can read the PRD document. [Take control]

with a live button. The request came from a different conversation, minutes earlier, about a document that thread had never mentioned. The audit trail agreed it was still outstanding:

02:54:29  computer.help_requested
01:57:36  computer.control_released
01:56:43  computer.control_taken

A help_requested with nothing after it.

Why

Control is a property of the Bot's computer, not of the conversation that asked. So an unanswered request sits on the computer and is shown to anybody looking at that Bot's screen, in any channel, indefinitely. The run that made it ended long before.

The part to care about is not the stale button. It is the reason: written for the person who asked, and rendered to whoever looks. In a deployment with more than one person that is somebody else's context on your screen — here, the existence and subject of a document.

The change

An unanswered ask stops being shown after ten minutes, and its reason is dropped with it, because the reason is the part that leaked.

Expired on read, not on a timer. There is nothing to wake: the asking run is over, and the only thing that cares is whoever looks next. A timer would be a scheduler for a value nobody is waiting on.

A person holding the wheel is never timed out. They may be halfway through typing a code, and taking the browser back mid-sign-in is worse than any stale prompt. Only the ask expires, never the holding, and there is a test pinning that.

Ten minutes is long enough that somebody who stepped away can still act on it, and short enough that it does not follow the Bot into tomorrow. The run does not resume either way, so the value trades "still useful" against "still on screen" and nothing else.

Tests

Three, on the state machine with an injected clock — the module has no Playwright import, so this needs no browser:

  • inside the window, the request and its reason are still shown
  • past it, requested is false and the reason is gone
  • a person who took the wheel still holds it an hour later
result
bun run test 1156 pass, 8 skip, 0 fail, 1164 across 103 files
bun run typecheck clean, all four packages plus agent-computer
bun run format:check clean

What this does not fix

The other half of #140, and the worse one for the person it happens to: an unanswered handover leaves a tool_calls message with no matching result, so the asking thread is permanently unusable —

400 An assistant message with 'tool_calls' must be followed by tool messages responding to
each 'tool_call_id'.

Every later message in that conversation fails, and the only way out is New chat, which loses it. That needs the run to write a result when it ends without one — "the person did not take the wheel" is an outcome the model can act on, and it keeps the history valid. Separate change; #140 stays open for it.

These are worth separating because they fail differently. This one shows one person's context to another. That one destroys a conversation.

A tool array tells a model a tool exists. It does not tell it the tool is
the right way to reach that system, and it competes with COMPUTER_GUIDANCE
— a page of emphatic prose about the browser that every Bot receives
whether or not it has a single connector, and that mentions connectors
nowhere. "Never claim you cannot browse" is in there. Nothing is.

The browser prose won. A Bot holding four Google Drive tools browsed to
drive.google.com, met a sign-in page its container can never satisfy, and
asked its person to sign in to a vendor that person had already connected.
The tools were loaded the whole time.

So the grants are now said out loud, in the Bot's own instructions, and
before the browser prose rather than after it. The text is generated from
the grants rather than written anywhere: switching a connector on, or
granting one more of its tools, changes what the Bot is told on its next
run with nothing to keep in step. A Bot holding nothing is told nothing.

Both paths, because they are built by different functions and the remote
one is where it failed: a framework Bot gets COMPUTER_GUIDANCE as its
entire prompt, so it had the least reason of any to reach for a tool.

Verified against the document rather than the answer. Read a PRD's text
through the API first, took a line out of it — "Kai reached 83%
company-wide" against weekly active users — then asked in the chat. The
Bot answered 83% and named the metric, using search_files, search_files,
read_file_content, and never touching the browser. Two minutes earlier,
on the same trail, a Bot without those grants met the sign-in wall.
The router saw an id, a name and a role description. So it routed on what
somebody wrote a coworker was for, which is not the same as what it can
do.

Asked what was in a Google Drive document, it chose the coworker whose
description says "company knowledge questions" and which held no Drive
grants at all. That coworker browsed to drive.google.com, met a sign-in
page its container can never satisfy, and asked the person to sign in to
an account they had already connected. The coworker that could have
answered was one line further down the same roster.

Candidates now carry the systems they hold tools for, and the prompt says
to prefer one that can reach what the message names. Purpose still comes
first: a specialist with no connectors is still right for a question
about its specialism, and the instruction says so, because the opposite
failure is sending everything to whoever happens to hold a connector.

Asked per request rather than held, so a grant added a minute ago counts.
A store that is slow or unhappy is treated as holding nothing, because a
connector lookup must never turn "who is this for" into an error. A
deployment with no connectors gets the prompt it got before, down to the
instruction line, which is not added when nothing on the roster can reach
anything.

Driven: the same question that went to Knowledge now goes to Risk
Analyst, with no @ mention, and the recorded reason is "Google Drive
access and investigation of company-wide metrics". It then answered from
the document.
Control belongs to a Bot's computer, not to the conversation that asked
for it, and a request nobody answered sat on that computer forever. The
run that made it had already ended. Everything else carried on.

So a brand new channel, on an unrelated question, showed a live "Take
control" for work the Bot was not doing, captioned "Google Docs is asking
for sign-in before I can read the PRD document" — a reason written for
whoever asked, rendered to whoever happened to look. In a deployment with
more than one person that is somebody else's context on your screen.

An unanswered ask now stops being shown after ten minutes, and its reason
goes with it, since the reason is the part that leaked. Expired on read
rather than on a timer: there is nothing to wake, the asking run is over,
and the only thing that cares is whoever looks next.

A person actually holding the wheel is never timed out. They may be
halfway through typing a code, and taking the browser back mid-sign-in is
worse than any stale prompt. Only the ask expires.

This is the half of #140 that crosses between conversations. The other
half — an unanswered tool call leaving the asking thread unusable with a
400 — is still open and needs the run to write a result when it ends.
Left behind from an earlier shape of these tests, and `lint` is an error
in CI rather than a warning.
@davidmckayv
davidmckayv merged commit 3861dfd into main Aug 22, 2026
8 checks passed
@davidmckayv
davidmckayv deleted the fix/a-help-request-does-not-outlive-its-run branch August 22, 2026 03:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant