Skip to content

fix: Race condition in window ownership registration causes multi-window routing failures #277

@Suhaib3100

Description

@Suhaib3100

Problem

When a new browser window is created, there's a race condition between:

  1. The controller extension sending window_created WebSocket message to register ownership
  2. HTTP requests from the agent arriving at the server for that window

The HTTP request often arrives before the WebSocket message is processed, causing:

  • Requests to route to the wrong window (falls back to primaryClientId)
  • Multi-profile support to break
  • Scheduled jobs executing in incorrect windows

Current Workaround

A hacky 1-second delay is used in scheduledJobRuns.ts:

// FIXME: Race condition - the controller-ext extension sends a window_created
// WebSocket message to register window ownership, but our HTTP request may arrive
// at the server before that registration completes. This delay is a temporary fix.
await new Promise((resolve) => setTimeout(resolve, 1000))

This causes poor UX and doesn't guarantee the race is resolved.

Affected Files

  • apps/server/src/browser/extension/bridge.ts (line 143-147)
  • apps/agent/entrypoints/background/scheduledJobRuns.ts (line 116-120)

Proposed Solution

Implement async window registration with polling in ControllerBridge.sendRequest():

  1. When a request arrives with a windowId that's not yet registered
  2. Poll/wait for up to 500ms for the ownership registration to complete
  3. Only fall back to primaryClientId after timeout
  4. Remove the hacky 1-second delay from scheduledJobRuns.ts

Implementation Approach

// In ControllerBridge.sendRequest()
async waitForWindowOwnership(windowId: number, timeoutMs: number = 500): Promise<string | null> {
  const startTime = Date.now()
  const pollInterval = 50 // ms
  
  while (Date.now() - startTime < timeoutMs) {
    const ownerClientId = this.windowOwnership.get(windowId)
    if (ownerClientId && this.clients.has(ownerClientId)) {
      return ownerClientId
    }
    await new Promise(resolve => setTimeout(resolve, pollInterval))
  }
  
  return null // Timeout - fall back to primary
}

Then in sendRequest():

let targetClientId = this.primaryClientId
if (windowId !== undefined) {
  const ownerClientId = await this.waitForWindowOwnership(windowId, 500)
  if (ownerClientId) {
    targetClientId = ownerClientId
  } else {
    this.logger.warn('Window ownership timeout, using primary', { windowId })
  }
}

Benefits

  • Proper multi-window/multi-profile support
  • Removes hacky delays
  • Better UX for scheduled jobs
  • More reliable window routing

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions