Skip to content

fix(drive): retry wait attempts whose reply never arrives - #134

Open
jlongster wants to merge 1 commit into
mainfrom
fix/bounded-ui-wait-attempts
Open

jlongster wants to merge 1 commit into
mainfrom
fix/bounded-ui-wait-attempts

Conversation

@jlongster

Copy link
Copy Markdown
Collaborator

Problem

Right after ui.submit, one ui.matches RPC sometimes gets no reply at all. poll had no per-attempt bound, so that single attempt waited through the whole wait timeout, and waitFor failed with UiWaitTimeoutError even though the text was on screen. The next call returns true immediately.

Root cause (in OpenCode, not Drive)

A wire trace (logging each frame sent and received in opencode-protocol.ts) showed that Drive sent {"id":23,"method":"ui.matches"} and OpenCode never sent a response with id 23. Drive's RPC client is fine. The isolated instance's opencode.log shows why:

level=WARN message="opencode drive ui websocket: request failed"
cause="NativeError: ot_session_acquire_buffer_lease failed: OutputBusy
  at acquireBufferLease (@opentui/core …)
  at withResolvedChars (@opentui/core …)
  at screen (packages/simulation/src/frontend/actions.ts:94)
  at matches (packages/simulation/src/frontend/actions.ts:150)"
  • packages/simulation/src/frontend/actions.ts:94: screen() calls currentRenderBuffer.getRealCharBytes(). While a render holds the output buffer lease (which happens while the first streamed reply is painting), that call throws OutputBusy. ui.matches (frontend/server.ts:24) wraps it in Effect.sync, so the error becomes a defect.
  • packages/simulation/src/control-server.ts:46-47: Effect.catch only handles typed failures and sends a JSON-RPC error. The defect falls through to Effect.catchCause, which only logs a warning and never replies. The client is left waiting forever.

(Line numbers are from OpenCode dev @ b4c5c9cae3.) The proper fix in OpenCode is to send a JSON-RPC error from the catchCause branch, or to retry or await the render before reading the buffer. ui.state and ui.snapshot go through the same path and have the same exposure.

Fix in Drive

Until OpenCode sends that reply, waitFor, getElement, and getNode bound each polling attempt to min(requestTimeout / 2, 1s). A timed-out attempt counts as "not yet", and the loop polls again, so one lost reply can't take over the whole wait. Direct calls (ui.matches(), ui.state()) keep the existing UiTimeoutError behavior, and polling still does not retry RPC errors.

Verification

  • New test: a peer that drops the first ui.matches, ui.state, and ui.snapshot. With the fix, waitFor, getNode, and getElement all succeed. On main it fails with UiTimeoutError.
  • Against opencode2: llm.serve("Hello there."), ui.submit("Say hi"), then ui.waitFor("Hello there.", { timeout: 5000 }), run 12 times each:
    • main: 6/12 timed out with UiWaitTimeoutError.
    • this branch: 12/12 passed. The runs that hit a dropped reply finished in about 1.3–1.5s.
  • bun run check and bun run test pass.

This branch has not been deployed

No deployments
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