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:
- Add the benign
GPU access not allowed error to Sentry ignoreErrors / drop it in beforeSend so it can never flood.
- Don't request GPU access when
--disable-gpu is 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-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.
Summary
Running Beeper Desktop 4.2.972 fully headless (Xvfb, no real display) leaves
GET /v1/chatsreturning{"items":[]}even though the profile has synced (384 rooms inindex.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
4.2.972(production), AppImage,--appimage-extract-and-runXvfb :101+ openbox127.0.0.1:23373firstSyncDone: true, all bridges connected, 384 rooms inindex.dbBug 1 —
startup-bootstrapthrowsTypeError: Cannot read properties of null (reading 'length'), renderer never gets seed dataEvery launch the main process logs:
The bootstrap builder (
build/main/main-entry-*.mjs, function I'll callbuildStartupBootstrap) is, de-minified:Root cause: the
key_values['startupThreadIDs']row is only ever written by the renderer (setKeyValue("startupThreadIDs", …),VGe(...)inbuild/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.lengththrowstry/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
startupThreadIDsas an empty list instead of crashing: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-bootstraperror 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 processingEven with Bug 1 fixed, the main process sat at 100–170% CPU and
/v1/chatswas still empty. A V8 tick profile (--js-flags=--prof) of the main process:straceconfirmed the C++/__readhalf is thousands ofopenat("/proc/<pid>/stat"...)per second — Sentry gathering per-event process metrics.Instrumenting Sentry's
captureExceptionpath revealed the exception being captured in a tight loop:Root cause: headless setups pass
--disable-gpu(as recommended everywhere). Chromium then continuously raisesGPU 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/procmetric reads consume the whole core. The renderer is starved, somx_room_messages → messages → chat-previewprocessing never completes →/v1/chatsempty.Workaround that fixes it (operator side): don't disable the GPU — use software GL instead:
(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:
GPU access not allowederror to SentryignoreErrors/ drop it inbeforeSendso it can never flood.--disable-gpuis set (or catch the failure once instead of re-raising it).\S*Error:stack-parser backtracking.Verified result
Beeper Desktop 4.2.972, headless under Xvfb, with (1) the
startupThreadIDsrow seeded and (2) launched with--use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshaderinstead of--disable-gpu:GET /v1/chats→ 25 chats with real titles (was[])GET /v1/chats/{id}/messages→ returns messagesstartup-bootstraperrors, no moreGPU access not allowedfloodThis 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/chatsempty while message search works" shape matches exactly.Happy to provide full logs, the V8 profile, or test a patched build.