Skip to content

fix(browser): close tracked tabs after gateway restart#110797

Merged
steipete merged 3 commits into
openclaw:mainfrom
FMLS:fix/browser-tab-cleanup-restart
Jul 19, 2026
Merged

fix(browser): close tracked tabs after gateway restart#110797
steipete merged 3 commits into
openclaw:mainfrom
FMLS:fix/browser-tab-cleanup-restart

Conversation

@FMLS

@FMLS FMLS commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

After a Gateway restart, the in-memory browser-tab registry was empty. /new, session lifecycle cleanup, and idle/cap sweeps therefore could not close tabs that OpenClaw had opened before the restart. The bug is most visible with shared existing-session browsers, where cleanup must recover OpenClaw ownership without ever adopting or closing user-created tabs.

Why the Diff Is Large

This cannot safely be fixed by serializing the old Map. A restart-safe cleanup path needs to prove both target ownership and browser-instance continuity before it closes anything. The production changes therefore establish a durable ownership boundary, while much of the diff is adversarial regression coverage for restart, alias, race, and mismatch cases.

The reviewed version keeps the design deliberately narrow:

  • Only host-local tabs with a restart-stable native CDP target and credential-free profile/browser fingerprints enter the browser plugin SQLite store.
  • autoConnect, browser-bridge, and otherwise non-durable targets remain process-local.
  • Existing-session Chrome MCP opens use a marker target to establish ownership before navigation. Explicit-CDP empty-browser opens capture the first native target without adopting pre-existing tabs.
  • Cleanup uses compare-and-swap claims and persisted pending intent, so crashes and retries cannot race a touch/untrack operation or silently lose lifecycle cleanup.
  • Lifecycle cleanup remains available for every durable row. Cold opaque Chrome MCP handles are excluded from post-restart idle/cap sweeps because their interaction handles cannot be proven current.
  • Durable aliases are normalized, sorted deterministically, and rehydrated at plugin startup. Exact durable and volatile identities win only when unambiguous; ambiguous aliases fail closed.

User Impact

Tabs created by OpenClaw through managed, remote-CDP, and explicit-cdpUrl existing-session profiles can now be cleaned after a Gateway restart. User-created tabs remain untouched. Tabs created before this version, and profiles without a stable browser identity, still require manual cleanup after restart.

Release note: Fix browser-tab leaks across Gateway restarts while preserving shared-browser user tabs.

AI-assisted: yes. The implementation was deeply reviewed, refactored, and tested by a maintainer. A redacted agent transcript was intentionally not included at the requester's direction.

Evidence

Head: 24226ec9fee3da36f4d5157da348086f49740875

  • All 18 changed Browser test files pass: 434 tests.
  • pnpm build and pnpm check pass on the reviewed tree, including TypeScript, lint, formatting, import-cycle, database-first, SDK-surface, and policy guards. Testbox proof: Actions run 29673821849.
  • Fresh autoreview completed with no accepted or actionable findings after fixes for alias drift, empty existing-session browsers, deterministic ordering, compensation errors, cross-kind ambiguity, and profile-less bridge tracking.
  • git diff --check passes.
  • Hosted CI's related unused-type finding was fixed by keeping the selector's return union private; its focused tests, fresh autoreview, and the exact Knip unused-export scan pass. Proof: Actions run 29674427433.
  • A full 290-shard Testbox run kept every Browser shard green. Six unrelated baseline shards failed outside the PR; isolated reruns passed for UI, PDF, and tooling, while QA catalog, Zalouser setup text, and Telegram lifecycle assertions reproduced without Browser changes.

Real behavior proof

A real headless Chrome stayed alive while two separate Node/OpenClaw processes shared the plugin SQLite state, simulating a Gateway restart. The harness created one user tab, one tracked OpenClaw tab, and one tracked tab that disappeared before cleanup. Process B reopened the registry and ran the production lifecycle cleanup path.

LIVE_RESTART_EVIDENCE {"durableRowsBeforeRestart":2,"cleanupClosedCount":1,"durableRowsAfterCleanup":0,"userSurvived":true,"ownedClosed":true,"missingRetired":true}

Observed result: the OpenClaw tab closed, the already-missing row retired, the durable store emptied, and the user tab survived.

What was not live-tested: durable restart cleanup for existing-session --autoConnect, because that mode intentionally has no restart-stable native ownership; cross-platform browser behavior. Those boundaries are covered by focused regression tests.

Made with Cursor

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime size: XL labels Jul 18, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jul 18, 2026
@clawsweeper

clawsweeper Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed July 19, 2026, 1:15 AM ET / 05:15 UTC.

Summary
The branch persists ownership-verified browser tabs in plugin SQLite and rehydrates them after Gateway restart so lifecycle cleanup can close only OpenClaw-created tabs.

PR surface: Source +1790, Tests +2841, Docs +48. Total +4679 across 39 files.

Reproducibility: yes. from source, with high confidence: a manually closed durable CDP target can return a new protocol error string that does not match the existing ignorable-close classification, and identity-unavailable durable records have no demonstrated convergence path.

Review metrics: 1 noteworthy metric.

  • Durable tab-store capacity: 5,000 entries, reject-new overflow. Unretired records can make future browser opens fail once the fixed namespace limit is reached.

Stored data model
Persistent data-model change detected: serialized state: extensions/browser/src/browser/cdp.close-tracked.test.ts, serialized state: extensions/browser/src/browser/cdp.helpers.test.ts, serialized state: extensions/browser/src/browser/cdp.ownership-fingerprint.test.ts, serialized state: extensions/browser/src/browser/cdp.test.ts, serialized state: extensions/browser/src/browser/client.ts, serialized state: extensions/browser/src/browser/session-tab-cleanup.test.ts, and 17 more. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🦐 gold shrimp
Proof: 🦞 diamond lobster
Patch quality: 🦐 gold shrimp
Result: needs maintainer review before merge.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P2] Handle the actual CDP missing-target response as retirement.
  • [P2] Add and document a bounded policy for identity-unavailable durable records.
  • [P2] Show focused after-fix evidence that repeated unavailable-browser cleanup cannot exhaust the 5,000-row store.

Risk before merge

  • [P1] A CDP target manually closed by the user can return "No target with given id found"; if that is not treated as an absent target, the durable record is retried indefinitely rather than retired.
  • [P1] Durable records that cannot re-establish browser identity have no demonstrated bounded retirement path. At the 5,000-row reject-new limit, a later durable browser open can be compensated by closing the newly opened tab and returning an error.
  • [P1] The PR deliberately changes post-restart cleanup behavior for shared existing-session browsers; maintainers need to accept the retention-versus-safety policy for browser instances that never return.

Maintainer options:

  1. Bound irrecoverable durable records (recommended)
    Treat confirmed missing CDP targets as retired and add a conservative, tested retirement policy for rows whose browser identity remains unavailable.
  2. Accept indefinite retention explicitly
    Land the current safety-first retention semantics only if maintainers accept that stale records may consume the finite durable-tab store.
  3. Pause the restart-persistence change
    Defer durable persistence if a safe retention policy cannot be agreed without weakening shared-browser ownership guarantees.
Copy recommended automerge instruction
@clawsweeper automerge

Special instructions:
Add a bounded durable-record retirement policy and regression coverage for missing CDP targets and permanently unavailable browser identity; preserve fail-closed ownership behavior for shared user tabs.

Next step before merge

  • [P2] A mechanical missing-target repair exists, but the durable-record retention boundary requires maintainer product and safety direction before an automated repair lane should proceed.

Maintainer decision needed

  • Question: Should durable browser-tab records with permanently unavailable browser identity be retired after a bounded policy window, rather than retained indefinitely until a future lifecycle cleanup can prove ownership?
  • Rationale: This is a product-safety choice between preserving possible future cleanup for a temporarily unavailable shared browser and guaranteeing that stale rows cannot block all later durable opens at the store limit.
  • Likely owner: steipete — They authored the current ownership and alias refinement and are assigned to the PR.
  • Options:
    • Add bounded stale-record retirement (recommended): Retire records after a conservative age or repeated identity-unavailable threshold, while immediately retiring verified missing targets.
    • Retain indefinitely: Keep all identity-unavailable records for a possible future browser recovery and explicitly accept capacity and later-open failure risk.

Security
Cleared: The diff adds no dependency, workflow, secret-handling, or supply-chain execution surface; its CDP ownership checks are intended to preserve the shared-browser security boundary.

Review findings

  • [P1] Retire CDP targets reported absent by the browser — extensions/browser/src/browser/session-tab-registry.ts:1
  • [P1] Bound durable rows that cannot recover browser identity — extensions/browser/src/browser/session-tab-store.ts:31-34
Review details

Best possible solution:

Keep ownership verification fail-closed, but make unrecoverable or confirmed-missing durable rows converge to removal under a narrowly specified, tested retention policy so stale records cannot exhaust the plugin store.

Do we have a high-confidence way to reproduce the issue?

Yes from source, with high confidence: a manually closed durable CDP target can return a new protocol error string that does not match the existing ignorable-close classification, and identity-unavailable durable records have no demonstrated convergence path.

Is this the best way to solve the issue?

Unclear. The ownership-verification approach is the right safety boundary, but indefinite retention is not the best complete fix because the fixed-capacity store can become unavailable to future opens.

Full review comments:

  • [P1] Retire CDP targets reported absent by the browser — extensions/browser/src/browser/session-tab-registry.ts:1
    Treat the CDP protocol response "No target with given id found" as an absent tracked target. Otherwise a user-closed owned tab remains durable, is reclaimed on every sweep, and never converges; this concern was already visible in the prior review cycle and remains unfixed at this head.
    Confidence: 0.9
  • [P1] Bound durable rows that cannot recover browser identity — extensions/browser/src/browser/session-tab-store.ts:31-34
    Add a conservative retirement rule for durable rows whose browser identity remains unavailable. With maxEntries: 5_000 and reject-new, indefinitely retained rows can eventually make every durable browser open compensate by closing the new tab and fail.
    Confidence: 0.9

Overall correctness: patch is incorrect
Overall confidence: 0.9

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 2f7da3057c05.

Label changes

Label justifications:

  • P2: The browser lifecycle regression has bounded blast radius but can prevent durable browser-tab opens after stale state accumulates.
  • merge-risk: 🚨 compatibility: The PR changes cleanup and retention semantics for existing-session and remote-CDP browser profiles across Gateway restart.
  • merge-risk: 🚨 availability: An unbounded durable registry with reject-new overflow can eventually cause browser-open operations to fail.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (live_output): The PR body provides after-fix live output from a two-process headless-Chrome restart simulation that directly shows the owned tab closes, the missing record retires, and the user tab survives.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides after-fix live output from a two-process headless-Chrome restart simulation that directly shows the owned tab closes, the missing record retires, and the user tab survives.
Evidence reviewed

PR surface:

Source +1790, Tests +2841, Docs +48. Total +4679 across 39 files.

View PR surface stats
Area Files Added Removed Net
Source 18 2168 378 +1790
Tests 18 2961 120 +2841
Docs 3 52 4 +48
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 39 5181 502 +4679

What I checked:

  • Submitted real behavior proof: The PR body reports a two-process, shared-plugin-SQLite run against a live headless Chrome: one owned tab closed, a missing owned tab retired, and a user tab survived. (24226ec9fee3)
  • Prior review blocker remains relevant: The previous completed review at 0e51404 identified unbounded durable rows whose browser identity cannot be recovered as a P1 concern; the current follow-up commit only privatizes the untrack selector return union and does not establish a retention bound. (extensions/browser/src/browser/session-tab-registry.ts, 24226ec9fee3)
  • Durable store capacity: The added durable store uses a 5,000-entry reject-new namespace, while restart cleanup intentionally retains records when browser identity or target closure cannot be safely established; that combination needs a bounded retirement policy. (extensions/browser/src/browser/session-tab-store.ts:31, 24226ec9fee3)
  • Current review discussion: A concrete review comment identifies the new CDP "No target with given id found" error as not matching the ignorable-close classification, which would keep manually closed tabs retrying instead of retiring. (extensions/browser/src/browser/session-tab-registry.ts, 24226ec9fee3)
  • Feature-history routing: The follow-up browser cleanup commits on this PR were authored by steipete, including the alias-disambiguation refinement at 0e51404. (extensions/browser/src/browser/session-tab-registry.ts, 0e51404185e8)

Likely related people:

  • steipete: Authored the current branch's restart-alias refinement and is assigned to this PR, making them the strongest available routing candidate for the durable cleanup policy. (role: recent area contributor; confidence: high; commits: 0e51404185e8, 24226ec9fee3; files: extensions/browser/src/browser/session-tab-registry.ts, extensions/browser/src/browser/session-tab-store.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (2 earlier review cycles)
  • reviewed 2026-07-18T16:14:17.250Z sha f4a9f39 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-19T05:04:34.487Z sha 0e51404 :: found issues before merge. :: [P1] Retire CDP targets reported absent by the browser | [P1] Bound durable rows that cannot recover browser identity

@Yigtwxx Yigtwxx left a comment

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.

Traced the cleanup convergence path. The design is careful — CAS claim tokens, restart-stable browser fingerprints from the /devtools/browser/<uuid> websocket path, no adoption of user tabs — and the restart tests are genuinely end-to-end rather than unit-level shape (reopening both the SQLite DB and the module, proving aliases are not recovered, covering cross-instance claim dedup and the 5001st-record rejection). That's above the usual bar.

My concern is that orphaned rows appear to have no retirement path, which turns a bounded reject-new store into a one-way ratchet. Two specifics inline.

One smaller thing, worth its own commit rather than a change here: cdp.helpers.ts:533 adds AbortSignal.any([ctrl.signal, init.signal]) inside fetchCdpChecked, which changes abort semantics for every CDP HTTP fetch in the extension, not just the new ownership probes. It looks correct and the new callers need it, but it's the one hunk in this diff whose blast radius is wider than tracked tabs. Otherwise the XL size reads honest to me — the bulk is tests and the source files all sit on the tracked-tab path.

return await runCleanupWithLifecyclePriority({ ...params, candidate });
}

export function isIgnorableTabCloseError(error: unknown): boolean {

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.

This list came over from main's isIgnorableCloseError, where the only producer was BrowserTabNotFoundError (errors.ts:73, message starts "tab not found: "). This PR adds a second close route — session-tab-registry.tscloseCdpTargetByIdTarget.closeTarget — and the CDP sender rejects with the protocol message verbatim (p.reject(new Error(parsed.error.message))).

For an unknown target that string is No target with given id found, which this repo emits itself in extension-relay/relay-bridge.ts:690,745,842,858. None of the four substrings match it.

So the most common real case — the user closes an OpenClaw tab by hand — leaves a durable row forever: every sweep re-claims it, probes /json/version, calls Target.closeTarget, gets a non-ignorable error, warns, and keeps the row. The tests don't catch it because the fixtures fabricate the string: session-tab-registry.sqlite.test.ts:516 and :966 both throw new Error("target not found"), which is in this list, so "converges on not-found" passes on a message the new path never actually produces.

Adding no target with given id would cover both producers.


/** Opens and publishes Browser's canonical durable tab store during plugin registration. */
export function initializeBrowserSessionTabStore(runtime: BrowserSessionTabStoreRuntime): void {
const sessionTabs = runtime.state.openSyncKeyedStore<unknown>({

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.

Related, and the reason the error-string gap above isn't just log noise: this namespace opens with maxEntries: 5_000, overflowPolicy: "reject-new" and no defaultTtlMs, though OpenKeyedStoreOptions supports one (src/plugin-state/plugin-state-store.types.ts:52). Combined with the non-durable / browser-identity-lookup-failed branch in session-tab-registry.ts — which returns without deleting whenever /json/version fails — a browser that never comes back at the same cdpUrl leaves its rows immortal. The only sweep is config-gated off by default (session-tab-cleanup.ts:41).

At the cap the failure isn't graceful: reject-new throws PLUGIN_STATE_LIMIT_EXCEEDED (plugin-state-store.sqlite.ts:497), which propagates out of trackSessionBrowserTab into the compensation path in browser-tool-session-tabs.ts:80-97 — that closes the tab the user just opened and rethrows. So once 5000 dead rows accumulate, every browser open on a durable profile opens a tab, closes it again, and errors, with no self-healing path.

A defaultTtlMs on the namespace, or an age-based retire in performDurableCleanup when identity lookup has been failing since trackedAt, would bound it.

@steipete steipete self-assigned this Jul 18, 2026
@clawsweeper clawsweeper Bot added status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. and removed status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 19, 2026
@steipete
steipete merged commit 4074e0c into openclaw:main Jul 19, 2026
118 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 19, 2026
* fix(browser): preserve tab cleanup across restarts

* fix(browser): disambiguate restart tab aliases

* fix(browser): keep untrack selection type private

---------

Co-authored-by: Peter Steinberger <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation gateway Gateway runtime merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: XL status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants