This document specifies the browser lifecycle, state transitions, and self-healing protocols.
- Generation ID: The engine maintains a
browser_generationcounter. - Trigger: Every successful new Browser process launch increments this counter. A failed launch leaves it unchanged.
- Initialization State: A newly created session starts with
last_browser_generation = Noneand is not associated with any browser generation until its first context initialization. - Propagation: Sessions track
last_browser_generation. A generation rollover is detected only after the session has been initialized (last_browser_generation is not None) and the tracked generation differs from the engine generation. On rollover detection, the session MUST purge all registry tabs immediately. - Invalidation Invariant: A generation rollover permanently invalidates all existing
PersistentTableases. Old tabs/pages from previous generations are stale and MUST NEVER be reused.
- Provider/Adapter Role: Providers and their adapters may only identify and escalate recovery requests.
- Session Authority:
ProviderSessionowns authoritative session-scoped recovery logic (context recreation, tab invalidation, and recovery coordination). - Engine Authority:
BrowserEngineis the authoritative lifecycle orchestrator and terminal shutdown authority. It manages process-level state transitions but does NOT act as a recreation mechanism after shutdown has been initiated.
- Convergence: Concurrent
ensure_healthy()calls must converge into a single recovery path. - Serialization: Redundant context recreation races are suppressed via
ProviderSession.init_lock. Only the first caller performs the setup; subsequent concurrent callers wait and verify the new state.
Unhealthy-browser replacement runs under BrowserEngine.management_lock:
- Clean ProviderSessions bound to old lifecycle/resources and wait for active cleanup.
- Close or skip old Browser through the runtime (
runtime.close_browser()). - Stop the driver through the runtime (
runtime.stop()). - Start the driver and launch a new Browser through the runtime (
runtime.start(),runtime.launch_browser()). - Publish the successful launch by incrementing
browser_generation. - Set up ProviderSession contexts.
ProviderSessions observe/store last_browser_generation; they do not choose generations. A close callback from generation N cannot terminate generation N+1.
- Behavior: Manual window closure may not immediately trigger a
disconnectedevent if Chromium remains active in the background. - Canonical Source: The status of the
keepalive_pageis the authoritative source for window liveness. - Hardening:
ProviderSession.is_aliveMUST checkkeepalive_page.is_closed().
Application shutdown follows:
ApplicationServer.handle_exit()marksshutdown_requested(source="application").- Uvicorn drains active connections while new page/session admission is rejected.
- FastAPI lifespan calls
BrowserEngine.close(source="application"). - Existing requests receive up to 15 seconds to finish; remaining requests receive request-owned abort callbacks.
- ProviderSession lifecycle tasks are cancelled/drained and ProviderSession resources close.
- Browser closes or is skipped, then the driver stops — both executed through the
BrowserRuntimeboundary (runtime.close_browser(),runtime.stop()).
ProviderSession resources always close before Browser, and Browser before the driver stops. BrowserEngine owns this ordering and all shutdown/recovery decisions; the runtime only executes the mechanics. Runtime terminal cleanup uses save_state=False; bootstrap/manual auth cleanup may use save_state=True.
Shutdown Invariant: Once shutdown intent exists, no new recovery or admission may begin. Workers recheck shutdown state after acquiring init_lock and before cleanup. Existing recovery may be cancelled and awaited during terminal cleanup.
- Intentional close: ProviderSession cleanup owns the context close and marks it so the callback cannot trigger terminal shutdown.
- Stale callback: A callback for an old context or generation is ignored.
- Unexpected current-context close: Without shutdown intent or an intentional marker, this triggers browser-disconnect terminal behavior.
- Application shutdown: Current-context closure is expected activity and does not restart crash recovery or shutdown orchestration. Active requests still receive immediate terminal browser-disconnect signals when their browser resource disappears.
Browser/page/context loss signals the request terminal event immediately. Before SSE headers are sent, BrowserRequestExecutor maps terminal BrowserDisconnectedError through the existing HTTP 502 contract. After headers are sent, the stream boundary consumes the terminal state without waiting for queue, chunk, total, or request timeout. If a raw Playwright close error arrives later for the same closure, the recorded terminal browser-disconnect state takes precedence; raw Playwright errors without that state retain existing behavior.
Request cleanup owns lease and semaphore release. BrowserEngine never decrements lease counters directly.
During application shutdown, an active stream may already have sent headers when browser or shutdown termination occurs. The stream boundary consumes that terminal state, ends without [DONE], and still completes request cleanup and lease release.
The system classifies failures into two categories with distinct response protocols.
- Scope: Transient transport or UI errors where the browser process remains authoritative and alive.
- Boundary: Recoverable flows may recreate session/context state ONLY while the engine remains healthy.
- Protocol: Call
ensure_healthy()-> context refresh or tab purge. Self-healing MUST NOT recreate the browser process. - Examples:
- Request timeout.
- Navigation failure.
- DOM element missing.
- Authentication expiry.
- Scope: Fatal loss of the user-visible interface or global process. The recovery boundary has been crossed and the system cannot restore structural integrity.
- Boundary: No context or browser recreation is permitted after terminal shutdown begins. Shutdown is irreversible (Terminal Shutdown Immutability).
- Protocol: Call
BrowserEngine.close(). No recreation allowed. - Examples:
- Manual window closure.
- Explicit browser disconnect.
- System-level shutdown signal (
SIGINT).
An application shutdown signal establishes shutdown intent before connection drain; it does not start a second crash-classification path.
If recovery itself fails, the failure MUST be escalated:
- Session Loss: Failed context recreation escalates to session invalidation or terminal engine shutdown.
- No Loops: Repeated recovery loops for the same fatal condition are forbidden. If
ensure_healthy()cannot restore invariants, the system must fail terminal.
The ensure_healthy() method acts as both a system validator and a recovery escalation entrypoint.
- Fail Fast: It MUST raise
RuntimeErrorimmediately if called during terminal shutdown. - Strict Invariants: It MUST NOT silently succeed if liveness invariants (e.g., connected browser, open keepalive page) are violated.
- Authority Escalation: It may escalate recovery to authoritative lifecycle managers (
ProviderSessionfor context recreation orBrowserEnginefor browser lifecycle handling). - Process Protection: Provider code MUST NEVER directly invoke browser-process recreation; it must escalate through the engine's authoritative path.
- No Resurrection Invariant:
ensure_healthy()may coordinate recovery only within the current healthy engine lifecycle. It MUST NEVER attempt to resurrect or re-initialize a terminated engine generation.
- State Immutability: Once
is_shutting_downisTrue, it can NEVER return toFalse. - Graceful Draining: The shutdown sequence allows a 15-second grace period for active requests to finish before killing resources.
- Safe Teardown: Task scheduling in disconnect callbacks MUST use the defensive
try/except RuntimeErrorpattern to avoid loop-teardown races.
AI Agents working on lifecycle or recovery logic must adhere to these strict constraints:
- No Resurrection: Never attempt to re-initialize or "self-heal" a browser or context after
is_shutting_downis set. - Strict Generation Invalidation: Always invalidate all registry tabs on generation mismatch; never attempt "best-effort" reuse of stale pages.
- Respect Authority Boundaries: Never bypass the
BrowserEngineorProviderSessionfor process/context creation. - Converge Recoveries: Ensure all recovery logic is guarded by locks and converges into a single path; never allow parallel context recreation.
- Fail terminal: If
ensure_healthycannot restore structural invariants, escalate to terminal shutdown rather than entering a recovery loop.
Conversation recovery is provider/backend-dependent. Browser lifecycle recovery remains owned by BrowserEngine and ProviderSession; conversation continuity for a chat request is owned by the selected provider/backend.
Gemini lifecycle generation IDs are chosen only by init_gemini_client().
- The initial generation ID is selected by lifecycle initialization.
- Replacement generation IDs are selected by lifecycle initialization.
_register_generation(client, generation)registers an explicit ID; it never chooses one.- Request, session, registry, restore, and streaming paths MUST NOT allocate, increment, or repair generation IDs.
Candidate replacement follows this commit order:
- Initialize candidate client.
- Select and register candidate generation.
- Update the session registry with the explicit generation.
- Publish global client and current generation.
- Retire the old generation.
If candidate initialization or registry commit fails, the candidate record is removed, the candidate is closed, and the old lifecycle state remains current.
Current client, current generation, generation record, and reverse client mapping MUST remain coherent. Malformed state raises a lifecycle invariant RuntimeError; no implicit generation repair exists.
Lifecycle error distinctions are:
GeminiClientNotInitializedError: initialization is unavailable.GeminiGenerationUnavailableError: an expected stale or retired generation race.RuntimeError: malformed lifecycle state or shutdown rejection of new lease acquisition.
The Gemini WebAPI backend uses SessionRegistry as an in-memory container protected by asyncio synchronization primitives. It maps cryptographically secure opaque tokens (conversation_id) to SessionManager instances and coordinates SQLite-backed snapshot recovery through the conversation repository.
The registry constructor requires a registered (client, generation) pair. update_client() and reopen() also require a registered generation. The registry MUST validate and propagate the supplied generation, but MUST NOT allocate, increment, or repair generation IDs. Every SessionManager receives the registry's exact client_generation; its separate session_generation tracks the generation of its current ChatSession.
- Creation: When a completions request is submitted without a
conversation_id, the system generates a secure opaque token. - Registry Mapping: The token maps to a dedicated
SessionManagerin memory, which lazily instantiates aChatSession. - API Return: The
conversation_idtoken is returned as a top-level key in the standard OpenAI-compatible completions response body. - Client Preservation: Subsequent requests by the same client present this token in the request body to continue the thread.
If the registry finds an active SessionManager for the given token, it reuses the session if and only if the requested model and gem match. The manager acts as a long-lived state container, bypassing the initialization overhead of the client and retaining history natively.
- Bootstrapping: On the first request of a new session, the provider concatenates the entire conversation history from
messagesto bootstrap the thread on Google's backend. - Persistent Recovery: The system utilizes a SQLite-backed repository to persist session snapshots. If a session is lost from memory (e.g., pruned due to TTL or after a server restart), the
SessionRegistrycan automatically restore the session state from the database using the suppliedconversation_id. - Durable Continuity: For Gemini WebAPI, long-running threads can remain continuous across process restarts or container recycling, provided the
conversation_idis preserved by the client and the corresponding SQLite snapshot exists. - Missing Snapshot Behavior: If an existing
conversation_idis not present in memory and no valid snapshot exists, the request fails explicitly. The current implementation does not silently rebuild an existing WebAPI conversation from incoming message history. - Deletion: Gemini WebAPI deletion reserves the local
conversation_idinSessionRegistrybefore remote deletion begins. This tombstone blocks concurrent reuse or SQLite restoration while the remote Gemini chat is being deleted and local state is being removed.
Each restore captures client generation N and acquires a lease for that exact generation before snapshot recovery, validation, deserialization, and ChatSession construction. A replacement may retire N, but cannot close it while the restore lease remains active.
Before publication, the restore rechecks registry shutdown state, the deletion tombstone, existing session, and current client/generation. A changed generation discards the candidate, releases lease N, and retries with the current generation. A concurrently created session may win; restore never overwrites it. Failed or cancelled restores clear in-flight coordination state. Restore preserves its existing pruning and capacity semantics.
Registry shutdown closes restore admission, cancels and awaits pending restore producers, and releases their leases before Gemini client shutdown. Successful lifecycle initialization explicitly reopens the existing registry without clearing in-memory sessions; stale chat sessions rebuild against the current explicit generation on next use.
Each committed Gemini WebAPI client has a generation record. Request paths lease the generation for the duration of direct client use. Replacement marks the old generation retired; the old client closes immediately only when no leases remain, otherwise its final lease release closes it. Shutdown rejects new leases, closes zero-lease records, and preserves active-leased records until release.
Shutdown never creates generation records. It retires all registered records, closes zero-lease records, and leaves active-leased generations alive until their final release. If the current client is malformed and is not represented by a record, shutdown defensively closes that client directly; it never creates a synthetic generation for cleanup.
The Gemini Playwright backend does not use SQLite conversation snapshots. It uses two continuity mechanisms:
- Live Tab Reuse:
ProviderSession.conversation_registrymaps Gemini provider conversation IDs to in-memoryPersistentTabinstances. If a matching tab is live and valid, the request reuses that tab. - Provider-Side URL Recovery: If no live tab is available but the request supplies a
conversation_id, the backend navigates a browser page tohttps://gemini.google.com/app/{conversation_id}and relies on Gemini's provider-side conversation history.
For new Playwright conversations, the provider-side conversation_id is discovered from the Gemini URL after submission and the temporary page is promoted to a PersistentTab in ProviderSession.conversation_registry.
reused_conversation=true in Playwright indicates live in-memory PersistentTab reuse. After a process restart or context recreation, Playwright may still resume the provider-side Gemini thread by URL navigation while reporting reused_conversation=false because no in-memory tab was reused.
If a stateful request switches models (e.g. from gemini-3-flash to gemini-3-pro) or changes the gem ID:
- Gemini WebAPI:
SessionManager._ensure_session()detects the mismatch, replaces theChatSession, and uses full prompt concatenation on the current request to bootstrap the new model/gem context. - Gemini Playwright: model/gem behavior is handled by the Playwright adapter and provider UI flow, not by SQLite snapshots.
To protect server memory, the SessionRegistry passively prunes idle sessions when the cache capacity exceeds MAX_SESSIONS = 500.
- Prunability Invariant: A session can ONLY be pruned if it is unlocked (
manager.lockis not locked) and hasactive_streams == 0(no active progressive stream tasks). - TTL Policy: Stale sessions are evicted if their idle time exceeds
IDLE_TIMEOUT = 3600(60 minutes).