fix(pi): close facade browser resources
why
This is PR 3 of the facade-close stack. It depends on #2793, which in turn depends on #2788.
Pi is a separate native facade host with its own launch and shutdown lifecycle. Once the shared facade propagates browser.close(), Pi must provide a real host callback; otherwise the compatibility boundary correctly reports that cleanup is unsupported. Keeping this adapter in a separate PR makes Pi's shutdown and retry policy reviewable without mixing it into the Eve implementation.
what changed
- Pi supplies the shared facade lifecycle callback and detaches the expected tools before cleanup.
- Cleanup is serialized so shutdown, explicit close, and replacement cannot race ownership.
- Model-requested close surfaces Stagehand/browser failures;
session_shutdownremains best-effort. - Browserbase release is captured immediately after launch, before
Stagehand.create(), so an initialization failure cannot lose the remote session. - A rejected memoized browser close retains a fresh SDK release operation for retry before the next launch and during final shutdown.
- Explicit cleanup failures are surfaced as a dedicated
StagehandFacadeCleanupErrorwith a fixed sanitized message and no raw SDK/CDP causes or aggregate members. - Focused tests cover explicit-close failure/retry, best-effort shutdown, and failed initialization followed by retained-release retry.
behavior before and after
| Pi lifecycle | Before | After |
|---|---|---|
await browser.close() | No host lifecycle callback. | Awaits Pi-owned cleanup. |
| Explicit cleanup failure | Can be swallowed while the tool succeeds. | Surfaced to the caller. |
| Shutdown cleanup | Best-effort but can lose failed ownership. | Best-effort with retained release targets. |
| Init + browser-close failure | Remote release path can be lost. | Release is attempted immediately and retained on failure. |
| Next launch | Can race or bypass failed cleanup. | Waits for serialized cleanup/retry first. |
test plan
- Pi tests: 6/6
- Pi typecheck
- Focused oxfmt/oxlint
- Core tests on PR #2788: 37/37
- Eve tests on PR #2793: 9/9
This PR does not claim a separate live Pi-agent E2E; the shared remote lifecycle behavior is covered by the Eve live A/B in #2793, while this PR focuses on Pi-specific ownership, error, retry, and shutdown paths.
No changeset is included because the Pi integration is a private example.
Summary by cubic
Pi now closes facade-launched browser resources and serializes cleanup. Previously model-requested browser.close() had no host callback and failures could be swallowed; now explicit close surfaces a sanitized error while shutdown remains best-effort.
- Routes
browser.close()through a lifecycle callback inStagehandFacadeTools;session_shutdownuses the same path. - Serializes teardown via a shared cleanup promise and retries queued Browserbase releases before any re-launch; avoids closing replaced resources.
- For Browserbase, captures a release operation at launch; on Stagehand init failure, attempts browser close and session release, queues failed releases for retry, and throws a combined init/cleanup error.
- Explicit-close failures throw
StagehandFacadeCleanupError(sanitized); shutdown suppresses errors but retains release targets for retry. - Adds focused tests in
packages/integrations/pi/tests/lifecycle.test.ts.