Skip to content

feat(agents): add the named browser session core - #2302

Merged
ben-reitz merged 9 commits into
mainfrom
feat/browser-session-core
Sep 24, 2026
Merged

ben-reitz merged 9 commits into
mainfrom
feat/browser-session-core

Conversation

@ben-reitz

@ben-reitz ben-reitz commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

What this is

A host-side session engine. You name a browser and get a live Browser Run session for it.

  resolve("checkout")
    none yet            → create                 restarted: false
    alive               → return it              restarted: false
    dead/closed/swept   → create a replacement   restarted: true

connect(name) = resolve + open the CDP socket.

Why

Today the model manages browser lifetime inside browser_execute (cdp.startSession(), sessionInfo(), carrying raw ids), and evals showed that's where it fails most. This moves lifetime host-side. Nothing model-facing changes here: browser_execute is untouched, and wiring it to named sessions comes later in the stack.

Behavior

  • No platform ids for the model. Hosts see sessionId; models never will.
  • Restarts are loud. A replaced browser reports restarted: true instead of silently appearing blank.
  • Settings survive replacement. keepAliveMs (default: 600s platform max), recording, and guardrails are stored and reapplied on every create.
  • Durable cleanup. close() tombstones durably, then deletes best-effort. sweep() closes browsers idle longer than sweepIdleMs (10 min) and prunes old tombstones. Alarm scheduling is in feat(agents): add the browser-sessions Lifecycle capability #2293.
  • Busy browsers aren't swept. Each CDP send refreshes the idle clock, throttled to at most half the sweep window.
  • Races report restarts. Dead sessions are tombstoned during replacement, so concurrent resolvers both see restarted: true.
  • One-shot is still an option. openOneShotBrowserSession() is create-use-delete with no store, and it's the only path for Kitesurf right now.

Storage uses browser:session:<name>, separate from the connector's existing cdp:exec:/cdp:reuse: keys.

Public API (agents/browser)

 createBrowserSession(browser, { keepAliveMs, includeTargets, recording,
+  guardrails })                                  // acquire now always sends JSON ({} if none)
-connectBrowserSession(browser, id, timeoutMs)    // still works, @deprecated
+connectBrowserSession(browser, id, { timeoutMs, onClose, onActivity })
-new CdpSession(ws, timeoutMs, dispose, sessionId)
+new CdpSession(ws, { timeoutMs, onClose, sessionId, onActivity })  // breaking for direct construction
 connectBrowser(browser, options)                 // options: engine-discriminated union;
                                                  // kitesurf rejects Chromium-only options at compile time

Type exports and the changeset are in #2294.

Reviewing

session-core.ts reads top to bottom: resolve → connect → close → sweep → one-shot. 23 tests in browser-session-core.test.ts; compile-time option checks in tests-d/browser-engine-options.test-d.ts.

Extra: where this is heading

Where this is heading

Before

This is today, with dynamic mode enabled. The model owns both the Browser Run platform session AND the CDP session:

  // ── browser_execute #1: log in, keep the browser ──────────────
  // target ceremony (every execution)
  const { targetInfos } = await cdp.send("Target.getTargets");
  const page = targetInfos.find((t) => t.type === "page");
  const { sessionId } = await cdp.attachToTarget({ targetId: page.targetId });

  await cdp.send("Page.navigate", { url: "https://shop.example/login" }, { sessionId });
  // ...fill form, submit, all with { sessionId }...

  // if the model forgets to do this next bit, then the logged-in browser is
  // deleted when this execution ends
  await cdp.startSession();
  // ── browser_execute #2: later turn ─────────────────────────────
  await cdp.sessionInfo();                       // is my browser still there?
  const { targetInfos } = await cdp.send("Target.getTargets");   // re-discover
  const page = targetInfos.find((t) => t.type === "page");
  const { sessionId } = await cdp.attachToTarget({ targetId: page.targetId }); // re-attach
  await cdp.send("Page.navigate", { url: "https://shop.example/checkout" }, { sessionId });

  // ...and eventually, if it remembers:
  await cdp.closeSession();

After

Target state: both Browser Run and CDP lifecycle management gone from the model's code:

  // ── browser_execute #1: log in ─────────────────────────────────
  await cdp.send("Page.navigate", { url: "https://shop.example/login" }, { sessionId: "active" });
  // ...fill form, submit, all with { sessionId: "active" }...
  // nothing else — persistence isn't the model's decision
  // ── browser_execute #2: later turn, same browser, still logged in ──
  await cdp.send("Page.navigate", { url: "https://shop.example/checkout" }, { sessionId: "active" });
  // if the browser had died meanwhile, the result envelope says "sessionRestarted: true"
  //  and the model knows to log in again

@changeset-bot

changeset-bot Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ff32623

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@ben-reitz
ben-reitz added this pull request to stack #2303 September 18, 2026 10:04
@ben-reitz
ben-reitz marked this pull request as ready for review September 18, 2026 10:05
@agent-think

agent-think Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

🟡 agents import sizes

Measured 344 runtime imports as minified bundles. The primary size is gzip; raw minified size is included for diagnosis. An existing import growing by more than 10% is marked red. This report is informational.

Red Yellow Green Unchanged New Removed
0 10 0 334 0 0

Compared 5848d71b with ff32623b. Open workflow run.

Changed imports (10)
Status Import Base gzip Head gzip Delta
🟡 agents/browser#BrowserConnector 50.5 KiB 50.6 KiB +180 B (+0.35%)
🟡 agents/browser#connectBrowserSession 37.5 KiB 37.6 KiB +131 B (+0.34%)
🟡 agents/browser#connectBrowser 37.8 KiB 37.9 KiB +118 B (+0.3%)
🟡 agents/browser#connectUrl 37.6 KiB 37.7 KiB +100 B (+0.26%)
🟡 agents/browser#CdpSession 37.2 KiB 37.3 KiB +95 B (+0.25%)
🟡 agents/browser#createBrowserSession 36.3 KiB 36.4 KiB +58 B (+0.16%)
🟡 agents/browser/ai#createBrowserRuntime 147.0 KiB 147.2 KiB +160 B (+0.11%)
🟡 agents/browser/ai#createBrowserTools 147.0 KiB 147.2 KiB +160 B (+0.11%)
🟡 agents/browser/tanstack-ai#createBrowserTools 162.7 KiB 162.9 KiB +151 B (+0.09%)
🟡 agents/browser#browserPdf 36.3 KiB 36.3 KiB +6 B (+0.02%)
All 344 current runtime imports
Status Import Gzip Raw minified
⚪ agents#__DO_NOT_USE_WILL_BREAK__agentContext 261.1 KiB 1141.0 KiB
⚪ agents#__DO_NOT_USE_WILL_BREAK__withInvocationScope 261.1 KiB 1141.0 KiB
⚪ agents#Agent 261.1 KiB 1141.0 KiB
⚪ agents#AGENT_TOOL_MILESTONE_PART 261.1 KiB 1141.0 KiB
⚪ agents#AGENT_TOOL_PROGRESS_PART 261.1 KiB 1141.0 KiB
⚪ agents#buildAgentPath 261.7 KiB 1143.3 KiB
⚪ agents#buildAgentUrl 261.8 KiB 1143.7 KiB
⚪ agents#callable 261.1 KiB 1141.0 KiB
⚪ agents#camelCaseToKebabCase 261.1 KiB 1141.0 KiB
⚪ agents#createHeaderBasedEmailResolver 261.3 KiB 1141.4 KiB
⚪ agents#DEFAULT_AGENT_STATIC_OPTIONS 261.1 KiB 1141.0 KiB
⚪ agents#DurableObjectOAuthClientProvider 261.1 KiB 1141.0 KiB
⚪ agents#getAgentByName 261.1 KiB 1141.0 KiB
⚪ agents#getCurrentAgent 261.1 KiB 1141.0 KiB
⚪ agents#getSubAgentByName 261.4 KiB 1141.6 KiB
⚪ agents#isDurableObjectCodeUpdateReset 261.1 KiB 1141.0 KiB
⚪ agents#isDurableObjectMemoryLimitReset 261.1 KiB 1141.0 KiB
⚪ agents#isDurableObjectStorageReset 261.1 KiB 1141.0 KiB
⚪ agents#isPlatformTransientError 261.1 KiB 1141.0 KiB
⚪ agents#MCP_SERVER_ID_MAX_LENGTH 261.1 KiB 1141.0 KiB
⚪ agents#MessageType 261.2 KiB 1141.3 KiB
⚪ agents#normalizeServerId 261.1 KiB 1141.0 KiB
⚪ agents#parseSubAgentPath 261.1 KiB 1141.0 KiB
⚪ agents#routeAgentEmail 261.4 KiB 1141.7 KiB
⚪ agents#routeAgentRequest 261.7 KiB 1142.9 KiB
⚪ agents#routeSubAgentRequest 261.3 KiB 1141.5 KiB
⚪ agents#SqlError 261.1 KiB 1141.0 KiB
⚪ agents#StreamingResponse 261.1 KiB 1141.0 KiB
⚪ agents#SUB_PREFIX 261.1 KiB 1141.0 KiB
⚪ agents#unstable_callable 261.2 KiB 1141.2 KiB
⚪ agents/agent-tools#agentTool 112.5 KiB 538.2 KiB
🟡 agents/browser#BrowserConnector 50.6 KiB 177.2 KiB
⚪ agents/browser#browserContent 36.3 KiB 127.4 KiB
⚪ agents/browser#browserExtract 36.3 KiB 127.4 KiB
⚪ agents/browser#browserLinks 36.3 KiB 127.4 KiB
⚪ agents/browser#browserMarkdown 36.3 KiB 127.4 KiB
🟡 agents/browser#browserPdf 36.3 KiB 127.3 KiB
⚪ agents/browser#BrowserRenderingError 36.0 KiB 126.7 KiB
⚪ agents/browser#browserScrape 36.3 KiB 127.4 KiB
⚪ agents/browser#browserScreenshot 36.3 KiB 127.3 KiB
⚪ agents/browser#browserSnapshot 36.3 KiB 127.4 KiB
🟡 agents/browser#CdpSession 37.3 KiB 130.1 KiB
⚪ agents/browser#CodemodeRuntime 39.6 KiB 139.0 KiB
🟡 agents/browser#connectBrowser 37.9 KiB 131.7 KiB
🟡 agents/browser#connectBrowserSession 37.6 KiB 130.8 KiB
🟡 agents/browser#connectUrl 37.7 KiB 130.9 KiB
🟡 agents/browser#createBrowserSession 36.4 KiB 127.7 KiB
⚪ agents/browser#DEFAULT_EXEC_SWEEP_IDLE_MS 36.0 KiB 126.6 KiB
⚪ agents/browser#DEFAULT_SWEEP_IDLE_MS 36.0 KiB 126.6 KiB
⚪ agents/browser#deleteBrowserSession 36.1 KiB 126.9 KiB
⚪ agents/browser#DurableBrowserSessionStore 36.4 KiB 127.6 KiB
⚪ agents/browser#getBrowserRecording 36.2 KiB 127.1 KiB
⚪ agents/browser#listBrowserTargets 36.1 KiB 126.9 KiB
⚪ agents/browser#loadCdpSpec 36.6 KiB 128.3 KiB
⚪ agents/browser#runQuickAction 36.0 KiB 126.6 KiB
🟡 agents/browser/ai#createBrowserRuntime 147.2 KiB 633.5 KiB
🟡 agents/browser/ai#createBrowserTools 147.2 KiB 633.5 KiB
⚪ agents/browser/ai#createQuickActionTools 122.5 KiB 554.3 KiB
🟡 agents/browser/tanstack-ai#createBrowserTools 162.9 KiB 702.4 KiB
⚪ agents/channels#ChannelHost 3.3 KiB 9.1 KiB
⚪ agents/channels#consumeChunks 237 B 321 B
⚪ agents/channels#createUserIdentityStore 1.3 KiB 3.2 KiB
⚪ agents/channels#fallback 155 B 160 B
⚪ agents/channels#fallbackChannel 893 B 1.9 KiB
⚪ agents/channels#fanout 151 B 156 B
⚪ agents/channels#fanoutChannel 875 B 1.9 KiB
⚪ agents/channels#identityKey 178 B 223 B
⚪ agents/channels#isChannelMessageSurface 271 B 452 B
⚪ agents/channels#linkChannelIdentities 113 B 99 B
⚪ agents/channels#matchesPath 110 B 96 B
⚪ agents/channels#routes 307 B 507 B
⚪ agents/channels#UserIdentityConflictError 248 B 335 B
⚪ agents/channels/ai-sdk#createSendMessageTool 112.2 KiB 537.4 KiB
⚪ agents/channels/ai-sdk#toChannelChunks 112.5 KiB 538.4 KiB
⚪ agents/channels/email#email 23.5 KiB 75.4 KiB
⚪ agents/channels/email#inboundEmail 22.3 KiB 72.3 KiB
⚪ agents/channels/slack#slack 5.5 KiB 14.6 KiB
⚪ agents/channels/slack#slackWebhook 2.2 KiB 5.1 KiB
⚪ agents/channels/tanstack-ai#createSendMessageTool 16.2 KiB 68.4 KiB
⚪ agents/channels/telegram#telegram 3.8 KiB 10.3 KiB
⚪ agents/channels/telegram#telegramWebhook 1.6 KiB 3.6 KiB
⚪ agents/channels/voice#browserVoice 1010 B 2.2 KiB
⚪ agents/chat#AbortRegistry 2.5 KiB 8.9 KiB
⚪ agents/chat#AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS 2.3 KiB 8.2 KiB
⚪ agents/chat#AgentToolProgressEmitter 2.7 KiB 9.5 KiB
⚪ agents/chat#AgentToolStreamProgressThrottle 2.4 KiB 8.3 KiB
⚪ agents/chat#aiSdkRecoveryCodec 2.3 KiB 8.2 KiB
⚪ agents/chat#applyAgentToolEvent 3.2 KiB 11.0 KiB
⚪ agents/chat#applyChunkToParts 2.3 KiB 8.2 KiB
⚪ agents/chat#applyToolUpdate 2.4 KiB 8.4 KiB
⚪ agents/chat#AutoContinuationController 2.3 KiB 8.2 KiB
⚪ agents/chat#awaitWithDeadline 2.4 KiB 8.4 KiB
⚪ agents/chat#broadcastTransition 3.2 KiB 11.6 KiB
⚪ agents/chat#buildChatRecoveringFrame 2.4 KiB 8.4 KiB
⚪ agents/chat#buildInClauseStrings 2.4 KiB 8.4 KiB
⚪ agents/chat#bumpChatRecoveryProgress 2.4 KiB 8.3 KiB
⚪ agents/chat#byteLength 2.5 KiB 8.5 KiB
⚪ agents/chat#CHAT_LAST_TERMINAL_KEY 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_MESSAGE_TYPES 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_RECOVERING_FLAG_TTL_MS 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_RECOVERING_KEY 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_RECOVERY_ALARM_DEBOUNCE_MS 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_RECOVERY_INCIDENT_KEY_PREFIX 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_RECOVERY_INCIDENT_TTL_MS 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_RECOVERY_PROGRESS_KEY 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_RECOVERY_TASK_NAME 2.3 KiB 8.2 KiB
⚪ agents/chat#CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS 2.3 KiB 8.2 KiB
⚪ agents/chat#ChatRecoveryEngine 4.5 KiB 15.5 KiB
⚪ agents/chat#chatRecoveryTaskRunOptions 2.4 KiB 8.6 KiB
⚪ agents/chat#ChatStreamStalledError 2.4 KiB 8.3 KiB
⚪ agents/chat#classifyAgentToolChildRecovery 2.4 KiB 8.5 KiB
⚪ agents/chat#clearChatTerminal 2.3 KiB 8.3 KiB
⚪ agents/chat#clientResolvableToolNames 2.3 KiB 8.3 KiB
⚪ agents/chat#ContinuationState 2.7 KiB 9.8 KiB
⚪ agents/chat#createAgentToolEventState 2.3 KiB 8.3 KiB
⚪ agents/chat#createChatFiberSnapshot 2.5 KiB 8.6 KiB
⚪ agents/chat#createChatRecoveryTaskDefinition 2.6 KiB 9.0 KiB
⚪ agents/chat#createChatStreams 6.4 KiB 22.1 KiB
⚪ agents/chat#createChatTurnTaskDefinition 2.7 KiB 8.9 KiB
⚪ agents/chat#createToolsFromClientSchemas 114.3 KiB 545.4 KiB
⚪ agents/chat#crossMessageToolResultUpdate 2.4 KiB 8.6 KiB
⚪ agents/chat#DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS 2.3 KiB 8.2 KiB
⚪ agents/chat#DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES 2.3 KiB 8.2 KiB
⚪ agents/chat#DEFAULT_CHAT_RECOVERY_MAX_WORK 2.3 KiB 8.2 KiB
⚪ agents/chat#DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS 2.3 KiB 8.2 KiB
⚪ agents/chat#DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS 2.3 KiB 8.2 KiB
⚪ agents/chat#DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE 2.4 KiB 8.3 KiB
⚪ agents/chat#dispatchChatRecoveryToHandoff 3.0 KiB 9.9 KiB
⚪ agents/chat#drainInteractionApplies 2.3 KiB 8.3 KiB
⚪ agents/chat#enforceRowSizeLimit 3.5 KiB 11.2 KiB
⚪ agents/chat#hasIncompleteToolBatch 2.4 KiB 8.6 KiB
⚪ agents/chat#interceptAgentToolBroadcast 2.5 KiB 8.7 KiB
⚪ agents/chat#isPlatformFailure 2.6 KiB 8.9 KiB
⚪ agents/chat#isReplayChunk 2.4 KiB 8.7 KiB
⚪ agents/chat#iterateWithStallWatchdog 2.6 KiB 8.8 KiB
⚪ agents/chat#KV_DELETE_MAX_KEYS 2.3 KiB 8.2 KiB
⚪ agents/chat#listActiveChatRecoveryIncidents 2.4 KiB 8.4 KiB
⚪ agents/chat#MAX_BOUND_PARAMS 2.3 KiB 8.2 KiB
⚪ agents/chat#MessageType 2.4 KiB 9.0 KiB
⚪ agents/chat#normalizeToolInput 2.3 KiB 8.2 KiB
⚪ agents/chat#parseProtocolMessage 2.5 KiB 9.0 KiB
⚪ agents/chat#partAwaitsClientInteraction 2.4 KiB 8.6 KiB
⚪ agents/chat#pausedExecutionUpdate 2.4 KiB 8.4 KiB
⚪ agents/chat#pendingChatTerminal 2.3 KiB 8.3 KiB
⚪ agents/chat#persistReconstructedOrphan 3.1 KiB 11.0 KiB
⚪ agents/chat#PreStreamTurns 2.6 KiB 9.3 KiB
⚪ agents/chat#readChatRecoveryProgress 2.3 KiB 8.3 KiB
⚪ agents/chat#reconcileMessages 2.8 KiB 9.6 KiB
⚪ agents/chat#reconcileOrphanPartial 2.4 KiB 8.5 KiB
⚪ agents/chat#recordChatTerminal 2.4 KiB 8.3 KiB
⚪ agents/chat#repairInterruptedToolParts 2.6 KiB 9.1 KiB
⚪ agents/chat#resolveChatRecoveryConfig 2.6 KiB 8.9 KiB
⚪ agents/chat#resolveToolMergeId 2.4 KiB 8.5 KiB
⚪ agents/chat#ResumableStream 5.2 KiB 17.3 KiB
⚪ agents/chat#ResumeHandshake 3.0 KiB 10.4 KiB
⚪ agents/chat#ROW_MAX_BYTES 2.3 KiB 8.2 KiB
⚪ agents/chat#runChatRecoveryExhaustion 2.6 KiB 8.9 KiB
⚪ agents/chat#sanitizeMessage 2.5 KiB 8.8 KiB
⚪ agents/chat#sendIfOpen 2.4 KiB 8.4 KiB
⚪ agents/chat#setChatRecovering 2.5 KiB 8.5 KiB
⚪ agents/chat#shouldCreditStreamProgress 2.4 KiB 8.3 KiB
⚪ agents/chat#STREAM_RESUME_NONE_REASONS 2.3 KiB 8.2 KiB
⚪ agents/chat#StreamAccumulator 2.9 KiB 10.7 KiB
⚪ agents/chat#StreamProgressCreditThrottle 2.4 KiB 8.3 KiB
⚪ agents/chat#SubmitConcurrencyController 2.9 KiB 10.3 KiB
⚪ agents/chat#sweepStaleChatRecoveryIncidents 2.4 KiB 8.5 KiB
⚪ agents/chat#TextSegmentJoiner 2.7 KiB 9.2 KiB
⚪ agents/chat#TIMED_OUT 2.3 KiB 8.2 KiB
⚪ agents/chat#toolApprovalUpdate 2.4 KiB 8.5 KiB
⚪ agents/chat#toolPartHasSettledResult 2.3 KiB 8.4 KiB
⚪ agents/chat#toolResultUpdate 2.4 KiB 8.5 KiB
⚪ agents/chat#truncateOlderMessages 3.2 KiB 10.5 KiB
⚪ agents/chat#truncateOlderToolResults 3.3 KiB 10.9 KiB
⚪ agents/chat#TurnQueue 2.6 KiB 9.2 KiB
⚪ agents/chat#unwrapChatFiberSnapshot 2.4 KiB 8.5 KiB
⚪ agents/chat#wrapChatFiberSnapshot 2.3 KiB 8.2 KiB
⚪ agents/chat-sdk#ChatSdkStateAdapter 263.5 KiB 1152.6 KiB
⚪ agents/chat-sdk#ChatSdkStateAgent 262.8 KiB 1150.0 KiB
⚪ agents/chat-sdk#createChatSdkState 263.5 KiB 1152.6 KiB
⚪ agents/chat-sdk#defaultKeyShard 261.2 KiB 1141.2 KiB
⚪ agents/chat-sdk#defaultThreadShard 261.1 KiB 1141.0 KiB
⚪ agents/chat/react#detectToolsRequiringConfirmation 3.3 KiB 8.3 KiB
⚪ agents/chat/react#extractClientToolSchemas 3.2 KiB 8.3 KiB
⚪ agents/chat/react#getAgentMessages 3.4 KiB 8.6 KiB
⚪ agents/chat/react#getToolApproval 3.1 KiB 8.0 KiB
⚪ agents/chat/react#getToolCallId 3.1 KiB 8.0 KiB
⚪ agents/chat/react#getToolInput 3.1 KiB 8.0 KiB
⚪ agents/chat/react#getToolOutput 3.1 KiB 8.0 KiB
⚪ agents/chat/react#getToolPartState 3.2 KiB 8.2 KiB
⚪ agents/chat/react#useAgentChat 133.5 KiB 611.6 KiB
⚪ agents/chat/react#WebSocketChatTransport 6.2 KiB 18.7 KiB
⚪ agents/chat/transport#WebSocketChatTransport 3.3 KiB 10.7 KiB
⚪ agents/client#AgentClient 21.8 KiB 75.2 KiB
⚪ agents/client#AgentConnectionError 16.3 KiB 57.8 KiB
⚪ agents/client#agentFetch 19.7 KiB 69.0 KiB
⚪ agents/client#createStubProxy 16.4 KiB 57.8 KiB
⚪ agents/client#DEFAULT_CALL_TIMEOUT_MS 16.2 KiB 57.6 KiB
⚪ agents/client#isTerminalCloseEvent 16.3 KiB 57.6 KiB
⚪ agents/client#nativeCall 16.5 KiB 58.3 KiB
⚪ agents/client#NativeCallQueue 16.7 KiB 58.9 KiB
⚪ agents/client#splitCallOptions 16.3 KiB 57.7 KiB
⚪ agents/context#AgentContextProvider 412 B 792 B
⚪ agents/context#AgentSearchProvider 640 B 1.3 KiB
⚪ agents/context#ContextBlocks 87.4 KiB 429.7 KiB
⚪ agents/email#createAddressBasedEmailResolver 193 B 227 B
⚪ agents/email#createCatchAllEmailResolver 110 B 97 B
⚪ agents/email#createHeaderBasedEmailResolver 334 B 492 B
⚪ agents/email#createSecureReplyEmailResolver 718 B 1.3 KiB
⚪ agents/email#DEFAULT_MAX_AGE_SECONDS 56 B 39 B
⚪ agents/email#isAutoReplyEmail 201 B 249 B
⚪ agents/email#signAgentHeaders 424 B 812 B
⚪ agents/experimental/webmcp#registerWebMcp 85.2 KiB 295.8 KiB
⚪ agents/lifecycle#getCurrentAgent 376 B 798 B
⚪ agents/lifecycle#Lifecycle 8.4 KiB 26.1 KiB
⚪ agents/lifecycle#LifecycleCapability 497 B 999 B
⚪ agents/mcp#createLegacyMcpHandler 378.6 KiB 1583.3 KiB
⚪ agents/mcp#createMcpHandler 390.9 KiB 1628.5 KiB
⚪ agents/mcp#DurableObjectEventStore 345.1 KiB 1441.8 KiB
⚪ agents/mcp#ElicitRequestSchema 345.1 KiB 1441.8 KiB
⚪ agents/mcp#experimental_createMcpHandler 378.7 KiB 1583.6 KiB
⚪ agents/mcp#getMcpAuthContext 345.1 KiB 1441.9 KiB
⚪ agents/mcp#MCP_SERVER_ID_MAX_LENGTH 345.1 KiB 1441.9 KiB
⚪ agents/mcp#McpAgent 345.1 KiB 1441.8 KiB
⚪ agents/mcp#normalizeServerId 345.1 KiB 1441.8 KiB
⚪ agents/mcp#RPC_DO_PREFIX 345.1 KiB 1441.8 KiB
⚪ agents/mcp#RPCClientTransport 345.1 KiB 1441.8 KiB
⚪ agents/mcp#RPCServerTransport 345.1 KiB 1441.8 KiB
⚪ agents/mcp#SSEEdgeClientTransport 345.2 KiB 1442.1 KiB
⚪ agents/mcp#StreamableHTTPEdgeClientTransport 345.2 KiB 1442.1 KiB
⚪ agents/mcp#WorkerTransport 348.5 KiB 1458.7 KiB
⚪ agents/mcp/client#getNamespacedData 62.9 KiB 240.0 KiB
⚪ agents/mcp/client#MCP_SERVER_ID_MAX_LENGTH 62.9 KiB 239.9 KiB
⚪ agents/mcp/client#MCPClientManager 158.6 KiB 702.6 KiB
⚪ agents/mcp/client#normalizeServerId 63.0 KiB 240.2 KiB
⚪ agents/mcp/do-oauth-client-provider#DurableObjectOAuthClientProvider 2.1 KiB 6.6 KiB
⚪ agents/mcp/server#createMcpHandler 80.5 KiB 307.2 KiB
⚪ agents/mcp/server#getMcpAuthContext 64.0 KiB 245.5 KiB
⚪ agents/observability#channels 259 B 549 B
⚪ agents/observability#genericObservability 470 B 1.2 KiB
⚪ agents/observability#subscribe 324 B 668 B
⚪ agents/observability/ai#wrapAISDK 8.8 KiB 30.5 KiB
⚪ agents/queue#Queue 3.1 KiB 9.6 KiB
⚪ agents/react#_testUtils 19.3 KiB 66.4 KiB
⚪ agents/react#useAgent 27.1 KiB 89.9 KiB
⚪ agents/react#useAgentToolEvents 21.1 KiB 73.7 KiB
⚪ agents/routing#getAgentByName 795 B 1.7 KiB
⚪ agents/routing#routeAgentRequest 1.6 KiB 3.6 KiB
⚪ agents/routing#RoutedAgents 2.4 KiB 6.2 KiB
⚪ agents/schedule#getSchedulePrompt 85.8 KiB 424.7 KiB
⚪ agents/schedule#scheduleSchema 85.3 KiB 423.6 KiB
⚪ agents/schedule#unstable_getSchedulePrompt 85.9 KiB 424.9 KiB
⚪ agents/schedule#unstable_scheduleSchema 85.3 KiB 423.6 KiB
⚪ agents/schedules#Scheduler 6.8 KiB 22.0 KiB
⚪ agents/schedules/parser#getSchedulePrompt 85.8 KiB 424.7 KiB
⚪ agents/schedules/parser#scheduleSchema 85.3 KiB 423.6 KiB
⚪ agents/sessions#COMPACTION_PREFIX 147 B 160 B
⚪ agents/sessions#createCompactFunction 1.7 KiB 4.0 KiB
⚪ agents/sessions#isCompactionMessage 177 B 200 B
⚪ agents/sessions#Session 2.0 KiB 6.3 KiB
⚪ agents/sessions#Sessions 9.6 KiB 34.3 KiB
⚪ agents/skills#fromManifest 309.8 KiB 1084.0 KiB
⚪ agents/skills#parseSkillFrontmatter 328.4 KiB 1146.2 KiB
⚪ agents/skills#parseSkillMarkdown 328.6 KiB 1146.5 KiB
⚪ agents/skills#r2 330.2 KiB 1150.4 KiB
⚪ agents/skills#runner 369.0 KiB 1297.8 KiB
⚪ agents/skills#SkillRegistry 416.4 KiB 1581.7 KiB
⚪ agents/skills/compile#compileSkillScript 15.4 KiB 43.4 KiB
⚪ agents/skills/compile#isCompilableSkillScript 15.4 KiB 43.3 KiB
⚪ agents/state#State 1.1 KiB 2.4 KiB
⚪ agents/streams#DEFAULT_MAX_CHUNK_BYTES 94 B 96 B
⚪ agents/streams#sseResponse 857 B 1.6 KiB
⚪ agents/streams#StreamClosedError 175 B 212 B
⚪ agents/streams#StreamNotFoundError 215 B 276 B
⚪ agents/streams#Streams 4.3 KiB 13.9 KiB
⚪ agents/streams#StreamSerializationError 171 B 201 B
⚪ agents/tasks#DuplicateTaskStepError 328 B 463 B
⚪ agents/tasks#MAX_SERIALIZED_BYTES 190 B 232 B
⚪ agents/tasks#MissingTaskDefinitionError 358 B 536 B
⚪ agents/tasks#NonRetryableError 238 B 308 B
⚪ agents/tasks#TaskReplayDivergedError 341 B 483 B
⚪ agents/tasks#Tasks 8.9 KiB 31.8 KiB
⚪ agents/tasks#TaskSerializationError 258 B 339 B
⚪ agents/types#MessageType 211 B 365 B
⚪ agents/vite#default 353.8 KiB 1356.1 KiB
⚪ agents/voice#addSFUTracks 324 B 424 B
⚪ agents/voice#createSFUSession 255 B 306 B
⚪ agents/voice#createSFUWebSocketAdapter 331 B 429 B
⚪ agents/voice#decodeVarint 157 B 160 B
⚪ agents/voice#downsample48kStereoTo16kMono 238 B 324 B
⚪ agents/voice#encodePayloadToProtobuf 189 B 279 B
⚪ agents/voice#encodeVarint 129 B 122 B
⚪ agents/voice#extractPayloadFromProtobuf 271 B 425 B
⚪ agents/voice#iterateText 1.6 KiB 3.8 KiB
⚪ agents/voice#renegotiateSFUSession 329 B 428 B
⚪ agents/voice#SentenceChunker 550 B 1.1 KiB
⚪ agents/voice#sfuFetch 294 B 358 B
⚪ agents/voice#upsample16kMonoTo48kStereo 211 B 272 B
⚪ agents/voice#VOICE_PROTOCOL_VERSION 51 B 31 B
⚪ agents/voice#withVoice 9.5 KiB 32.7 KiB
⚪ agents/voice#withVoiceInput 4.8 KiB 16.0 KiB
⚪ agents/voice#WorkersAIFluxSTT 1.5 KiB 4.3 KiB
⚪ agents/voice#WorkersAINova3STT 1.6 KiB 4.3 KiB
⚪ agents/voice#WorkersAITTS 682 B 1.4 KiB
⚪ agents/voice/client#VOICE_PROTOCOL_VERSION 473 B 768 B
⚪ agents/voice/client#VoiceClient 10.1 KiB 31.6 KiB
⚪ agents/voice/client#WebSocketVoiceTransport 4.6 KiB 13.3 KiB
⚪ agents/voice/errors#logVoiceError 132 B 173 B
⚪ agents/voice/errors#toVoiceError 94 B 80 B
⚪ agents/voice/errors#voiceErrorMessage 125 B 111 B
⚪ agents/voice/errors#VoiceProviderError 190 B 345 B
⚪ agents/voice/react#useVoiceAgent 13.6 KiB 42.5 KiB
⚪ agents/voice/react#useVoiceInput 13.3 KiB 41.4 KiB
⚪ agents/voice/react#WebSocketVoiceTransport 7.4 KiB 21.3 KiB
⚪ agents/voice/sfu#addSFUTracks 323 B 424 B
⚪ agents/voice/sfu#createSFUSession 254 B 306 B
⚪ agents/voice/sfu#createSFUWebSocketAdapter 329 B 429 B
⚪ agents/voice/sfu#decodeVarint 155 B 160 B
⚪ agents/voice/sfu#downsample48kStereoTo16kMono 236 B 324 B
⚪ agents/voice/sfu#encodePayloadToProtobuf 188 B 279 B
⚪ agents/voice/sfu#encodeVarint 129 B 122 B
⚪ agents/voice/sfu#extractPayloadFromProtobuf 270 B 425 B
⚪ agents/voice/sfu#renegotiateSFUSession 329 B 428 B
⚪ agents/voice/sfu#sfuFetch 292 B 358 B
⚪ agents/voice/sfu#upsample16kMonoTo48kStereo 209 B 272 B
⚪ agents/voice/text#iterateText 1.6 KiB 3.8 KiB
⚪ agents/voice/text#SentenceChunker 549 B 1.1 KiB
⚪ agents/voice/types#VOICE_PROTOCOL_VERSION 51 B 31 B
⚪ agents/voice/workers-ai#WorkersAIFluxSTT 1.5 KiB 4.3 KiB
⚪ agents/voice/workers-ai#WorkersAINova3STT 1.6 KiB 4.3 KiB
⚪ agents/voice/workers-ai#WorkersAITTS 682 B 1.4 KiB
⚪ agents/websockets#CAPNWEB_TRANSPORT_QUERY 12.4 KiB 43.3 KiB
⚪ agents/websockets#CAPNWEB_TRANSPORT_VALUE 12.4 KiB 43.3 KiB
⚪ agents/websockets#capnWebTransportUrl 12.5 KiB 43.5 KiB
⚪ agents/websockets#CF_NO_PROTOCOL_KEY 12.4 KiB 43.3 KiB
⚪ agents/websockets#CF_READONLY_KEY 12.4 KiB 43.3 KiB
⚪ agents/websockets#isCapnWebTransportUpgrade 12.5 KiB 43.4 KiB
⚪ agents/websockets#registerInternalConnectionKeys 12.4 KiB 43.4 KiB
⚪ agents/websockets#WebSockets 20.0 KiB 69.3 KiB
⚪ agents/workflows#AgentWorkflow 262.4 KiB 1145.7 KiB
⚪ agents/workflows#WorkflowRejectedError 261.2 KiB 1141.2 KiB
⚪ agents/x402#normalizeNetwork 14.7 KiB 61.1 KiB
⚪ agents/x402#withX402 23.0 KiB 89.2 KiB
⚪ agents/x402#withX402Client 104.1 KiB 346.5 KiB

Reported by agent-think[bot].

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2302

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2302

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2302

hono-agents

npm i https://pkg.pr.new/hono-agents@2302

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2302

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2302

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2302

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2302

commit: ff32623

@ben-reitz
ben-reitz force-pushed the feat/browser-session-core branch 4 times, most recently from 5b4dad0 to 438e007 Compare September 19, 2026 16:36
keepAliveMs?: number;
includeTargets?: boolean;
recording?: boolean;
guardrails?: BrowserSessionGuardrails;

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.

If nothing is passed, is everything allowed or nothing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follows the default Browser Run behaviour of allowing all if nothing is passed.

@aron-cf aron-cf 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.

I think a usage example would help in the PR description. My understanding of this is it means that we can recover an existing browser session across executions?

Comment thread packages/agents/src/browser/browser-run.ts Outdated
Comment thread packages/agents/src/browser/browser-run.ts Outdated

/** The store key for a named browser session. */
export function namedBrowserSessionKey(name: string): string {
return `${NAMED_SESSION_KEY_PREFIX}${name}`;

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.

Does name need to be validated somewhere?

@ben-reitz ben-reitz Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's set in the host code, and only used in the session store so never reaches anything dangerous. I think the only limit it could break is the DO storage key max size which is massive and would break loudly.

we could iterate on this later if/when we start letting models or users influence names - that's when we might want to validate a more sensible length cap or check chars are printable etc.

* (and before it prunes a tombstone). Defaults to
* {@link DEFAULT_SWEEP_IDLE_MS} (10 minutes).
*/
sweepIdleMs?: number;

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 is weird that this exists here because the session doesn't actually handle the timeout, it's just here as metadata for the sweep method. I think it'd be cleaner to pass it into sweep(since: number) in the lifecycle capability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually moved both sweepIdleMs and touchIntervalMs to be a property of the session itself to fix a bug that occurred when the touchIntervalMs < sweepIdleMs.

This diagram hopefully explains it better than i can in words, but in essence, we have to keep track of when we last used the CDP connection between the agent and the browser, but we also need to throttle how often we write that message to the store (using touchIntervalMs). That throttle means if the sweep happens more often than the touchIntervalMs, the CDP connection could be in active use, but the browser will still get swept up if touchIntervalMs < sweepIdleMs

flowchart LR
    subgraph W["WRITER: every open connection (connect())"]
        A[CDP message<br/>sent or received] --> B{last touch<br/>older than<br/>touchIntervalMs?}
        B -- no --> X[skip]
        B -- yes --> C[touch:<br/>updatedAt = now]
    end
    C --> S[("store<br/>browser:session:checkout<br/>updatedAt")]
    subgraph R["READER: sweep() on the Lifecycle alarm"]
        D{now − updatedAt<br/>≥ sweepIdleMs?} -- yes --> E[close browser<br/>+ tombstone]
        D -- no --> F[leave it]
    end
    S --> D
Loading

Comment on lines +72 to +79
/**
* Minimum interval between activity-driven `updatedAt` refreshes on
* connected sockets. Defaults to {@link SESSION_TOUCH_INTERVAL_MS} and is
* always capped at half of `sweepIdleMs`, so continuous CDP traffic
* refreshes the idle clock before a sweep deadline can pass. Overridable
* primarily for tests.
*/
touchIntervalMs?: number;

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.

I'm not sure I understand this one either, it looks like it exists to throttle the touch calls in the onActivity handler, but I don't follow how it connects to the sleep.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread packages/agents/src/browser/session-core.ts Outdated
devin-ai-integration[bot]

This comment was marked as resolved.

@ben-reitz

ben-reitz commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

I think a usage example would help in the PR description. My understanding of this is it means that we can recover an existing browser session across executions?

Yep that's right. I've updated the PR description to try and make it clearer.

devin-ai-integration[bot]

This comment was marked as resolved.

@ben-reitz
ben-reitz force-pushed the feat/browser-session-core branch from 6c81bde to 3186292 Compare September 23, 2026 19:08
ben-reitz added a commit that referenced this pull request Sep 23, 2026
Review question on #2302: omitting guardrails entirely follows the
documented Browser Run default — all hostnames are allowed. Say so on
the type instead of making every reader ask.
ben-reitz added a commit that referenced this pull request Sep 23, 2026
…ssion

Review question on #2302 asked whether the conditional body existed
because the acquire endpoint rejects an empty body. Verified against the
live endpoint: acquire returns 200 for no body, an empty {} JSON body,
and a guardrails body alike. With the concern disproven, drop the
conditional and send one uniform request shape.
ben-reitz added a commit that referenced this pull request Sep 23, 2026
Five positional parameters with two trailing optional callbacks made
construction sites unreadable (review feedback on #2302). The socket
stays positional; timeoutMs, onClose, sessionId, and onActivity move
into a named CdpSessionOptions object.
ben-reitz added a commit that referenced this pull request Sep 23, 2026
Review feedback on #2302: Chromium-only options paired with
browser: "kitesurf" were only caught at runtime. ConnectBrowserOptions
and OneShotBrowserSessionOptions are now engine-discriminated unions —
selecting Kitesurf removes keepAliveMs/includeTargets/recording (and
guardrails for one-shots) at the type level, with browser: "chromium"
accepted explicitly on the default arm.

Runtime guards remain for plain-JS callers, consolidated into one
check with one message per entry point. New tests-d file pins the
compile-time rejections; runtime smuggle tests use casts to keep
exercising the guards.

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 1 new potential issue.

Devin Review

Comment on lines +273 to +275
const cdp = await connectBrowserSession(this.#browser, resolved.sessionId, {
timeoutMs: this.#timeoutMs,
onActivity: () => {

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.

🟡 Expiry race aborts named connection

When the browser expires after resolve, connect attaches to its stale ID and rejects instead of replacing it. Separate liveness and upgrade requests leave this expiration window.

Learn more

resolve() proves liveness with a /json/list request, then connect() performs a separate WebSocket upgrade. Browser Run can expire or reclaim the session between those requests. connectBrowserSession makes one upgrade attempt and turns a response without a WebSocket into an error, so this path never reaches the dead-session replacement logic.

Example: The liveness request for session-7 succeeds at the end of its keep-alive window. Browser Run expires it before the upgrade arrives. connect("checkout") rejects instead of returning a new session with restarted: true.

Recommended fix: Make attachment failure identify 404/410 responses, then atomically tombstone the matching stored session and retry resolution and attachment. Bound retries and avoid replacing an entry that another caller already swapped.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Provider-independent named sessions over the existing store contract:

- NamedBrowserSessions.resolve() is reattach-or-create keyed by
  browser:session:<name> — hosts wire names, models never see session
  identity. restarted: true signals loudly whenever a prior browser was
  lost (died upstream, closed, or swept); first-ever use is not a
  restart.
- Durable creation options (keepAliveMs, recording,
  guardrails.allowedDomains/allowedDomainSets) are reapplied on every
  create so they survive the restart path. keep_alive pins to the
  600-second platform max by default.
- createBrowserSession() gains guardrails support — they ride the
  acquire POST body per the Browser Run REST contract; Kitesurf rejects
  them.
- close() tombstones and deletes; sweep() closes idle sessions, leaves
  tombstones, and prunes aged ones (creator-overridable 10-minute TTL,
  alarm scheduling lands with the Lifecycle capability slice).
- openOneShotBrowserSession() is the trivial create-and-close branch:
  no store, platform session deleted when the CDP socket closes.
  connectBrowserSession() gains an optional onClose to support it.

Lock discipline mirrors BrowserConnector: locks wrap storage only,
probes and Browser Run calls run outside, commits re-check for
concurrent swaps (covered by tests, including a lock-across-network
violation detector). Internal module — no public export changes until
the starter-pack slice. Existing browser_execute behavior untouched.
Review question on #2302: omitting guardrails entirely follows the
documented Browser Run default — all hostnames are allowed. Say so on
the type instead of making every reader ask.
…ssion

Review question on #2302 asked whether the conditional body existed
because the acquire endpoint rejects an empty body. Verified against the
live endpoint: acquire returns 200 for no body, an empty {} JSON body,
and a guardrails body alike. With the concern disproven, drop the
conditional and send one uniform request shape.
Five positional parameters with two trailing optional callbacks made
construction sites unreadable (review feedback on #2302). The socket
stays positional; timeoutMs, onClose, sessionId, and onActivity move
into a named CdpSessionOptions object.
Review feedback on #2302: Chromium-only options paired with
browser: "kitesurf" were only caught at runtime. ConnectBrowserOptions
and OneShotBrowserSessionOptions are now engine-discriminated unions —
selecting Kitesurf removes keepAliveMs/includeTargets/recording (and
guardrails for one-shots) at the type level, with browser: "chromium"
accepted explicitly on the default arm.

Runtime guards remain for plain-JS callers, consolidated into one
check with one message per entry point. New tests-d file pins the
compile-time rejections; runtime smuggle tests use casts to keep
exercising the guards.
CDP activity refreshed the idle clock only through a throttled, asynchronous
store touch. A sweep that won the key lock first still saw the stale
updatedAt and deleted the browser under a command that had already been sent.

Record each send in memory synchronously and have the sweep's locked recheck
consult it, so any command sent before that recheck keeps its browser.
…stone

Pruning an aged-out tombstone removed the only record that a name had ever
owned a browser, so the next resolve reported restarted: false and the caller
was never told its state was gone — the common return-after-a-pause case.

Move pruned tombstones to a browser:retired:<name> marker outside the swept
keyspace (so the Lifecycle sweep job still retires) and treat it as restart
evidence on the create path.
A superseded socket that kept sending after its session was closed, swept,
or replaced re-recorded activity for a dead session id with no cleanup path.
Each sweep now drops activity for ids the store no longer holds live.

Also document that activity tracking is per instance: use one
NamedBrowserSessions per store.
…ed overload

Plain-JavaScript and prebuilt callers of new CdpSession(ws, timeoutMs, dispose, sessionId) would otherwise silently fall back to the 10s default and drop the close callback and session id. Normalize either form at runtime; the options object stays the preferred API.
@ben-reitz
ben-reitz force-pushed the feat/browser-session-core branch from fd42849 to ff32623 Compare September 24, 2026 13:22
@ben-reitz
ben-reitz merged commit 3f6bdad into main Sep 24, 2026
16 checks passed
@ben-reitz
ben-reitz deleted the feat/browser-session-core branch September 24, 2026 14:59
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.

2 participants