From 59bbcf5d9983555aac790cacc3f127b8d1fdbef4 Mon Sep 17 00:00:00 2001 From: Zakir Jiwani <108548454+JiwaniZakir@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:53:40 +0000 Subject: [PATCH 1/2] docs: document PG LISTEN/NOTIFY migration in mailbox source Co-Authored-By: Claude Sonnet 4.6 --- src/lib/mailbox.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lib/mailbox.ts b/src/lib/mailbox.ts index b0d1b16e3..434822e2f 100644 --- a/src/lib/mailbox.ts +++ b/src/lib/mailbox.ts @@ -1,13 +1,30 @@ /** * Mailbox — Durable message store with unread/read semantics. * + * ## Migration note (commit 44a153a3) + * + * Previously messages were written to `.genie/mailbox/.json` files and + * delivered by a polling loop that woke up every few seconds to check for new + * entries. This introduced latency proportional to the poll interval and made + * cross-process coordination fragile. + * + * The current implementation replaced file-based polling with PostgreSQL: + * - Messages are persisted to the `mailbox` table (durable, queryable). + * - A `AFTER INSERT` trigger fires `pg_notify('genie_mailbox_delivery', …)` + * with payload `:`. + * - `subscribeDelivery()` calls `sql.listen('genie_mailbox_delivery', …)` so + * the scheduler daemon receives the notification instantly — no polling. + * - A 30-second fallback poll catches any notifications missed during + * reconnects or daemon restarts. + * + * `.genie/mailbox/` JSON files are no longer written or read; references to + * that path in older docs are outdated. + * * Messages persist to PostgreSQL `mailbox` table before any push delivery * attempt. This ensures durability (DEC-7). * * Delivery is state-aware: messages are queued and pushed to tmux * panes only when the worker is idle (not mid-turn). - * - * PG LISTEN/NOTIFY triggers instant delivery notification on new inserts. */ import { v4 as uuidv4 } from 'uuid'; From fc0de3853801bd522280a0b67cdf34b3d70db46f Mon Sep 17 00:00:00 2001 From: Rafael Calassara Date: Sat, 4 Apr 2026 16:41:12 -0300 Subject: [PATCH 2/2] fix(db): detect WSL2 and adjust pgserve startup timeout Add WSL2 detection via /proc/version check and increase pgserve startup timeout from 15s to 30s on WSL2 systems. Use exponential backoff polling with 2s initial bootstrap delay to accommodate slower I/O during database initialization. Non-WSL2 systems (Mac, Linux, Windows) unchanged. - New module: src/lib/wsl2-detect.ts with memoized isWSL2() detection - Modified: src/lib/db.ts to use conditional timeout and bootstrap delay - Fixes pgserve timeout failures on WSL2 without affecting other platforms --- src/lib/db.ts | 23 ++++++++++++++++++++--- src/lib/wsl2-detect.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 src/lib/wsl2-detect.ts diff --git a/src/lib/db.ts b/src/lib/db.ts index 817f2a092..1a9e44072 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -13,6 +13,7 @@ import { join } from 'node:path'; import type postgres from 'postgres'; import { runMigrations } from './db-migrations.js'; import { needsSeed, runSeed } from './pg-seed.js'; +import { isWSL2 } from './wsl2-detect.js'; /** * Re-export Sql type for callers that need to annotate sql connection parameters. @@ -327,10 +328,16 @@ function findPgserveBin(): string { * Avoids the self-referencing proxy deadlock that occurs when the * MultiTenantRouter Bun TCP proxy runs in the same event loop as * the daemon that also connects to it. + * + * On WSL2, uses an extended timeout (30s) due to slower I/O performance. */ async function startPgserveOnPort(port: number): Promise { mkdirSync(DATA_DIR, { recursive: true }); + const isWsl = isWSL2(); + const timeoutMs = isWsl ? 30000 : 15000; + const timeoutSec = timeoutMs / 1000; + const child = spawn( findPgserveBin(), [ @@ -352,7 +359,13 @@ async function startPgserveOnPort(port: number): Promise { child.unref(); pgserveChild = child; - const deadline = Date.now() + 15000; + const initialBootstrapMs = isWsl ? 2000 : 100; + await new Promise((r) => setTimeout(r, initialBootstrapMs)); + + const deadline = Date.now() + timeoutMs; + let retryDelayMs = 100; + const maxRetryDelayMs = 1000; + while (Date.now() < deadline) { if (await isPostgresHealthy(port)) { activePort = port; @@ -361,7 +374,9 @@ async function startPgserveOnPort(port: number): Promise { writeLockfile(port); return port; } - await new Promise((r) => setTimeout(r, 500)); + + await new Promise((r) => setTimeout(r, retryDelayMs)); + retryDelayMs = Math.min(Math.floor(retryDelayMs * 1.5), maxRetryDelayMs); } try { @@ -369,7 +384,9 @@ async function startPgserveOnPort(port: number): Promise { } catch { /* dead */ } - throw new Error(`pgserve failed to start on port ${port} (timeout after 15s)`); + + process.env.GENIE_PG_AVAILABLE = 'false'; + throw new Error(`pgserve failed to start on port ${port} (timeout after ${timeoutSec}s)`); } /** Register process exit handler to clean up lockfile (once). */ diff --git a/src/lib/wsl2-detect.ts b/src/lib/wsl2-detect.ts new file mode 100644 index 000000000..7e55cbec3 --- /dev/null +++ b/src/lib/wsl2-detect.ts @@ -0,0 +1,31 @@ +/** + * WSL2 environment detection. + * + * Genie on WSL2 experiences slower pgserve startup due to I/O characteristics. + * This utility detects WSL2 and allows callers to adjust timeouts accordingly. + */ + +import { readFileSync } from 'node:fs'; + +let memoized: boolean | null = null; + +/** + * Detect if running on WSL2 by checking /proc/version for "microsoft" or "wsl". + * Result is memoized for performance. + * + * @returns true if running on WSL2, false otherwise + */ +export function isWSL2(): boolean { + // Return memoized result if already computed + if (memoized !== null) return memoized; + + try { + const procVersion = readFileSync('/proc/version', 'utf-8'); + memoized = procVersion.toLowerCase().includes('microsoft') || procVersion.toLowerCase().includes('wsl'); + return memoized; + } catch { + // /proc/version not readable (non-Linux or permission issue) — assume not WSL2 + memoized = false; + return false; + } +}