fix(control-hub): make browser.wait actually wait - #2212
Merged
Conversation
`browser.wait` read only `duration_ms`, so the very plausible
`{ "ms": 1800000 }` was dropped and the call fell through to a branch that
returned `{ success: true }` instantly. An agent asked to pause 30 minutes
got "Wait completed" back in milliseconds and moved straight on. The
action documented no parameters at all, so the model had to guess the key,
and even a correct guess was silently capped at 30 seconds.
- Accept the spellings models emit: `duration_ms` / `ms` / `wait_ms` /
`sleep_ms`, plus `seconds` / `secs` variants, numeric strings included.
- Reject a `wait` carrying neither duration nor condition with
INVALID_PARAMS instead of reporting a success that never waited.
- Raise the cap to 60 minutes, in step with AgentWait's MAX_TIMEOUT_MS, and
report `ms` / `requested_ms` / `clamped` so a shortened wait says so.
- Race the sleep against the turn's cancellation token, and stop
`call_impl` from folding Cancelled into an `ok: false` envelope — a stop
during a long pause must not wait out the pause, nor look like a tool
error the model tries to recover from.
- Serve duration waits before session resolution: a pure pause touches no
page, and agents pace themselves long before they open a browser.
- Resolve `{ condition, timeout_ms }`: the condition always wins and any
duration bounds it, rather than sleeping and never looking at the page.
Condition waits keep their previous 15s default, now configurable.
- Document all of it in the tool description, and point repeating schedules
at the Cron tool, which ends the turn instead of pinning it open.
This was referenced Aug 11, 2026
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.
Problem
Asked to pause 30 minutes, the agent called
ControlHub { domain: "browser", action: "wait", params: { ms: 1800000 } }and got back{ ok: true, data: { action: "wait", success: true }, summary: "Wait completed" }— in milliseconds. It then carried straight on to the next step.This was not a flake. Three things stacked up:
params.duration_ms, somswas dropped on the floor.BrowserActions::waitfell through toOk({ success: true, action: "wait" })and returned immediately. That payload — nomsfield,success: true— is exactly what the failing call produced.ms.min(30_000), silently. A 30-minute wait was not expressible.Underneath all three: the ControlHub description documented no
waitparameters whatsoever, so the model had to guess the key and got no feedback when it guessed wrong.Changes
duration_ms/ms/wait_ms/sleep_ms, plusseconds/secs/duration_svariants (converted), numeric strings and floats included. Millisecond keys are read before second keys so a call carrying both cannot be off by 1000×.waitwith neither duration nor condition returnsINVALID_PARAMSwith the correct usage, instead of reporting a success that did not happen.AgentWaitTool::MAX_TIMEOUT_MS. The result reportsms/requested_ms/clamped, and the summary readsWaited 30m00s— or says "clamped" outright when it was shortened.call_implalso stops foldingBitFunError::Cancelledinto anok: falseenvelope, which would both hide a user's stop from the pipeline and invite the model to "recover" from a turn already being torn down.{ condition, timeout_ms }disambiguated — a condition always wins, and any duration passed alongside becomes its timeout rather than a sleep that never looks at the page. Condition waits keep the previous 15s default, now configurable; the selector poll became deadline-based instead of a fixed 30 iterations.Crontool for anything recurring —waitholds the turn open for its full duration, so it suits a one-off pause, not a schedule.Testing
cargo test -p bitfun-core --lib— 1946 passed, 0 failed.Seven new tests cover the parameter aliases, that the wait genuinely elapses, clamp reporting, the missing-parameter rejection, cancellation mid-wait,
{condition, timeout_ms}not degrading into a sleep, and the description carrying both the parameter docs and the Cron pointer.cargo fmt --checkandcargo clippyare unchanged from the base — 7 pre-existing fmt diffs and 11 pre-existingMutexGuard held across awaitfindings, all in files this PR does not touch.