Skip to content

fix(web-ai): bound assistant DOM reads by the polling deadline - #90

Closed
dbc-hbin wants to merge 1 commit into
lidge-jun:mainfrom
dbc-hbin:fix/web-ai-bounded-assistant-dom-reads
Closed

fix(web-ai): bound assistant DOM reads by the polling deadline#90
dbc-hbin wants to merge 1 commit into
lidge-jun:mainfrom
dbc-hbin:fix/web-ai-bounded-assistant-dom-reads

Conversation

@dbc-hbin

Copy link
Copy Markdown

Fixes #88

Problem

ChatGPT polling could stay alive with no progress well past --timeout.

pollWebAi checks its deadline only at the while boundary, then awaits:

const answers = await readAssistantMessages(page);

which calls:

await page.evaluate(readTopLevelAssistantTexts, ASSISTANT_SELECTORS)

Playwright's page.evaluate() accepts no timeout option (only locator.evaluate takes one). So when assistant-message extraction stalled on a very large conversation, control never reached the next deadline check. The process and the session lock stayed alive while the stderr heartbeat went silent, which matches the reported symptom: sessions doctor reported a valid target, a healthy command-lock heartbeat, and no CDP or login failure.

The same function also read and cleaned every top-level assistant message on each 500 ms tick, then sliced off everything before baseline.assistantCount — so long conversations paid innerText on the whole history to obtain one new answer.

Changes

  • add withAssistantReadTimeout() and resolveAssistantReadBudgetMs() in chatgpt-response-dom.mjs; each assistant read is raced against the smaller of the remaining command deadline and a per-read ceiling
  • bound the page.evaluate() path, the locator fallback, and the post-timeout recovery read in chatgpt-response-observer.mjs, which otherwise re-hung the command it exists to rescue
  • add readAssistantTextsAfterIndex(): count turns in-page but serialize only the turns at/after the baseline
  • keep emitting the poll heartbeat while reads stall, since silence with a live process was the original symptom
  • surface assistant-dom-read-timeout:<n> on the timeout envelope so a stalled DOM read is distinguishable from provider generation still streaming

Behavior

A read that exceeds its budget retries on the next tick; at the deadline the existing recoverable provider.poll-timeout envelope is returned, so poll and sessions resume semantics are unchanged.

Two details worth reviewing:

  • Exhausting the budget exactly at the loop boundary is treated as the deadline itself, not as a DOM-read failure, so ordinary timeouts are not mislabeled as stalls.
  • When reads were already stalling, the post-timeout recovery read uses a shorter budget so recovery cannot extend the command by another full ceiling.

The trimmed read falls back to the existing full read whenever it observes no turns, which keeps pages that cannot serialize the object argument on the previous behavior.

Tests

New test/unit/web-ai-assistant-read-deadline.test.mjs reproduces the defect deterministically without ChatGPT, as suggested in the issue: it drives pollWebAi with a page.evaluate() that never resolves and asserts the command still honors its own deadline, returns the recoverable timeout envelope, and reports the stall.

It also covers the budget helpers (clamping, rejected reads, sentinel) and proves via getter instrumentation that historical turns are no longer re-serialized.

I verified the test actually catches the bug: with the bound removed, that poll test hangs until the runner's own timeout instead of passing.

One existing fake page in web-ai-provider-session.test.mjs keyed its answer sequence off a raw evaluate call count, so it was updated to key off the assistant-read argument shape instead. The assertions are unchanged.

Verification

npx vitest run test/unit test/integration   # 1438 passed, 5 skipped
npm run typecheck                            # clean
npm run check:strict-baseline                # OK
bash structure/verify-counts.sh              # 76 passed (str_func.md refreshed via fix:counts)

test/integration/post-action-smoke.test.mjs and self-heal-smoke.test.mjs fail in my environment before this change as well, because no Chromium is installed locally; they are unrelated to this diff.

I have not reproduced the fix against a live 40–60 KB multi-turn ChatGPT conversation, so the real-world stall threshold is still unverified; the deadline behavior itself is covered deterministically.

…-jun#88)

ChatGPT polling could stay alive with no progress well past `--timeout`. The
poll loop only re-checked its deadline at the `while` boundary, then awaited
`readAssistantMessages()`, which called `page.evaluate()` with no per-call bound.
Playwright's `page.evaluate()` accepts no timeout option, so when assistant
message extraction stalled on a very large conversation, control never reached
the next deadline check: the process and the session lock stayed alive while the
stderr heartbeat went silent.

- add `withAssistantReadTimeout()` and `resolveAssistantReadBudgetMs()`; every
  assistant read is raced against the smaller of the remaining command deadline
  and a per-read ceiling
- bound the `page.evaluate()` path, the locator fallback, and the post-timeout
  recovery read, which otherwise re-hung the command it exists to rescue
- add `readAssistantTextsAfterIndex()`: count turns in-page but serialize only
  the turns after the baseline, instead of re-serializing the whole conversation
  on every 500ms tick
- keep emitting the poll heartbeat while reads stall, since silence with a live
  process was the original symptom
- report a stalled read distinctly from ongoing generation via an
  `assistant-dom-read-timeout:<n>` warning on the timeout envelope

A read that exceeds its budget now retries on the next tick and, at the
deadline, returns the existing recoverable `provider.poll-timeout` envelope, so
`poll` and `sessions resume` behavior is unchanged. Exhausting the budget at the
loop boundary is treated as the deadline itself, not as a DOM-read failure, so
ordinary timeouts are not mislabeled.

Tests: new `test/unit/web-ai-assistant-read-deadline.test.mjs` drives
`pollWebAi` with a `page.evaluate()` that never resolves and asserts the command
still honors its deadline and reports the stall; it also covers the budget
helpers and proves historical turns are no longer re-serialized. Verified that
removing the bound makes the poll test hang until the runner's own timeout.
@dbc-hbin

Copy link
Copy Markdown
Author

Superseded by #89, which now carries this fix as its own commit (79db5d1) alongside the #87 wiring fix.

Merging them separately would have required resolving a structure/str_func.md line-count conflict in between, since both changes touch web-ai/chatgpt.mjs. #89 keeps the two commits separate so this change can still be reviewed on its own, and it is now verified with the full suite green (163 files / 1454 tests, nothing skipped) plus a live-Chrome check of the bounded read path.

Closing to keep review in one place. No content was dropped.

@dbc-hbin dbc-hbin closed this Jul 28, 2026
@dbc-hbin
dbc-hbin deleted the fix/web-ai-bounded-assistant-dom-reads branch July 28, 2026 00:14
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.

pollWebAi can hang past --timeout when assistant DOM evaluation stalls

1 participant