Skip to content

v1.43.3.0 fix(browse): headed-mode idle timer + onDisconnect target wrong BrowserManager for embedders#1645

Merged
garrytan merged 5 commits into
mainfrom
garrytan/fix-for-gbrowser
May 22, 2026
Merged

v1.43.3.0 fix(browse): headed-mode idle timer + onDisconnect target wrong BrowserManager for embedders#1645
garrytan merged 5 commits into
mainfrom
garrytan/fix-for-gbrowser

Conversation

@garrytan
Copy link
Copy Markdown
Owner

Summary

Four module-level lifecycle handlers in browse/src/server.ts (idleCheckTick, parent watchdog, SIGTERM handler, onDisconnect default wire) read the module-level BrowserManager directly. Embedders (gbrowser's phoenix overlay) pass their own instance into buildFetchHandler({ browserManager: ... }), so the module-level instance never has launchHeaded() called on it. connectionMode stays 'launched' forever, headed-mode early-returns never fire, and after 30 minutes of HTTP idle the server self-terminates out from under the still-open overlay window.

Fix: introduce let activeBrowserManager: BrowserManager at module scope (symmetric with the existing let activeShutdown pattern). buildFetchHandler retargets it at cfg.browserManager and CHAINS cfg.browserManager.onDisconnect to activeShutdown — preserving any caller-installed handler instead of clobbering it.

3-commit bisect:

  1. fix(browse): 6 edit sites in browse/src/server.ts (Edits 1, 2, 3, 4, 5-verify, 6) + __testInternals__ export.
  2. test(browse): 5 new behavioral tests in browse/test/server-factory.test.ts + delete the broken string-grep test from browse/test/sidebar-ux.test.ts.
  3. chore: VERSION + CHANGELOG for v1.43.3.0.

Test Coverage

CODE PATHS (new/changed)
[+] browse/src/server.ts
  ├── idleCheckTick() [NEW EXTRACTION]
  │   ├── headed early-return            → [★★★ T1: REGRESSION]
  │   ├── tunnel early-return            → [★★★ T4]
  │   └── threshold exceeded + shutdown  → [★★★ T2: paired defensive]
  ├── parent watchdog (line ~658)         → covered by T5 static guard
  ├── SIGTERM handler (line ~1212)         → covered by T5 static guard
  ├── buildFetchHandler binding (Edit 4)
  │   └── activeBrowserManager retarget   → covered via T1 setup path
  └── cfgBrowserManager.onDisconnect chain → [★★★ T3 — async .rejects.toThrow]

COVERAGE: 5/5 paths tested (100%)
QUALITY: ★★★:5  |  GAPS: 0

Tests run: 33/33 pass in browse/test/server-factory.test.ts; 50/50 across browser-manager-unit + server-factory.

Pre-Landing Review

  • /plan-eng-review ran clean (0 arch / 0 quality / 0 perf issues; 2 test gaps surfaced via D1 + D2 and resolved with T3 + T4).
  • /codex plan review caught 6 substantive issues the static eng review missed: Bun memoizes dynamic imports, initRegistry token-reuse throws, shutdown() is async (sync .toThrow() cannot catch the rejection), cfg.browserManager.onDisconnect is a public field callers may set (would have been silently clobbered by a bare reassign), the original plan missed the SIGTERM site at line 1186, and tests belonged in server-factory.test.ts not sidebar-ux.test.ts. All 6 verified against actual code and incorporated into the shipped plan. The chain semantics in Edit 4, the SIGTERM site in Edit 6, the __testInternals__ surface (including resetShutdownState + setLastActivity), the test relocation, and the .rejects.toThrow assertion all trace to Codex findings.

Plan Completion

All plan items DONE: 6 production edits applied (browse/src/server.ts:601, 658, 705, 1212, 1387, 1394), 5 behavioral tests added (browse/test/server-factory.test.ts:388–530), broken string-grep test deleted (browse/test/sidebar-ux.test.ts:1596), VERSION + CHANGELOG updated. Sanity-grep confirms exactly 3 module-level activeBrowserManager.getConnectionMode() reads (idleCheckTick, watchdog, SIGTERM) and 0 stray module-level browserManager.getConnectionMode() reads outside handleCommandInternalImpl response payloads (which report mode informationally and are out of scope for lifecycle).

Test plan

  • bun test browse/test/server-factory.test.ts — 33/33 pass (5 new + 28 existing factory contract)
  • bun test browse/test/browser-manager-unit.test.ts — 17/17 pass
  • Static sanity-grep: exactly 3 module-level activeBrowserManager.getConnectionMode() sites (idleCheckTick, watchdog, SIGTERM); 0 stray module-level reads outside handleCommand response payloads
  • Manual gbrowser overlay session: headed Chromium runs >30 min HTTP idle without server self-terminating
  • Manual window-close on embedder-owned headed window fires cfgBrowserManager.onDisconnect chain (caller cb first, then gstack shutdown)

🤖 Generated with Claude Code

garrytan and others added 3 commits May 21, 2026 16:17
…indirection

Module-level idleCheckTick, parent watchdog, SIGTERM handler, and
buildFetchHandler's onDisconnect wire all read the module-level
BrowserManager directly. For embedders (gbrowser) that pass their
own instance into buildFetchHandler, the module-level instance
never has launchHeaded() called on it — connectionMode stays
'launched' forever, headed-mode early-returns never fire, and
after 30 min of HTTP idle the server self-terminates out from
under the overlay.

Adds `let activeBrowserManager: BrowserManager` at module scope
(symmetric with the existing `let activeShutdown` pattern).
buildFetchHandler retargets it at cfg.browserManager and CHAINS
cfg.browserManager.onDisconnect to activeShutdown, preserving any
caller-installed handler instead of clobbering it.

Six edit sites in browse/src/server.ts:
- Edit 1 (~705): declare activeBrowserManager
- Edit 2 (~596): extract idleCheckTick + __testInternals__ export
- Edit 3 (~658): parent watchdog reads activeBrowserManager
- Edit 4 (~1387): retarget + chain cfgBrowserManager.onDisconnect
- Edit 5 (verify): line 714 default stays in place
- Edit 6 (~1212): SIGTERM handler reads activeBrowserManager
…rally

Adds 5 behavioral tests to browse/test/server-factory.test.ts under
a new 'idle timer + onDisconnect dual-instance fix' describe block:

- T1 (CRITICAL — REGRESSION): headed embedder does not auto-shutdown
  at idle. Pins the bug this PR fixes.
- T2 (paired defensive): headless still auto-shuts down at idle.
  Catches a future refactor that breaks the inverse case.
- T3 (chain semantics): buildFetchHandler chains
  cfgBrowserManager.onDisconnect, preserving any caller-set handler.
  Uses .rejects.toThrow for the async shutdown path.
- T4 (tunnelActive): tunnel-active blocks idle-shutdown even in
  headless mode.
- T5 (static guard): exactly 3 module-level lifecycle sites use
  activeBrowserManager.getConnectionMode() — idleCheckTick, parent
  watchdog, SIGTERM. Catches refactor-introduced regressions before
  CI.

Reuses existing makeMinimalConfig() + __resetRegistry() patterns
from the factory contract tests. New makeMockBrowserManager() helper.
beforeEach also resets module state via setTunnelActive,
setLastActivity, and resetShutdownState from __testInternals__.

Also deletes the old 'idle check skips in headed mode' string-grep
test from browse/test/sidebar-ux.test.ts at line 1596. That test
would have passed even with the dual-instance bug present
(grepped for "=== 'headed'" + 'return' in the same window).
Behavioral coverage moved to server-factory.test.ts.

Verified: 33/33 tests pass in browse/test/server-factory.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

E2E Evals: ✅ PASS

6/6 tests passed | $1.11 total cost | 12 parallel runners

Suite Result Status Cost
e2e-browse 2/2 $0.13
e2e-deploy 2/2 $0.31
e2e-qa-workflow 1/1 $0.65
llm-judge 1/1 $0.02

12x ubicloud-standard-8 (Docker: pre-baked toolchain + deps) | wall clock ≈ slowest suite

@garrytan garrytan merged commit 61c9a20 into main May 22, 2026
21 checks passed
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.

1 participant