chenglou/pretext · error · Error

Timed out waiting for ${browser} automation lock

Error message

Timed out waiting for ${browser} automation lock

What it means

Thrown by acquireBrowserAutomationLock() when it cannot obtain the exclusive lock file (LOCK_DIR/<browser>.lock, under TMPDIR/pretext-browser-automation-locks) within timeoutMs (default 120s). The lock is per-browser and created with O_EXCL (open 'wx'); on EEXIST it checks if the owning pid is still alive and, if dead, self-heals by removing the stale lock and retrying. Only if a live owner holds it for the full timeout does this throw.

Source

Thrown at scripts/browser-automation.ts:211

            rmSync(lockPath)
          } catch {
            // Best effort cleanup.
          }
        },
      }
    } catch (error) {
      if (!(error instanceof Error) || !String(error).includes('EEXIST')) throw error
      const metadata = readLockMetadata(lockPath)
      if (metadata !== null && !isProcessAlive(metadata.pid)) {
        try {
          rmSync(lockPath)
          continue
        } catch {
          // Another process may have replaced or removed it. Retry normally.
        }
      }
      if (Date.now() - start >= timeoutMs) {
        throw new Error(`Timed out waiting for ${browser} automation lock`)
      }
      await sleep(250)
    }
  }
}

async function canReachUrl(url: string): Promise<boolean> {
  try {
    const response = await fetch(url)
    return response.ok
  } catch {
    return false
  }
}

const LOOPBACK_BASES = [
  'http://127.0.0.1',
  'http://localhost',

View on GitHub (pinned to ac49b09b7d)

Solutions

  1. Check for a live owner: read the pid from the lock file (TMPDIR/pretext-browser-automation-locks/<browser>.lock) and inspect that process.
  2. If the owner is genuinely stuck or orphaned, kill it and remove the lock file manually.
  3. Serialize browser-automation jobs per browser (do not run corpus-sweep and accuracy-check in parallel for the same browser).
  4. If you need a shorter/longer wait, pass a custom timeoutMs to acquireBrowserAutomationLock.

Example fix

# before: two concurrent jobs race the same browser lock
bun run scripts/accuracy-check.ts &
bun run scripts/corpus-sweep.ts &

# after: serialize per browser
bun run scripts/accuracy-check.ts && bun run scripts/corpus-sweep.ts
# or manually clear a dead-owner lock
rm "$TMPDIR/pretext-browser-automation-locks/chrome.lock"
Defensive patterns

Strategy: validation

Validate before calling

// Check the lock before acquiring; report the live owner if held.
import { readLockMetadata, isProcessAlive } from './browser-automation'
const meta = readLockMetadata(lockPath)
if (meta !== null && isProcessAlive(meta.pid)) {
  console.error(`Lock held by live pid ${meta.pid}; waiting up to 120s`)
}

Try / catch

// Wrap acquireBrowserAutomationLock so lock contention is a clean message, not a crash.
let lock
try {
  lock = await acquireBrowserAutomationLock(browser)
} catch (error) {
  console.error(`Could not acquire ${browser} lock: ${(error as Error).message}`)
  console.error(`Remove $TMPDIR/pretext-browser-automation-locks/${browser}.lock if stale.`)
  process.exit(1)
}

Prevention

When it happens

Trigger: Another accuracy-check/benchmark-check/corpus-sweep process is already running for the same browser and holds the lock; a previous process is hung (alive pid but stuck); or two scripts started near-simultaneously and one starved the other past 120s. The single-owner-per-browser rule (documented in AGENTS.md) is the design reason.

Common situations: Running corpus-sweep and accuracy-check concurrently against Chrome; a prior checker that crashed without releasing the lock but whose shell wrapper process is still alive; a hung browser automation session; CI scheduling overlapping jobs for the same browser.

Understand the failure class

Related errors


AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12). Data as JSON: /api/errors/be246b935659b762. Report an issue: GitHub.