You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tools that return page state in OpenChrome use inconsistent envelopes:
read_page returns mode-specific payloads (ax returns a refs tree; dom returns a compressed DOM string; css returns a styles report).
page_content returns either raw HTML (when outerHTML: true) or a JSON envelope when an element is missing.
inspect returns a structured object.
validate_page returns a checks summary.
None of them prepend a uniform state header identifying (URL, title, mode, capturedAt). The agent must learn each tool's response shape separately and cannot reliably grep "what page is this snapshot from" without parsing the inner payload. This becomes painful once #845 (includeSnapshot chaining) lands and multiple state blobs flow back in a single response.
BrowserMCP/mcp's captureAriaSnapshot (Apache-2.0) wraps every state-returning call with a tiny constant header:
This is a **text-mode-only** convention — the structured JSON returns are untouched.
**Scoping note (deliberate omission)**: BrowserMCP's header has no analogue of CDP `loaderId` because BrowserMCP has no CDP. A `loaderId`-based staleness marker is genuinely useful for OpenChrome agents but requires new CDP plumbing (subscribe to `Page.frameNavigated`, store per-frame `loaderId`, retrieve at capture). That is a **separate issue**; this one prepends only fields that are already cheap to compute (URL, title, server clock).
**Why this is not redundant with #841**: #841 standardises *input descriptions* in `tools/list`. This issue standardises *output envelopes* in tool *responses*. They share no fields.
**Why this is not redundant with #831**: #831 introduces refs and `STALE_REF` semantics on inputs. This issue prepends a header on outputs. They share no fields.
## Proposed Implementation
1. **New helper** `src/tools/_shared/state-header.ts` (the directory may be created by #845 or this issue, whichever lands first):
```ts
export interface PageStateHeader {
url: string;
title: string;
mode: 'ax' | 'dom' | 'css' | 'html' | 'inspect' | 'validate';
capturedAt: number; // unix ms, server wall-clock at response assembly
tabId: string;
}
export function formatHeaderText(h: PageStateHeader): string { /* … */ }
followed by a blank line, then the existing payload.
JSON-mode responses untouched. When a tool already returns a structured object (e.g., page_content with missing element, validate_page structured report), the fields are added as a top-level state object:
Data sources (all already available — no new CDP plumbing):
url: page.url() from puppeteer-core.
title: page.title() from puppeteer-core (cached value; cheap).
tabId: existing session-manager handle.
capturedAt: Date.now() at the moment the response is assembled (post-Ralph wait, if any).
Backward compatibility: env opt-out OPENCHROME_STATE_HEADER=off reverts to pre-PR output byte-exact. Default is on. Snapshot test under tests/fixtures/state-header/ proves byte-identity with v1.11.0 when the env is set to off.
src/tools/_shared/state-header.ts exists with the helper and types.
All four tools (read_page, page_content, inspect, validate_page) prepend the header (text mode) or add the state object (JSON mode).
All four fields populated from existing sources; no new CDP listeners or state tracking introduced.
capturedAt is the server-side wall-clock at the moment the response is assembled (post-Ralph wait if any). Verified to be monotonic-non-decreasing across two consecutive calls on the same tab.
OPENCHROME_STATE_HEADER=off produces byte-identical output to v1.11.0 (snapshot test).
No new dependencies; uses only existing page.url() / page.title() accessors.
npm run build && npm test green.
PR targets develop.
Real verification (post-merge, via openchrome MCP)
Reproducer at scripts/verify/B2-state-header.mjs.
Build and start: node dist/index.js.
mcp__openchrome__navigate to https://example.com.
mcp__openchrome__read_page mode=ax → response text starts with four lines:
mcp__openchrome__inspect { tabId: <t>, selector: "h1" } → same four header lines before the inspect payload, with Page Mode: inspect.
mcp__openchrome__page_content with a missing selector → response is JSON with a top-level state object containing the four fields plus tabId.
Cross-tool consistency check: capture read_page and inspect responses within 100 ms. Their url and title MUST be identical; their capturedAt MUST differ by < 200 ms and be non-decreasing.
Cheap staleness use case: navigate to https://example.org, then call read_page — the url field MUST differ from step 3's value. (This is what makes the header agent-useful: a URL-equality check is enough to invalidate stale refs without re-fetching the whole tree.)
Backward-compat path:
OPENCHROME_STATE_HEADER=off node dist/index.js
Repeat step 3 — response is byte-identical to v1.11.0 output (compared against a checked-in fixture under tests/fixtures/state-header/v1.11.0-read-page-ax.txt).
No CDP regression: launch with OPENCHROME_TRACE_CDP=1; capture the CDP method-name set used during the test session. The set MUST equal the v1.11.0 baseline (no new methods added). Recorded in PR description.
Out of scope (deferred)
A frame-load identifier (loaderId-style staleness token) — needs new CDP listeners; file as a follow-up if URL-equality proves insufficient in practice.
Network-level fields (response status, content-type) in the header — those belong to oc_evidence_bundle.
Per-frame headers for iframe captures (top-frame only in this issue).
Adding the header to page_screenshot — agents already get URL/title from CDP image metadata.
Removing the env opt-out after a deprecation period — not in this PR.
Dropped loaderId from the header — it required new CDP Page.frameNavigated listeners and per-frame state tracking, transforming a 1-day formatter into a CDP-plumbing project. Filed as deferred follow-up.
Removed the false "evidence-bundle already carries these fields" claim — src/core/contracts/evidence-bundle.ts has no context object.
Header is now 4 fields populated entirely from already-cheap page.url() / page.title() / Date.now() accessors; the staleness use case is reframed around URL-equality.
Added a "no new CDP methods" regression assertion to the verification.
OpenChrome 실검증 체크리스트
2026-05-14 재검증 완료. 최신 origin/develop 코드, targeted Jest/lint, OpenChrome CLI 실호출, localhost fixture 산출물로 직접 확인 가능한 항목만 close 근거로 사용했다.
Tier:
core(additive output formatter; no schema breaking change)PR target:
developBackground
Tools that return page state in OpenChrome use inconsistent envelopes:
read_pagereturns mode-specific payloads (axreturns a refs tree;domreturns a compressed DOM string;cssreturns a styles report).page_contentreturns either raw HTML (whenouterHTML: true) or a JSON envelope when an element is missing.inspectreturns a structured object.validate_pagereturns a checks summary.None of them prepend a uniform state header identifying
(URL, title, mode, capturedAt). The agent must learn each tool's response shape separately and cannot reliably grep "what page is this snapshot from" without parsing the inner payload. This becomes painful once #845 (includeSnapshotchaining) lands and multiple state blobs flow back in a single response.BrowserMCP/mcp'scaptureAriaSnapshot(Apache-2.0) wraps every state-returning call with a tiny constant header:read_page(all three modes)page_content(when returning raw HTML)inspectvalidate_pagepage_contentwith missing element,validate_pagestructured report), the fields are added as a top-levelstateobject:{ "state": { "url": "...", "title": "...", "mode": "html", "capturedAt": 1715000000000, "tabId": "t1" }, // …existing fields unchanged… }url:page.url()from puppeteer-core.title:page.title()from puppeteer-core (cached value; cheap).tabId: existing session-manager handle.capturedAt:Date.now()at the moment the response is assembled (post-Ralph wait, if any).OPENCHROME_STATE_HEADER=offreverts to pre-PR output byte-exact. Default ison. Snapshot test undertests/fixtures/state-header/proves byte-identity with v1.11.0 when the env is set tooff.Acceptance Criteria
src/tools/_shared/state-header.tsexists with the helper and types.read_page,page_content,inspect,validate_page) prepend the header (text mode) or add thestateobject (JSON mode).capturedAtis the server-side wall-clock at the moment the response is assembled (post-Ralph wait if any). Verified to be monotonic-non-decreasing across two consecutive calls on the same tab.OPENCHROME_STATE_HEADER=offproduces byte-identical output to v1.11.0 (snapshot test).page.url()/page.title()accessors.npm run build && npm testgreen.develop.Real verification (post-merge, via openchrome MCP)
Reproducer at
scripts/verify/B2-state-header.mjs.node dist/index.js.mcp__openchrome__navigatetohttps://example.com.mcp__openchrome__read_pagemode=ax→ response text starts with four lines:mcp__openchrome__inspect { tabId: <t>, selector: "h1" }→ same four header lines before the inspect payload, withPage Mode: inspect.mcp__openchrome__page_contentwith a missing selector → response is JSON with a top-levelstateobject containing the four fields plustabId.read_pageandinspectresponses within 100 ms. TheirurlandtitleMUST be identical; theircapturedAtMUST differ by < 200 ms and be non-decreasing.https://example.org, then callread_page— theurlfield MUST differ from step 3's value. (This is what makes the header agent-useful: a URL-equality check is enough to invalidate stale refs without re-fetching the whole tree.)tests/fixtures/state-header/v1.11.0-read-page-ax.txt).OPENCHROME_TRACE_CDP=1; capture the CDP method-name set used during the test session. The set MUST equal the v1.11.0 baseline (no new methods added). Recorded in PR description.Out of scope (deferred)
loaderId-style staleness token) — needs new CDP listeners; file as a follow-up if URL-equality proves insufficient in practice.oc_evidence_bundle.page_screenshot— agents already get URL/title from CDP image metadata.Dependencies
_shared/directory) — either issue may create it; the helper here is content-additive.read_page) — independent; this issue is about the envelope, not the refs map.Effort
S (~1 dev-day). One helper, four tool touch-points, one snapshot fixture. No CDP plumbing.
References
captureAriaSnapshot:BrowserMCP/mcp/src/utils/aria-snapshot.ts(Apache-2.0).page.url()/page.title()viarebrowser-puppeteer-core.src/tools/read-page.ts,src/tools/page-content.ts,src/tools/inspect.ts,src/tools/validate-page.ts.Revision history
loaderIdfrom the header — it required new CDPPage.frameNavigatedlisteners and per-frame state tracking, transforming a 1-day formatter into a CDP-plumbing project. Filed as deferred follow-up.src/core/contracts/evidence-bundle.tshas nocontextobject.page.url()/page.title()/Date.now()accessors; the staleness use case is reframed around URL-equality.OpenChrome 실검증 체크리스트
검증 대상
검증 증거
npm run build통과.npm run lint:tier통과: 521 modules / 1239 dependencies, no dependency violations.npm run lint:tool-schemas통과: 82 baselined violations, 0 new.oc_connection_healthconnected, localhost fixturenavigate성공.이슈별 코드/테스트 근거
산출물
.omx/reverify-evidence/targeted-jest.log.omx/reverify-evidence/lint-tier.log.omx/reverify-evidence/lint-tool-schemas.log.omx/reverify-evidence/openchrome-live-smoke.log