Repository navigation
Beeper Desktop headless: /v1/chats empty — two root causes (startup-bootstrap null deref + GPU-access Sentry flood) with verified fixes #22
Description
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 inindex.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(...)returnsundefinedJSON.parse(undefined ?? "null")→nullnull.lengththrows- the whole
try/catchreturnsnull,renderer-bootstrap-syncreturns no seed data, and the inbox React tree never mounts — which is exactly whymx_room_messagesis never processed into the chat-preview layer and/v1/chatsstays 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:
- Add the benign
GPU access not allowederror to SentryignoreErrors/ drop it inbeforeSendso it can never flood. - Don't request GPU access when
--disable-gpuis set (or catch the failure once instead of re-raising it). - 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-bootstraperrors, no moreGPU access not allowedflood
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
linear-code commentedon Jul 13, 2026
LABS-109
COOPER2703 commentedon Jul 22, 2026
+1 I have the same problem on my setup, also fully headless.
Beeper Version: 4.2.998
Melickonline commentedon Aug 13, 2026
Confirming this on production/stable Server 4.3.34 (manual install) on macOS — not nightly-CLI-specific.
Same pattern here:
index.db threads table populated (823 rows), accounts table has 0 rows
Thousands of "queued N events for unloaded account" warnings, queue growing unbounded
/v1/chats and /v1/chats/search return empty regardless of parameters
Possibly related observation: the e2ee recovery-key unlock (POST /v1/app/setup/verification/recovery-key) doesn't persist across a server restart — every boot requires re-unlocking before state becomes ready. Not sure if that's the same root cause or a separate symptom of "no renderer to drive setup headlessly," but flagging it in case it's useful.
Phone-side symptom that matches: in WhatsApp's Linked devices list the server device permanently shows a sync error, and during a server restart the phone briefly reported "failed to unlock device."
Happy to share more logs/details if helpful.
batuhan commentedon Sep 25, 2026
Bug 1 is fixed in Desktop 4.2.1013: the bootstrap now treats a missing
startupThreadIDsas[]. The related Server symptom (accounts=0, "unloaded account") was a separate bug, fixed in Server 4.3.121. We couldn't reproduce the GPU-access error flood on 4.3.152 under Xvfb with--disable-gpuon a signed-out profile; we're still looking into it. Can you retest on current stable with your signed-in profile?This action is being done autonomously.
batuhan commentedon Sep 25, 2026
Follow-up on bug 2: we found the cause. The "GPU access not allowed" loop comes from the Sentry Electron SDK's GPU-context integration. On Linux with the GPU disabled, its GPU-info lookup fails, and the SDK re-reports that failure through the same code path forever. It isn't limited to headless setups: it also hits Linux users who turn off hardware acceleration in settings. We're updating to the SDK release that handles this. Until that ships, launching with
--use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshaderinstead of--disable-gpustill works around it, as you found.This action is being done autonomously.