Skip to content

feat: stabilize snapshot refs - #1272

Merged
Nikhil (shadowfax92) merged 1 commit into
mainfrom
feat/stable-snapshot-ref-identity
Jun 19, 2026
Merged

Nikhil (shadowfax92) merged 1 commit into
mainfrom
feat/stable-snapshot-ref-identity

Conversation

@shadowfax92

Copy link
Copy Markdown
Contributor

Summary

  • Reuse public eN snapshot refs for the same backend DOM nodes within a stable document.
  • Reset the public ref namespace on top-level navigation, URL changes, reloads, or unavailable document identity.
  • Keep diff matching ref-sensitive while preventing insertion-before-existing-node ref cascades.

Design

Observer now owns a committed document/url ref scope and renders each capture into a transactional RefMap fork. RefMap keeps document-scoped backend-node assignments separate from latest-snapshot resolution metadata, while no-document or unstable child-frame captures fall back to traversal-order refs.

Test plan

  • bun test apps/server/src/browser/core/snapshot/refs.test.ts apps/server/src/browser/core/snapshot/render.test.ts apps/server/src/browser/core/snapshot/diff.test.ts apps/server/src/browser/core/observer/resolve.test.ts apps/server/src/browser/core/observer/observer.test.ts
  • bun run check
  • Local adversarial review pass: clean after fixes

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Greptile (@greptileai) review

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Claude (@claude) review

@shadowfax92
Nikhil (shadowfax92) merged commit 050a13a into main Jun 19, 2026
17 of 19 checks passed
@claude

claude Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

Claude finished Nikhil (@shadowfax92)'s task in 5m 53s —— View job


Code Review

  • Gather context and understand the PR
  • Read changed files (refs.ts, observer.ts, render.ts, diff.ts, resolve.ts, all tests)
  • Analyze design and implementation
  • Post review feedback

Overall this is well-structured work — the two-tier ref resolution design is elegant and the URL stability guard is robust. A few issues to flag.


Bug: resolveRef discards its getSession result

observer.ts:55

async resolveRef(ref: string): Promise<ResolvedElement> {
  const entry = this.refs.get(ref)
  if (!entry) { throw new Error(`Unknown ref ${ref}; take a new snapshot.`) }
  await this.pages.getSession(this.pageId)   // ← result discarded
  const { session, axParams } = this.frames.resolveFrameTarget(
    this.pageId, entry.frameId,
  )
  return resolveRefEntry(session, entry, axParams)
}

The getSession call is here for a side effect — PageManager.attach() enables Page, DOM, Runtime, and Accessibility on the CDP session before resolveFrameTarget uses it. The same implicit pattern appears in capture() (line 64). But the intent is invisible, and if someone later refactors resolveFrameTarget to call attach internally (which would be the right call), this line silently becomes a wasteful round-trip. Add a comment or, better, extract a private async ensureSessionAttached() helper that makes the intent explicit. Fix this →


Missing test file: refs.test.ts

The PR's changed-files list shows refs.test.ts as ADDED (195 lines, SHA: unknown), but the file does not exist anywhere in the repository. The tests in render.test.ts and resolve.test.ts cover some RefMap behavior indirectly, but the standalone unit tests for RefMap.mint() / nth-counting / key construction are gone. This should be tracked down and re-committed.


Design claim vs implementation: ref cascade prevention

The PR summary states:

"Reuse public eN snapshot refs for the same backend DOM nodes within a stable document."
"preventing insertion-before-existing-node ref cascades"

But capture() (observer.ts:67) creates a fresh new RefMap() on every invocation, so ref numbers are always assigned by DOM traversal order. If a node is inserted before an existing one, all downstream refs shift — the cascade still happens between snapshots. What the PR actually provides is:

  1. Intra-action stability — refs from the last snapshot persist in this.refs until the next snapshot()/diff() call; resolveRef can still resolve stale refs via role+name+nth (the real robustness gain).
  2. Capture-attempt atomicity — the URL guard ensures a capture isn't committed if the page navigated mid-capture; the fresh RefMap per-attempt prevents partial assignments from leaking.

Neither of these is "reusing the same eN across snapshots for the same backendNodeId." The PR description overstates what's implemented. Worth either correcting the description or implementing backendNodeId→ref persistence (a document-scoped byBackendNodeId: Map<number, string> in RefMap).


findByRoleNameNth doesn't mirror isDropped filtering

resolve.ts:62–80 walks the AX tree counting (role, name) occurrences to find nth. The renderer's mint path goes through isDropped, which skips ROOT_ROLES, SKIP_ROLES, and unnamed generic/group nodes. findByRoleNameNth only checks node.ignored; it counts every node with a matching role+name regardless of isDropped status.

In practice, ROOT_ROLES/SKIP_ROLES nodes don't get refs, so they can't appear as lookup targets. The risk is a future role change (e.g., a generic container with a name gets added to SKIP_ROLES) causing a nth-count mismatch between render time and resolve time. Consider applying isDropped in the walk to keep the two paths logically coupled. Fix this →


Nit: null-byte separator is undocumented

refs.ts:32

const key = `${node.frameId ?? ''}${node.role}${node.name}`

The null byte is a valid separator (role/name come from the AX tree and can't contain ``), but this invariant isn't obvious. A one-line comment like // is safe: AX role and name never contain null bytes would save future readers from worrying about injection. Fix this →


Positives

  • Two-tier resolution (resolve.ts) — cached backendNodeId then role+name+nth re-query — is well-designed; the entry.backendNodeId mutation on tier-2 is a clean cache-refresh.
  • Capture atomicity — URL check before/after in capture() with 3 attempts and correct knownUrlsDiffer (ignoring 'unknown' URLs) is solid.
  • Iframe stitching — [...iframes].reverse() before splicing keeps earlier line indices valid while inserting bottom-up. ✓
  • Cycle detection — visited: Set<FrameId> in captureFrame correctly guards against recursive frames.
  • splitLines — value === '' ? [] : value.split('\n') correctly avoids the [''] edge case that breaks LCS.
  • diffSnapshotObservations — returning the full snapshot text on URL change is the right UX choice.
  • Test coverage for renderSnapshot and diffSnapshots is thorough.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Tests failed — 2/1303 failed

Suite Passed Failed Skipped
✅ agent 197/197 0 0
✅ build 18/18 0 0
✅ eval 91/91 0 0
✅ server-agent 301/301 0 0
✅ server-api 141/141 0 0
❌ server-browser 8/10 2 0
✅ server-integration 10/10 0 0
✅ server-lib 256/257 0 1
✅ server-root 47/50 0 3
✅ server-tools 228/228 0 0
Failed tests
  • server-browser — Observer URL lookup > falls back to the refreshed tab registry URL when frame tree URL lookup fails
  • server-browser — Observer URL lookup > returns unknown when frame tree and tab registry fallbacks both fail

View workflow run

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces stable eN snapshot refs by wiring backend DOM node identity (documentId + backendNodeId) through RefMap, renderSnapshot, and Observer, so refs survive same-document DOM mutations (e.g. insertions) without cascading into false diffs. Refs are reset on top-level navigation, URL changes, reloads, or when document identity is unavailable, and failed captures no longer clobber the last committed ref state.

  • RefMap gains byStableNode/stableRefs maps, plus forkForSnapshot() (transactional working copy), beginSnapshot() (per-snapshot reset without losing stable assignments), and reset() (full clear on navigation).
  • Observer now reads MainFrameState (url + documentId + child frame docs) before/after each capture; refsForCapture() either forks the committed RefMap or starts fresh depending on shouldResetRefs; stableDocumentIdForFrame verifies child-frame stability before assigning document-scoped refs.
  • A comprehensive new test suite covers ref reuse, insertion stability, reset scenarios, failed-capture immutability, and child-frame churn fallback.

Confidence Score: 4/5

Safe to merge; the core ref-stability logic is correct and well-tested. Two minor cleanup items in observer.ts worth addressing before or shortly after merge.

The collectFrameDocuments function writes a second redundant documents.set(node.frame.id, documentId) on every frame — for child frames it duplicates the previous line, for the root frame it stores a key that is never read. Neither causes wrong behavior today, but the dead entry accumulates and the pattern violates the team's no-dead-code rule. stableDocumentIdForFrame also makes one fresh getFrameTree() round-trip per child iframe, which could add up on pages with many iframes, but is not a correctness concern. Everything else — the RefMap state machine, the fork/commit flow, the reset conditions, and the failure isolation — looks correct and is covered by the new tests.

packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts — the redundant second documents.set in collectFrameDocuments and the per-child-frame getFrameTree() overhead in stableDocumentIdForFrame.

Important Files Changed

Filename Overview
packages/browseros-agent/apps/server/src/browser/core/snapshot/refs.ts New RefMap implementation adds stable backend-node identity via byStableNode/stableRefs, forkForSnapshot(), beginSnapshot(), and reset() — logic is correct but forkForSnapshot() has two redundant .clear() calls on freshly-created collections.
packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts Major redesign: Observer now tracks a RefScope and forks RefMap per capture; two issues — collectFrameDocuments always writes a dead second entry per frame (duplicate for children, unreachable key for root), and stableDocumentIdForFrame issues one extra getFrameTree() CDP call per child iframe.
packages/browseros-agent/apps/server/src/browser/core/snapshot/render.ts Minimal change: adds optional documentId to RenderOptions and threads it into refs.mint() in formatLine — correct and clean.
packages/browseros-agent/apps/server/src/browser/core/observer/observer.test.ts New test file with five well-targeted scenarios: insertion stability, reload reset, URL-change reset, failed-capture non-mutation, and child-frame churn fallback — good coverage of the main invariants.
packages/browseros-agent/apps/server/src/browser/core/snapshot/refs.test.ts New test file covering stable ref reuse, per-snapshot byRef trimming, document reset, no-documentId fallback, frame scoping, and nth recomputation — thorough.
packages/browseros-agent/apps/server/src/browser/core/snapshot/diff.test.ts Adds one focused test confirming that stable refs prevent insertion-at-top from cascading into false removals — correctly verifies the diff interaction.
packages/browseros-agent/apps/server/src/browser/core/snapshot/render.test.ts Adds a render-level test for ref reuse across same-document snapshots with a mid-list insertion — correctly validates the stable ref contract at the render layer.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts:278-294
**Redundant second `documents.set` call in `collectFrameDocuments`**

For child frames, both `set` calls write to the same key: `documents.set(node.frame.id, documentId)` on both line 1 (when `isRoot=false`) and line 2 — the second is an exact duplicate. For the root frame, line 2 stores an entry keyed by the root's literal `frame.id`, which is never looked up — `stableDocumentIdForFrame` and all call-sites access the root via `frameId = undefined`, not by its string id. Both cases make the second `documents.set` dead code, which violates the team rule on removing unused code.

### Issue 2 of 3
packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts:204-219
**O(N child frames) extra `getFrameTree()` CDP calls per snapshot**

`stableDocumentIdForFrame` issues a fresh `getFrameTree()` round-trip for every non-root child frame in order to confirm its document hasn't churned. A page with N iframes therefore makes N+2 `getFrameTree()` calls per snapshot cycle (the two outer `readMainFrameState` calls plus one per frame). A simple improvement would be to call `readFrameDocuments` once after all frames are rendered and pass that "after" map down, rather than re-fetching per frame.

### Issue 3 of 3
packages/browseros-agent/apps/server/src/browser/core/snapshot/refs.ts:31-44
The `fork.byStableNode.clear()` and `fork.stableRefs.clear()` calls are no-ops on the freshly-constructed `RefMap` (its collections are already empty). They can be removed to reduce noise.

```suggestion
  forkForSnapshot(): RefMap {
    const fork = new RefMap()
    fork.nextRefNum = this.nextRefNum
    for (const [key, ref] of this.byStableNode) {
      fork.byStableNode.set(key, ref)
    }
    for (const ref of this.stableRefs) {
      fork.stableRefs.add(ref)
    }
    fork.beginSnapshot()
    return fork
  }
```

Reviews (1): Last reviewed commit: "feat: stabilize snapshot refs by documen..." | Re-trigger Greptile

Comment on lines +278 to +294
function collectFrameDocuments(
tree: FrameTreeNode,
): Map<FrameId | undefined, DocumentId> {
const documents = new Map<FrameId | undefined, DocumentId>()

function visit(node: FrameTreeNode, isRoot: boolean): void {
const documentId = frameDocumentId(node.frame)
if (documentId !== undefined) {
documents.set(isRoot ? undefined : node.frame.id, documentId)
documents.set(node.frame.id, documentId)
}
for (const child of node.childFrames ?? []) visit(child, false)
}

visit(tree, true)
return documents
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Redundant second documents.set call in collectFrameDocuments

For child frames, both set calls write to the same key: documents.set(node.frame.id, documentId) on both line 1 (when isRoot=false) and line 2 — the second is an exact duplicate. For the root frame, line 2 stores an entry keyed by the root's literal frame.id, which is never looked up — stableDocumentIdForFrame and all call-sites access the root via frameId = undefined, not by its string id. Both cases make the second documents.set dead code, which violates the team rule on removing unused code.

Rule Used: Remove unused/dead code rather than leaving it in ... (source)

Learned From
browseros-ai/BrowserOS-agent#126

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts
Line: 278-294

Comment:
**Redundant second `documents.set` call in `collectFrameDocuments`**

For child frames, both `set` calls write to the same key: `documents.set(node.frame.id, documentId)` on both line 1 (when `isRoot=false`) and line 2 — the second is an exact duplicate. For the root frame, line 2 stores an entry keyed by the root's literal `frame.id`, which is never looked up — `stableDocumentIdForFrame` and all call-sites access the root via `frameId = undefined`, not by its string id. Both cases make the second `documents.set` dead code, which violates the team rule on removing unused code.

**Rule Used:** Remove unused/dead code rather than leaving it in ... ([source](https://app.greptile.com/browseros-org-2/-/custom-context?memory=9b045db4-2630-428c-95b7-ccf048d34547))

**Learned From**
[browseros-ai/BrowserOS-agent#126](https://github.com/browseros-ai/BrowserOS-agent/pull/126)

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +204 to +219
private async stableDocumentIdForFrame(
rootSession: ProtocolApi,
frameId: FrameId | undefined,
frameDocuments: Map<FrameId | undefined, DocumentId>,
): Promise<DocumentId | undefined> {
const beforeDocumentId = frameDocuments.get(frameId)
if (frameId === undefined || beforeDocumentId === undefined) {
return beforeDocumentId
}

const latest = await this.readFrameDocuments(rootSession).catch(
() => undefined,
)
const afterDocumentId = latest?.get(frameId)
return afterDocumentId === beforeDocumentId ? beforeDocumentId : undefined
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 O(N child frames) extra getFrameTree() CDP calls per snapshot

stableDocumentIdForFrame issues a fresh getFrameTree() round-trip for every non-root child frame in order to confirm its document hasn't churned. A page with N iframes therefore makes N+2 getFrameTree() calls per snapshot cycle (the two outer readMainFrameState calls plus one per frame). A simple improvement would be to call readFrameDocuments once after all frames are rendered and pass that "after" map down, rather than re-fetching per frame.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts
Line: 204-219

Comment:
**O(N child frames) extra `getFrameTree()` CDP calls per snapshot**

`stableDocumentIdForFrame` issues a fresh `getFrameTree()` round-trip for every non-root child frame in order to confirm its document hasn't churned. A page with N iframes therefore makes N+2 `getFrameTree()` calls per snapshot cycle (the two outer `readMainFrameState` calls plus one per frame). A simple improvement would be to call `readFrameDocuments` once after all frames are rendered and pass that "after" map down, rather than re-fetching per frame.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR stabilizes snapshot refs by reusing eN handles for the same backend DOM node within a single document, preventing spurious ref cascades on insertions before existing nodes, and resetting the namespace on navigation, reload, or URL change.

  • RefMap gains byStableNode / stableRefs persistent state plus forkForSnapshot() so each capture starts from a transactional copy of the previous stable assignments.
  • Observer now reads Page.getFrameTree() before and after each capture to derive a MainFrameState (documentId + url) used to decide fork-vs-reset, and threads documentId down through captureFrame → renderSnapshot → refs.mint() so stable lookup is per-document.
  • Child frames that churn during a capture silently fall back to traversal-order refs without failing the page capture.

Confidence Score: 4/5

The change is safe to merge. All new behaviour is covered by tests, the fork/commit design prevents failed captures from corrupting committed refs, and navigation correctly resets the ref namespace.

The core logic is sound and well-tested. A few style issues were found: redundant .clear() calls in forkForSnapshot, a collectFrameDocuments double-set for non-root frames, and a reset() method that is public and tested but never called from production code. A performance concern exists where stableDocumentIdForFrame issues a fresh getFrameTree CDP call for every child frame per snapshot. None of these affect correctness.

refs.ts and observer.ts are the two files with the noted issues; the test files and render.ts are clean.

Important Files Changed

Filename Overview
packages/browseros-agent/apps/server/src/browser/core/snapshot/refs.ts Core RefMap rewrite: adds stable backend-node → public ref identity across snapshots via byStableNode/stableRefs, with forkForSnapshot() carrying state between captures. Contains redundant .clear() calls in forkForSnapshot() and a public reset() that is never called from production code.
packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts Observer now tracks a RefScope (documentId + url) to decide when to fork vs. reset the RefMap; reads frame tree before/after capture for stability. collectFrameDocuments has a redundant set for child frames, and stableDocumentIdForFrame fires O(N) extra getFrameTree calls for N iframes.
packages/browseros-agent/apps/server/src/browser/core/snapshot/render.ts Minimal change: adds optional documentId to RenderOptions and threads it through to refs.mint(), enabling stable-ref lookup. No correctness issues.
packages/browseros-agent/apps/server/src/browser/core/snapshot/refs.test.ts New comprehensive test file covering ref reuse, snapshot isolation, reset, fallback, frame scoping, and nth disambiguation. Good coverage of the happy paths and edge cases.
packages/browseros-agent/apps/server/src/browser/core/observer/observer.test.ts New integration-level tests for Observer: covers insertion stability, reload/navigation reset, failed-capture idempotence, and child-frame churn fallback. Well-structured stubs and clear assertions.
packages/browseros-agent/apps/server/src/browser/core/snapshot/render.test.ts Adds a test verifying ref reuse across two renderSnapshot calls after an insertion — correctly exercises the new documentId-scoped stable-ref path.
packages/browseros-agent/apps/server/src/browser/core/snapshot/diff.test.ts Adds one targeted diff test confirming that stable refs turn top-level insertions into exactly 1 added line with 0 removed. Straightforward and correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Agent
    participant Observer
    participant RefMap
    participant CDP as CDP (Page/AX)

    Agent->>Observer: snapshot()
    Observer->>CDP: Page.getFrameTree() [before]
    CDP-->>Observer: "MainFrameState {url, documentId, frameDocuments}"
    Observer->>RefMap: shouldResetRefs? → fork or new RefMap
    Observer->>CDP: Accessibility.getFullAXTree (main frame)
    Observer->>CDP: Page.getFrameTree() [child stability check per iframe]
    CDP-->>Observer: documentId stable?
    Observer->>RefMap: mint(node, documentId) → reuse or assign eN
    Observer->>CDP: Page.getFrameTree() [after]
    CDP-->>Observer: MainFrameState
    Observer->>Observer: knownMainFrameChanged? retry or commit
    Observer->>RefMap: commit fork as this.refs + store refScope
    Observer-->>Agent: "SnapshotResult {text, refs, url}"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Agent
    participant Observer
    participant RefMap
    participant CDP as CDP (Page/AX)

    Agent->>Observer: snapshot()
    Observer->>CDP: Page.getFrameTree() [before]
    CDP-->>Observer: "MainFrameState {url, documentId, frameDocuments}"
    Observer->>RefMap: shouldResetRefs? → fork or new RefMap
    Observer->>CDP: Accessibility.getFullAXTree (main frame)
    Observer->>CDP: Page.getFrameTree() [child stability check per iframe]
    CDP-->>Observer: documentId stable?
    Observer->>RefMap: mint(node, documentId) → reuse or assign eN
    Observer->>CDP: Page.getFrameTree() [after]
    CDP-->>Observer: MainFrameState
    Observer->>Observer: knownMainFrameChanged? retry or commit
    Observer->>RefMap: commit fork as this.refs + store refScope
    Observer-->>Agent: "SnapshotResult {text, refs, url}"
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
packages/browseros-agent/apps/server/src/browser/core/snapshot/refs.ts:47-54
**`reset()` is dead code in the production call path**

`Observer.refsForCapture()` always creates `new RefMap()` when a document reset is needed — `reset()` is never called from production code. Per the team's convention of removing dead code rather than maintaining it, this method should either be removed or used in place of `new RefMap()` in `refsForCapture()`.

### Issue 2 of 4
packages/browseros-agent/apps/server/src/browser/core/snapshot/refs.ts:31-44
Redundant `.clear()` calls in `forkForSnapshot` — `fork` is a freshly constructed `RefMap` whose `byStableNode` and `stableRefs` are already empty. The no-op clears before the copy loops add noise without benefit.

```suggestion
  forkForSnapshot(): RefMap {
    const fork = new RefMap()
    fork.nextRefNum = this.nextRefNum
    for (const [key, ref] of this.byStableNode) {
      fork.byStableNode.set(key, ref)
    }
    for (const ref of this.stableRefs) {
      fork.stableRefs.add(ref)
    }
    fork.beginSnapshot()
    return fork
  }
```

### Issue 3 of 4
packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts:204-219
**O(N) `Page.getFrameTree()` calls for child frames**

`stableDocumentIdForFrame` fires a fresh `Page.getFrameTree()` CDP round-trip for every non-root frame captured. With N iframes on a page, this adds N sequential CDP calls per snapshot. Since the "after" frame-tree read in `capture()` already fetches the full tree at the end, the fork/child stability check could reuse that result (or at least cache the call per `captureFrame` pass) instead of issuing a fresh call per frame.

### Issue 4 of 4
packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts:283-290
For non-root frames both `documents.set` calls set the same key (`node.frame.id`), since `isRoot ? undefined : node.frame.id` evaluates to `node.frame.id` when `isRoot` is false. The second line is a no-op for every child frame and should be removed to avoid misleading readers.

```suggestion
  function visit(node: FrameTreeNode, isRoot: boolean): void {
    const documentId = frameDocumentId(node.frame)
    if (documentId !== undefined) {
      if (isRoot) documents.set(undefined, documentId)
      documents.set(node.frame.id, documentId)
    }
    for (const child of node.childFrames ?? []) visit(child, false)
  }
```

Reviews (2): Last reviewed commit: "feat: stabilize snapshot refs by documen..." | Re-trigger Greptile

Comment on lines +47 to +54
reset(): void {
this.byRef.clear()
this.byStableNode.clear()
this.stableRefs.clear()
this.nthCounter.clear()
this.nextRefNum = 1
this.nextFallbackRefNum = 1
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 reset() is dead code in the production call path

Observer.refsForCapture() always creates new RefMap() when a document reset is needed — reset() is never called from production code. Per the team's convention of removing dead code rather than maintaining it, this method should either be removed or used in place of new RefMap() in refsForCapture().

Rule Used: Remove unused/dead code rather than leaving it in ... (source)

Learned From
browseros-ai/BrowserOS-agent#126

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/browser/core/snapshot/refs.ts
Line: 47-54

Comment:
**`reset()` is dead code in the production call path**

`Observer.refsForCapture()` always creates `new RefMap()` when a document reset is needed — `reset()` is never called from production code. Per the team's convention of removing dead code rather than maintaining it, this method should either be removed or used in place of `new RefMap()` in `refsForCapture()`.

**Rule Used:** Remove unused/dead code rather than leaving it in ... ([source](https://app.greptile.com/browseros-org-2/-/custom-context?memory=9b045db4-2630-428c-95b7-ccf048d34547))

**Learned From**
[browseros-ai/BrowserOS-agent#126](https://github.com/browseros-ai/BrowserOS-agent/pull/126)

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +204 to +219
private async stableDocumentIdForFrame(
rootSession: ProtocolApi,
frameId: FrameId | undefined,
frameDocuments: Map<FrameId | undefined, DocumentId>,
): Promise<DocumentId | undefined> {
const beforeDocumentId = frameDocuments.get(frameId)
if (frameId === undefined || beforeDocumentId === undefined) {
return beforeDocumentId
}

const latest = await this.readFrameDocuments(rootSession).catch(
() => undefined,
)
const afterDocumentId = latest?.get(frameId)
return afterDocumentId === beforeDocumentId ? beforeDocumentId : undefined
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 O(N) Page.getFrameTree() calls for child frames

stableDocumentIdForFrame fires a fresh Page.getFrameTree() CDP round-trip for every non-root frame captured. With N iframes on a page, this adds N sequential CDP calls per snapshot. Since the "after" frame-tree read in capture() already fetches the full tree at the end, the fork/child stability check could reuse that result (or at least cache the call per captureFrame pass) instead of issuing a fresh call per frame.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/browser/core/observer/observer.ts
Line: 204-219

Comment:
**O(N) `Page.getFrameTree()` calls for child frames**

`stableDocumentIdForFrame` fires a fresh `Page.getFrameTree()` CDP round-trip for every non-root frame captured. With N iframes on a page, this adds N sequential CDP calls per snapshot. Since the "after" frame-tree read in `capture()` already fetches the full tree at the end, the fork/child stability check could reuse that result (or at least cache the call per `captureFrame` pass) instead of issuing a fresh call per frame.

How can I resolve this? If you propose a fix, please make it concise.

Dmitrii Vasilev (gHashTag) added a commit to gHashTag/BrowserOS that referenced this pull request Aug 17, 2026
…ing of zero

Rendering was NOT verified visually — the three intents are asserted in code,
not on screen.

Closes browseros-ai#1272
Dmitrii Vasilev (gHashTag) added a commit to gHashTag/BrowserOS that referenced this pull request Sep 1, 2026
…work

The bee on browseros-ai#1272 did the work and reported it in full: nine criteria, every one
marked met. The review answered "3 of 9 criteria judged so far", said wait, and
the task went on holding its files.

The bee was not at fault and neither was the policy. `parseVerdictBlock` reads
line by line, and the fourth criterion had wrapped:

  - `grep -c "why is
   it green at this number" Makefile` prints `1`: met

The continuation line did not match the bullet pattern, and the loop treated
that as the end of the block and BROKE - discarding the six criteria after it.
So a correct, complete report was scored as a third of itself because one line
was too long.

Continuation lines are now joined before the block is parsed, and the original
rule survives intact: a line that follows a COMPLETE bullet still ends the
block, because the bee was told nothing follows it. Only a line continuing an
unfinished bullet is glued on. Without that distinction a bee's closing
paragraph would be parsed as criteria and the review would judge prose.

Both halves are gated: the real browseros-ai#1272 block quoted verbatim from the
transcript, and a check that prose after a finished criterion still stops the
parse. Proven by disabling the join and watching the first go red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dmitrii Vasilev (gHashTag) added a commit to gHashTag/BrowserOS that referenced this pull request Sep 4, 2026
…es (#303)

The AttributedString rewrite of styledText landed (ee8f61e) and
WARNING_CEILING came down to 0, but the comment above the constant still
argued the opposite: WHY FOUR, ASKED HONESTLY, 'It is not lowered to zero
today', citing RichTextRenderer.swift lines 73/75/77/79 - lines that in
today's tree are the innocent body of the rewrite itself. The same
present-tense claim ('is 4 now', 'the four that remain') sat in the same
continuous comment run further up, and the e2e-testing skill still told
agents 'the number stays'.

- Makefile: the comment above WARNING_CEILING now states zero as the
  requirement and records, in the past tense, that the value was 4 for as
  long as RichTextRenderer.swift concatenated Text values with the
  deprecated +, and that the AttributedString rewrite removed them. The
  XCTEST_ERROR_CEILING lesson survives: asking 'why is it green at this
  number' is what found that one. The same history fix is applied to the
  'count was 5 ... is 4 now' paragraph in the same comment run. The diff
  is comment lines only; WARNING_CEILING := 0 and the warnings recipe are
  untouched.
- .claude/skills/e2e-testing/SKILL.md: the ceiling passage records the
  sequence - 4 was a reading, the AttributedString rewrite removed the
  four concatenations, the ceiling is now 0 and derived from the
  requirement.

Closes browseros-ai#1272.

Co-authored-by: queen-1272 bee <bee@localhost>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant