Skip to content

read_page mode:'content' — rendered pages through the extraction pipeline#189

Merged
NotASithLord merged 1 commit into
mainfrom
feat/read-page-content
Jul 8, 2026
Merged

read_page mode:'content' — rendered pages through the extraction pipeline#189
NotASithLord merged 1 commit into
mainfrom
feat/read-page-content

Conversation

@jonybur

@jonybur jonybur commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Follow-on to #187, aimed at the OM2W failure taxonomy (budget exhaustion on tab-driven tasks — the dominant fixable bucket at the 26.7% baseline).

  • read_page gains mode:'content': grabs the rendered DOM's outerHTML (post-JS — pages fetch_url can't see) and routes it through the same offscreen Readability+Turndown extractor and the same spill-and-page machinery fetch_url content pipeline: clean-markdown extraction + spill-and-page #187 shipped. One new tiny injected grabber; no new vendoring, no page-world library injection, same untrusted fence. Fail-open to the default snapshot everywhere (non-readerable / Firefox / extraction error / grab failure).
  • Default snapshot mode untouched (content mode returns no interactables; the tool description teaches READ vs OPERATE).
  • Paging-loop counter-pressure: the fetch_url content pipeline: clean-markdown extraction + spill-and-page #187 measurement caught one task walking a whole spilled document (465k actor tokens); read_web_cache now teaches deliberate paging.

Zero added authority: same gated dispatch, same fence, the markdown transform runs on bytes the actor could already read.

Tests: content-mode suite (extraction, spill+footer-outside-fence, all fail-open paths, default-mode untouched). Full battery green (2747 bun tests, strict typecheck, eslint, tscheck floor, no gen drift).

Measurement: the fetch suite covers the fetch path; a rendered-page before/after needs a tab-heavy task set — proposed as the same before/after protocol on the web-actor suite once #119 (which carries it) merges, then an OM2W-30 re-run to see if the baseline moves.

🤖 Generated with Claude Code

@jonybur jonybur requested a review from NotASithLord as a code owner July 7, 2026 14:08
@jonybur

jonybur commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Measured: Online-Mind2Web re-run on this branch — 46.7% (14/30), up from the 26.7% baseline

Same 30 tasks, same model (Opus 4.8), same judge (WebJudge/o4-mini), fresh export on this branch (= main incl. #119+#187, plus this PR's content mode).

July 7 baseline this branch
Pass rate 26.7% (8/30) 46.7% (14/30)
Median steps/task 11 8
25-step-cap deaths 6 4

Per-task: 6 both-pass, 8 gained, 2 lost (ordinary n=1 live variance — both are "didn't state the final answer" at similar step counts), 14 both-fail. The gains are the mechanism working, most visibly AeroAPI: 26 steps (capped, answer cut off) → 4 steps, PASS — the actor read the pricing page instead of thrashing against it. Amazon add-to-cart completed at 14→8 steps; the editor's-choice hunt came in under the cap at 26→19.

Attribution, honestly: the baseline predates the July merges, so this delta measures the whole content pipeline (#187 extraction + this PR's read_page content mode + paging) together with main's July fixes (#183, #184). Isolating this PR alone would cost another run; the composed number is the one that matters for the roadmap.

Context: this puts peerd above browser-use's independently verified 40% (full-300, Princeton HAL) on this 30-task subset — with the usual caveats (subset, ±8pp sampling noise, n=1 per task). The remaining failure mass is the incomplete-final-action bucket (14 both-fail), which is the next lever.

Scorecards: out/6aa56e07-pipeline{,_result} vs out/6aa56e07{,_result} under scripts/cdp/om2w/.

@NotASithLord NotASithLord left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review pass on the diff, plus local verification (checked out the branch, ran the new test file and bun install).

Implementation: sound. mode:'content' is a faithful, minimal-risk extension of the #187 pipeline:

  • Reuses the exact same webOffscreenClient.extractMarkdown + windowText/pagingFooter spill machinery fetch_url already ships, rather than reimplementing it — verified by diffing the two call sites (fetch-url.js:129-165 vs the new block in read-page.js). Same fencing (wrapUntrusted, footer outside the fence), same fail-open shape.
  • Fail-open is real and multi-layered: no webOffscreenClient, readerable:false, a thrown extraction error, and a failed executeScript grab (grabbed?.html falsy) all fall through to the unchanged default snapshot path below. Ran the test suite locally (bun test tests/peerd-runtime/tools/read-page-content.test.ts) — 4/4 pass, and they do cover all three fail-open branches plus the untouched-default-mode case.
  • readOuterHtmlInjected's classic-function-with-'use strict' shape matches the file's existing readPageInjected convention (both are chrome.scripting.executeScript-injected bodies, consistent with CLAUDE.md's injected-script exemption).
  • The CAPABILITY_CONSUMERS.webCache update (adding 'read_page') is correct and necessary — content mode now reads ctx.webCache on overflow, so a narrowed/spawned actor granted read_page but not fetch_url/read_web_cache would otherwise silently lose the closure. Confirmed against spawn.js's strip logic — fails open to a direct slice, not a crash, same as fetch_url's own !webCache branch.
  • 2MB injected-grab cap is a sane belt-and-suspenders bound ahead of the offscreen extractor's own cap.

CI is fully green (12/12 checks). No other correctness, security, or test-coverage issues found.

Blocking: missing DCO sign-off. Neither commit on this branch (21d3847e, the merge commit 15eaeebe) carries a Signed-off-by trailer. CONTRIBUTING.md/the PR template require git commit -s for every PR, and this exact gap blocked #170 last review pass. Please amend/rebase with -s before merge.

Once sign-off is added, this looks mergeable as-is.


Generated by Claude Code

…re as markdown

The OM2W failure taxonomy says budget exhaustion on tab-driven tasks; the
snapshot's 4k text cap forces re-reads and wandering. mode:'content' grabs
the LIVE DOM's outerHTML (post-JS, unlike fetch_url's served bytes) and
routes it through the SAME offscreen Readability+Turndown extractor and the
SAME spill-and-page machinery fetch_url uses (16k window, read_web_cache
paging). Default snapshot mode untouched - content mode returns no
interactables, so the description teaches READ vs OPERATE.

FAIL-OPEN: a non-readerable page, a missing extractor (Firefox), an
extraction error, or a failed grab all fall through to the default snapshot.

Plus the paging-loop counter-pressure from the #187 measurements (one task
walked a whole spilled document): read_web_cache's description now teaches
deliberate paging - the window usually carries the answer; 1-2 targeted
slices, not a document walk.

Signed-off-by: Jonathan Bursztyn <jobur93@gmail.com>
@jonybur jonybur force-pushed the feat/read-page-content branch from 15eaeeb to 70df9cd Compare July 8, 2026 00:51
@jonybur

jonybur commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Signed — the branch is now a single DCO-signed commit (70df9cd); the unsigned merge commit is gone (history flattened onto main, tree verified byte-identical to the reviewed tip, so the 46.7% OM2W run above measured exactly this code).

@NotASithLord NotASithLord left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the DCO block — the sign-off checkbox is being removed from the PR template (history was 78% unsigned; the convention was never enforced). Prior review found the implementation sound and CI is 12/12 green, so approving on the merits.


Generated by Claude Code

@NotASithLord NotASithLord merged commit ab35dc6 into main Jul 8, 2026
12 checks passed
@NotASithLord NotASithLord deleted the feat/read-page-content branch July 8, 2026 02:28
NotASithLord pushed a commit that referenced this pull request Jul 8, 2026
A skeptical, adversarially-verified review of the four just-merged PRs
surfaced a set of correctness, integration, doc, and test-hygiene defects.
Each fix below survived a 2–3 vote adversarial verification pass.

Correctness
- manifests.js: the `research` / `browse-only` presets allowed a spill
  producer (fetch_url, read_page mode:'content') but omitted read_web_cache,
  so a spilled body's trusted paging footer instructed a tool the manifest
  gate then refused — the spilled tail unreachable, turns burned on
  guaranteed-refused calls. Add read_web_cache to both presets (read-only,
  local cache, no new authority).
- read-page.js: mode:'content' clipped the raw DOM at 2MB but reported
  truncated:false, silently dropping the tail. Surface an explicit
  htmlTruncated signal (distinct from the paging `truncated` flag).
- read-web-cache.js: the evicted-entry hint said "re-fetch the URL", which
  for a read_page spill steers the actor to fetch_url a page it already
  rendered (losing the post-JS DOM). Make the recovery hint source-aware.

Provider integration (#170 rebase + adjacent)
- error-classify.js: Z.ai signals out-of-credit as "insufficient balance …
  recharge" on HTTP 429; the needles missed it, so a spent GLM account got 3
  pointless retries and no failover. Add the needles (+ regression test).
- pricing.js: glm-4.6 was priced identically to the glm-5.2 flagship
  ($1.4/$4.4), a ~3x overestimate that inflated cost and tripped spend limits
  early. Correct to its published tier ($0.43/$1.74) (+ regression test).
- openai.js: the stream parser was called without a provider label, so
  in-stream errors on OpenAI chats were attributed to "openrouter". Pass
  { provider: 'openai' }.

Docs / classification
- CLAUDE.md, README.md, peerd-provider/index.js: provider inventory said
  OpenAI "doesn't exist yet" and omitted OpenAI + Z.ai GLM; both ship. Update
  (and defer to registry.js per the no-hard-coded-inventories rule).
- idb.js: web_extract_cache was labeled "fetched public bytes"; read_page
  content mode can store a logged-in tab's rendered DOM. Correct the comment.
- error-display.js: usage-limit copy hard-coded "Anthropic / OpenRouter
  billing"; genericize (the error already names the provider dynamically).
- docs/benchmarks/2026-07-web-thread.md: reconcile the failure-taxonomy
  count (categories overlap) and the 6/29 code-arm denominator (one run did
  not complete scoring).

Test hygiene
- spawn.test.ts: the "strips EVERY capability" test passed vacuously (the
  fixture omitted webCache). Complete the fixture, keep the universal claim
  honest, and add the read_page→webCache grant assertion #189 introduced.
- read-page-content.test.ts: pin the grab-failure fail-open path the header
  claimed but never exercised, plus the new htmlTruncated signal.

NOT fixed here (flagged for a deliberate follow-up): unpriced runner models
(glm-4.5-air, OpenRouter's default haiku) render $0.00 and bypass the spend
limit — the correct fix is a cost-path change (thread `estimated` through the
tally; fail-closed on unpriced under a set limit) affecting multiple
providers, not an obvious one-liner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8EYHtUpzTX6YGfEXPRPKF
NotASithLord added a commit that referenced this pull request Jul 8, 2026
A skeptical, adversarially-verified review of the four just-merged PRs
surfaced a set of correctness, integration, doc, and test-hygiene defects.
Each fix below survived a 2–3 vote adversarial verification pass.

Correctness
- manifests.js: the `research` / `browse-only` presets allowed a spill
  producer (fetch_url, read_page mode:'content') but omitted read_web_cache,
  so a spilled body's trusted paging footer instructed a tool the manifest
  gate then refused — the spilled tail unreachable, turns burned on
  guaranteed-refused calls. Add read_web_cache to both presets (read-only,
  local cache, no new authority).
- read-page.js: mode:'content' clipped the raw DOM at 2MB but reported
  truncated:false, silently dropping the tail. Surface an explicit
  htmlTruncated signal (distinct from the paging `truncated` flag).
- read-web-cache.js: the evicted-entry hint said "re-fetch the URL", which
  for a read_page spill steers the actor to fetch_url a page it already
  rendered (losing the post-JS DOM). Make the recovery hint source-aware.

Provider integration (#170 rebase + adjacent)
- error-classify.js: Z.ai signals out-of-credit as "insufficient balance …
  recharge" on HTTP 429; the needles missed it, so a spent GLM account got 3
  pointless retries and no failover. Add the needles (+ regression test).
- pricing.js: glm-4.6 was priced identically to the glm-5.2 flagship
  ($1.4/$4.4), a ~3x overestimate that inflated cost and tripped spend limits
  early. Correct to its published tier ($0.43/$1.74) (+ regression test).
- openai.js: the stream parser was called without a provider label, so
  in-stream errors on OpenAI chats were attributed to "openrouter". Pass
  { provider: 'openai' }.

Docs / classification
- CLAUDE.md, README.md, peerd-provider/index.js: provider inventory said
  OpenAI "doesn't exist yet" and omitted OpenAI + Z.ai GLM; both ship. Update
  (and defer to registry.js per the no-hard-coded-inventories rule).
- idb.js: web_extract_cache was labeled "fetched public bytes"; read_page
  content mode can store a logged-in tab's rendered DOM. Correct the comment.
- error-display.js: usage-limit copy hard-coded "Anthropic / OpenRouter
  billing"; genericize (the error already names the provider dynamically).
- docs/benchmarks/2026-07-web-thread.md: reconcile the failure-taxonomy
  count (categories overlap) and the 6/29 code-arm denominator (one run did
  not complete scoring).

Test hygiene
- spawn.test.ts: the "strips EVERY capability" test passed vacuously (the
  fixture omitted webCache). Complete the fixture, keep the universal claim
  honest, and add the read_page→webCache grant assertion #189 introduced.
- read-page-content.test.ts: pin the grab-failure fail-open path the header
  claimed but never exercised, plus the new htmlTruncated signal.

NOT fixed here (flagged for a deliberate follow-up): unpriced runner models
(glm-4.5-air, OpenRouter's default haiku) render $0.00 and bypass the spend
limit — the correct fix is a cost-path change (thread `estimated` through the
tally; fail-closed on unpriced under a set limit) affecting multiple
providers, not an obvious one-liner.


Claude-Session: https://claude.ai/code/session_01L8EYHtUpzTX6YGfEXPRPKF

Co-authored-by: Claude <noreply@anthropic.com>
@NotASithLord NotASithLord mentioned this pull request Jul 12, 2026
NotASithLord added a commit that referenced this pull request Jul 12, 2026
Promotes [Unreleased] to [0.2.7] and bumps the version. The changelog
was empty going into this cut, so wrote entries for the 5 user-facing
PRs since v0.2.6 (Z.ai GLM as a BYOK provider #170, the fetch_url
clean-markdown + spill-and-page pipeline #187, read_page mode:'content'
#189, the experimental web-actor code-REPL surface + the actor-cascade
stop fix #119, and the post-merge correctness fixes #194), skipping the
3 that are internal-only (a benchmark report doc, a PR-template chore).
Grounded in each PR's own description, not invented; zero em dashes.

Signed-off-by: Ariel Deschapell <arieldeschapell@Ariels-MacBook-Air.local>
Co-authored-by: Ariel Deschapell <arieldeschapell@Ariels-MacBook-Air.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants