browser relay: multi-session override, stale session recovery, better error feedback
- relay accepts new extension connections by closing old one (last wins, code 4000) - extension recognizes code 4000 and stops auto-reconnect (shows "replaced" badge) - fix ping interval race where old WS close handler killed new connection's pings - retry CDP commands on "Session with given id not found" by re-attaching debugger - describeError() maps known errors to user-friendly badge tooltips - persisted relay tokens restored on server start - user-scoped token salts, token regeneration/deletion endpoints - extension download endpoint, integrations UI for browser relay setup - browser relay env vars passed to pi-bridge sandboxes - browser screen layout with chat panel and prompt prefix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,9 @@ const tabOperationLocks = new Set()
|
||||
/** @type {Set<number>} */
|
||||
const reattachPending = new Set()
|
||||
|
||||
/** @type {Set<number>} */
|
||||
const reattachingTabs = new Set()
|
||||
|
||||
let reconnectAttempt = 0
|
||||
let reconnectTimer = null
|
||||
|
||||
@@ -162,8 +165,12 @@ async function ensureRelayConnection() {
|
||||
}
|
||||
})
|
||||
|
||||
ws.onclose = () => {
|
||||
ws.onclose = (ev) => {
|
||||
if (ws !== relayWs) return
|
||||
if (ev.code === 4000) {
|
||||
onRelayReplaced()
|
||||
return
|
||||
}
|
||||
onRelayClosed('closed')
|
||||
}
|
||||
ws.onerror = () => {
|
||||
@@ -205,6 +212,30 @@ function onRelayClosed(reason) {
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
function onRelayReplaced() {
|
||||
relayWs = null
|
||||
relayGatewayToken = ''
|
||||
relayConnectRequestId = null
|
||||
|
||||
for (const [id, p] of pending.entries()) {
|
||||
pending.delete(id)
|
||||
p.reject(new Error('Replaced by another browser session'))
|
||||
}
|
||||
|
||||
reattachPending.clear()
|
||||
|
||||
for (const [tabId, tab] of tabs.entries()) {
|
||||
if (tab.state === 'connected') {
|
||||
setBadge(tabId, 'error')
|
||||
void chrome.action.setTitle({
|
||||
tabId,
|
||||
title: 'Officer Browser Relay: replaced by another browser — click to reconnect',
|
||||
})
|
||||
}
|
||||
}
|
||||
// Do NOT schedule reconnect — user must click to take over again
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
@@ -534,6 +565,15 @@ async function detachTab(tabId, reason) {
|
||||
await persistState()
|
||||
}
|
||||
|
||||
function describeError(err) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
if (msg.includes('not reachable')) return 'relay not reachable — check server address in options'
|
||||
if (msg.includes('Missing relay token')) return 'no token configured — open options to set up'
|
||||
if (msg.includes('401') || msg.includes('Unauthorized')) return 'token rejected — check token in options'
|
||||
if (msg.toLowerCase().includes('timeout')) return 'connection timed out'
|
||||
return msg.length > 80 ? msg.slice(0, 80) + '…' : msg
|
||||
}
|
||||
|
||||
async function connectOrToggleForActiveTab() {
|
||||
const [active] = await chrome.tabs.query({ active: true, currentWindow: true })
|
||||
const tabId = active?.id
|
||||
@@ -576,7 +616,7 @@ async function connectOrToggleForActiveTab() {
|
||||
setBadge(tabId, 'error')
|
||||
void chrome.action.setTitle({
|
||||
tabId,
|
||||
title: 'Officer Browser Relay: relay not running (open options for setup)',
|
||||
title: `Officer Browser Relay: ${describeError(err)}`,
|
||||
})
|
||||
void maybeOpenHelpOnce()
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
@@ -660,7 +700,24 @@ async function handleForwardCdpCommand(msg) {
|
||||
? { ...debuggee, sessionId }
|
||||
: debuggee
|
||||
|
||||
return await chrome.debugger.sendCommand(debuggerSession, method, params)
|
||||
try {
|
||||
return await chrome.debugger.sendCommand(debuggerSession, method, params)
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
if (!message.includes('Session with given id not found')) throw err
|
||||
|
||||
// Chrome's internal debugger session became stale — re-attach and retry once
|
||||
console.warn(`Stale session for tab ${tabId}, re-attaching debugger`)
|
||||
reattachingTabs.add(tabId)
|
||||
try {
|
||||
await chrome.debugger.detach(debuggee).catch(() => {})
|
||||
await chrome.debugger.attach(debuggee, '1.3')
|
||||
await chrome.debugger.sendCommand(debuggee, 'Page.enable').catch(() => {})
|
||||
return await chrome.debugger.sendCommand(debuggee, method, params)
|
||||
} finally {
|
||||
reattachingTabs.delete(tabId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDebuggerEvent(source, method, params) {
|
||||
@@ -695,6 +752,7 @@ async function onDebuggerDetach(source, reason) {
|
||||
const tabId = source.tabId
|
||||
if (!tabId) return
|
||||
if (!tabs.has(tabId)) return
|
||||
if (reattachingTabs.has(tabId)) return
|
||||
|
||||
if (reason === 'canceled_by_user' || reason === 'replaced_with_devtools') {
|
||||
void detachTab(tabId, reason)
|
||||
|
||||
Reference in New Issue
Block a user