Skip to content

Beeper Desktop headless: /v1/chats empty — two root causes (startup-bootstrap null deref + GPU-access Sentry flood) with verified fixes #22

Description

@pallagj

Summary

Running Beeper Desktop 4.2.972 fully headless (Xvfb, no real display) leaves GET /v1/chats returning {"items":[]} even though the profile has synced (384 rooms in index.db), reproducing the same "chats API empty on headless" symptom as #14 and #21. I traced it to two independent, concrete bugs and have a verified fix for both — after applying them the API went from 0 → 25 chats and the inbox processing completes normally.

Both fixes are small and local. Sharing full root cause + repro so a proper fix can ship.

Environment

  • Beeper Desktop 4.2.972 (production), AppImage, --appimage-extract-and-run
  • Ubuntu 24.04 headless server, no GPU, running under Xvfb :101 + openbox
  • Desktop API on 127.0.0.1:23373
  • Account fully signed in + E2EE-verified, firstSyncDone: true, all bridges connected, 384 rooms in index.db

Bug 1 — startup-bootstrap throws TypeError: Cannot read properties of null (reading 'length'), renderer never gets seed data

Every launch the main process logs:

[AppStart] renderer-bootstrap-sync: migrations ready, reading bootstrap
[error] [startup-bootstrap] failed to build bootstrap payload TypeError: Cannot read properties of null (reading 'length')

The bootstrap builder (build/main/main-entry-*.mjs, function I'll call buildStartupBootstrap) is, de-minified:

function buildStartupBootstrap() {
  const db = new Sqlite();
  db.init(path.join(dataDir, "index.db"), { readonly: true, fileMustExist: true });
  const raw = db.pluck_get("SELECT value from key_values WHERE key = ?", "startupThreadIDs");
  const ids = JSON.parse(raw ?? "null");            // <-- raw is undefined on a fresh profile → JSON.parse("null") → null
  const rows = db.prepareSQL(threadsByIdSQL(ids.length)).all(...ids);  // <-- ids.length throws on null
  return {
    rawKeyValues: ...,
    initialAccounts: ...,
    initialLabels: ...,
    initialUnions: ...,
    startupThreads: mapThreads(rows),
  };
}

Root cause: the key_values['startupThreadIDs'] row is only ever written by the renderer (setKeyValue("startupThreadIDs", …), VGe(...) in build/renderer/App-*.js) after the inbox has mounted. On a profile that has never had a successful GUI mount (i.e. any headless/server profile), the row does not exist yet, so:

  • pluck_get(...) returns undefined
  • JSON.parse(undefined ?? "null") → null
  • null.length throws
  • the whole try/catch returns null, renderer-bootstrap-sync returns no seed data, and the inbox React tree never mounts — which is exactly why mx_room_messages is never processed into the chat-preview layer and /v1/chats stays empty.

It's a chicken-and-egg: the renderer must mount to write the key, but the missing key prevents the mount.

Fix — treat a missing/invalid startupThreadIDs as an empty list instead of crashing:

const parsed = JSON.parse(raw ?? "null");
const ids = Array.isArray(parsed) ? parsed : [];
const rows = db.prepareSQL(threadsByIdSQL(ids.length)).all(...ids);

I confirmed this is the trigger by seeding the row manually
(INSERT OR REPLACE INTO key_values (key, value) VALUES ('startupThreadIDs', '[...20 threadIDs...]')),
after which the startup-bootstrap error disappears on every subsequent launch and the renderer mounts.


Bug 2 — with --disable-gpu (every headless setup), a "GPU access not allowed" error floods Sentry and pegs the CPU, starving chat/message processing

Even with Bug 1 fixed, the main process sat at 100–170% CPU and /v1/chats was still empty. A V8 tick profile (--js-flags=--prof) of the main process:

 [Summary]
   31.4% JavaScript   45.7% C++   23.0% shared libs
 [JavaScript] (hottest)
   29.1%  RegExp: \S*Error:            <-- Sentry stack-trace parser
    0.6%  <anonymous> build/main/init-sentry-electron-*.mjs
 [Bottom up]
   29.1%  RegExp: \S*Error:  ->  init-sentry-electron-*.mjs  (Sentry event pipeline)
   21.5%  __read  ->  init-sentry-electron-*.mjs             (per-event /proc/*/stat metric reads)

strace confirmed the C++/__read half is thousands of openat("/proc/<pid>/stat"...) per second — Sentry gathering per-event process metrics.

Instrumenting Sentry's captureException path revealed the exception being captured in a tight loop:

Error: GPU access not allowed. Reason: GPU access is disabled through commandline
switch --disable-gpu and --disable-software-rasterizer.
    at Object.processEvent (build/main/init-sentry-electron-*.mjs)
    at ... (Sentry event pipeline)

Root cause: headless setups pass --disable-gpu (as recommended everywhere). Chromium then continuously raises GPU access not allowed; Beeper's Sentry Electron integration captures every occurrence, and the stack-trace parser regex \S*Error: (catastrophic backtracking on the large minified frames) plus the per-event /proc metric reads consume the whole core. The renderer is starved, so mx_room_messages → messages → chat-preview processing never completes → /v1/chats empty.

Workaround that fixes it (operator side): don't disable the GPU — use software GL instead:

--use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshader

(drop --disable-gpu). GPU access is then "allowed" via SwiftShader, the error stops, Sentry goes quiet, CPU drops to ~15–30%, and chats populate.

Proper fix (Beeper side), any of:

  1. Add the benign GPU access not allowed error to Sentry ignoreErrors / drop it in beforeSend so it can never flood.
  2. Don't request GPU access when --disable-gpu is set (or catch the failure once instead of re-raising it).
  3. Rate-limit Sentry captures and/or fix the \S*Error: stack-parser backtracking.

Verified result

Beeper Desktop 4.2.972, headless under Xvfb, with (1) the startupThreadIDs row seeded and (2) launched with --use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshader instead of --disable-gpu:

  • GET /v1/chats → 25 chats with real titles (was [])
  • GET /v1/chats/{id}/messages → returns messages
  • total Beeper CPU ~16% (was one process pegged at 100%+)
  • no more startup-bootstrap errors, no more GPU access not allowed flood

This is very likely the same underlying failure behind #14 and #21 on Beeper Server — Bug 1 (bootstrap on a profile that never mounted a GUI) applies directly; Bug 2 is Chromium-specific so it may differ on the non-Electron server binary, but the "renderer/processing never runs → /v1/chats empty while message search works" shape matches exactly.

Happy to provide full logs, the V8 profile, or test a patched build.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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