Skip to content

fix(server): close pages by refreshed target id#979

Closed
Nikhil (shadowfax92) wants to merge 1 commit intodevfrom
polecat/garnet/bosmain-wcw@moxnktij
Closed

fix(server): close pages by refreshed target id#979
Nikhil (shadowfax92) wants to merge 1 commit intodevfrom
polecat/garnet/bosmain-wcw@moxnktij

Conversation

@shadowfax92
Copy link
Copy Markdown
Contributor

Summary

Automated refinery PR for BrowserOS issue #438 from polecat branch.

  • Issue bead: bosmain-wcw
  • Merge request bead: bosmain-wisp-4qv
  • Polecat: garnet
  • Branch: polecat/garnet/bosmain-wcw@moxnktij

Verification

Refinery rebased the branch on current origin/dev and ran the recorded verification commands.

  • bun run lint from packages/browseros-agent passed
  • The following local target/environment failures reproduce on clean origin/dev and are tracked as pre-existing:
    • bosmain-bjx: server browser tests cannot resolve @browseros/shared constants
    • bosmain-woi: server typecheck cannot find tsc
    • bosmain-vbu: server build cannot resolve @smithy/core/endpoints

Fixes #438

@github-actions github-actions Bot added the fix label May 9, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

✅ Tests passed — 1226/1230

Suite Passed Failed Skipped
agent 80/80 0 0
build 9/9 0 0
eval 93/93 0 0
server-agent 261/261 0 0
server-api 203/203 0 0
server-browser 7/7 0 0
server-integration 9/10 0 1
server-lib 242/242 0 0
server-root 60/63 0 3
server-skills 31/31 0 0
server-tools 231/231 0 0

View workflow run

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 9, 2026

Greptile Summary

This PR fixes closePage to close tabs by targetId instead of tabId, and adds resilience for the case where a tab's target is refreshed between the time the page info is cached and when the close is attempted.

  • closePage now calls resolvePageInfo (cache-first, then listPages on miss) before closing, and retries with freshly-fetched page info when the initial closeTab call fails due to a stale targetId.
  • Two new private helpers (resolvePageInfo, unknownPageError) extract repeated patterns from the class, and a new test file covers all three closePage paths.

Confidence Score: 3/5

The retry path introduced to handle stale targetIds leaves the original session entry in this.sessions uncleaned, causing a map leak every time a tab's target changes during close.

The main change is a well-motivated fix for stale-targetId closes, but the session cleanup at the end of closePage always deletes the new targetId on the retry path, not the original one that actually has a session entry. This is a concrete defect on the exact code path the PR is meant to harden.

packages/browseros-agent/apps/server/src/browser/browser.ts — specifically the cleanup block at the end of closePage and how info.targetId is used after reassignment.

Important Files Changed

Filename Overview
packages/browseros-agent/apps/server/src/browser/browser.ts Refactors closePage to use targetId instead of tabId, adds cache-miss refresh via resolvePageInfo, and introduces a retry on stale-targetId errors — but the session cleanup deletes the new targetId instead of the original one on the retry path, leaking the old session entry.
packages/browseros-agent/apps/server/tests/browser/browser.test.ts New test file covering the three closePage scenarios (normal, cache-miss refresh, and targetId-change retry); second test hardcodes page ID 1, making it sensitive to the initial value of nextPageId.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Browser
    participant CDPBrowser as cdp.Browser
    participant PagesMap as pages / sessions

    Caller->>Browser: closePage(pageId)
    Browser->>PagesMap: resolvePageInfo(pageId)
    alt cache hit
        PagesMap-->>Browser: PageInfo (cached)
    else cache miss
        Browser->>CDPBrowser: listPages()
        CDPBrowser-->>Browser: updated pages
        PagesMap-->>Browser: PageInfo (refreshed)
    end

    Browser->>CDPBrowser: "closeTab({ targetId: info.targetId })"
    alt success
        CDPBrowser-->>Browser: ok
        Browser->>PagesMap: pages.delete(pageId)
        Browser->>PagesMap: sessions.delete(info.targetId)
    else error (targetId stale)
        CDPBrowser-->>Browser: error
        Browser->>Browser: refreshPageInfo(pageId)
        alt targetId/tabId changed
            Browser->>CDPBrowser: "closeTab({ targetId: refreshed.targetId })"
            CDPBrowser-->>Browser: ok
            Browser->>PagesMap: pages.delete(pageId)
            Note over Browser,PagesMap: sessions.delete(refreshed.targetId) — OLD targetId entry leaks
        else unchanged
            Browser-->>Caller: rethrow error
        end
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/browseros-agent/apps/server/src/browser/browser.ts:613-628
**Stale `targetId` session entry leaks on retry path**

When the retry branch is taken (`info = refreshed`), the final `this.sessions.delete(info.targetId)` deletes the *new* `targetId` — which almost certainly has no session entry — while the session that was registered under the *old* `targetId` is left in the map indefinitely. Every close that hits this retry path leaks one entry in `this.sessions`. The fix is to capture the original `targetId` before reassigning `info` and delete that one.

### Issue 2 of 2
packages/browseros-agent/apps/server/tests/browser/browser.test.ts:102-113
**Hardcoded page ID `1` makes the test fragile**

`closePage(1)` assumes `Browser.nextPageId` starts at `1`, so the first page produced by `listPages()` gets assigned ID `1`. If the initial counter ever changes, this test silently exercises the "unknown page → `listPages()` refresh" branch instead of the "cache miss" scenario it intends to cover. Using `pages[0].pageId` (after calling `listPages()` explicitly, as done in the other tests) would remove the assumption.

Reviews (1): Last reviewed commit: "fix: close pages by target id (bosmain-w..." | Re-trigger Greptile

Comment on lines +613 to 628
} catch (error) {
const refreshed = await this.refreshPageInfo(page)
if (
!refreshed ||
(refreshed.targetId === info.targetId && refreshed.tabId === info.tabId)
) {
throw error
}

info = refreshed
await this.cdp.Browser.closeTab({ targetId: info.targetId })
}

this.consoleCollector.detach(page)
this.pages.delete(page)
this.sessions.delete(info.targetId)
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.

P1 Stale targetId session entry leaks on retry path

When the retry branch is taken (info = refreshed), the final this.sessions.delete(info.targetId) deletes the new targetId — which almost certainly has no session entry — while the session that was registered under the old targetId is left in the map indefinitely. Every close that hits this retry path leaks one entry in this.sessions. The fix is to capture the original targetId before reassigning info and delete that one.

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

Comment:
**Stale `targetId` session entry leaks on retry path**

When the retry branch is taken (`info = refreshed`), the final `this.sessions.delete(info.targetId)` deletes the *new* `targetId` — which almost certainly has no session entry — while the session that was registered under the *old* `targetId` is left in the map indefinitely. Every close that hits this retry path leaks one entry in `this.sessions`. The fix is to capture the original `targetId` before reassigning `info` and delete that one.

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

Comment on lines +102 to +113
it('refreshes pages before treating closePage page IDs as unknown', async () => {
const { browser, cdp } = createBrowser([
createTab({ tabId: 402, targetId: 'target-402' }),
])

await browser.closePage(1)

assert.deepStrictEqual(cdp.closeTabCalls, [{ targetId: 'target-402' }])
assert.strictEqual(cdp.tabs.length, 0)
})

it('retries closePage when the tab target changes before close', async () => {
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 Hardcoded page ID 1 makes the test fragile

closePage(1) assumes Browser.nextPageId starts at 1, so the first page produced by listPages() gets assigned ID 1. If the initial counter ever changes, this test silently exercises the "unknown page → listPages() refresh" branch instead of the "cache miss" scenario it intends to cover. Using pages[0].pageId (after calling listPages() explicitly, as done in the other tests) would remove the assumption.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/tests/browser/browser.test.ts
Line: 102-113

Comment:
**Hardcoded page ID `1` makes the test fragile**

`closePage(1)` assumes `Browser.nextPageId` starts at `1`, so the first page produced by `listPages()` gets assigned ID `1`. If the initial counter ever changes, this test silently exercises the "unknown page → `listPages()` refresh" branch instead of the "cache miss" scenario it intends to cover. Using `pages[0].pageId` (after calling `listPages()` explicitly, as done in the other tests) would remove the assumption.

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

@shadowfax92
Copy link
Copy Markdown
Contributor Author

Refinery rejected this merge request after the review gate. Greptile reported an unresolved P1 issue in packages/browseros-agent/apps/server/src/browser/browser.ts: the close_page retry path can leak the stale targetId session entry. Source issue bosmain-wcw has been reopened for rework; branch not merged.

@shadowfax92 Nikhil (shadowfax92) deleted the polecat/garnet/bosmain-wcw@moxnktij branch May 9, 2026 01:45
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.

close_page fails with "Unknown page" error even when using page IDs from list_pages

1 participant