diff --git a/src/commands/apply-migrations.ts b/src/commands/apply-migrations.ts index 6504396e9..259409485 100644 --- a/src/commands/apply-migrations.ts +++ b/src/commands/apply-migrations.ts @@ -364,17 +364,27 @@ export async function runApplyMigrations(args: string[]): Promise { const { createEngine } = await import('../core/engine-factory.ts'); const cfg = lc(); if (cfg) { - const eng = await createEngine(toEngineConfig(cfg)); - await eng.connect(toEngineConfig(cfg)); - const verStr = await eng.getConfig('version'); - const schemaVer = parseInt(verStr || '1', 10); - await eng.disconnect(); - if (schemaVer < LATEST_VERSION) { - console.warn( - `\n⚠️ Schema version ${schemaVer} is behind latest ${LATEST_VERSION}.\n` + - ` Schema migrations run automatically on next connectEngine() / initSchema().\n` + - ` To run them now: gbrain init --migrate-only\n`, - ); + // v0.36.x #1100: skip the pre-flight warning on PGLite. The probe + // briefly holds the single-writer lock; if a downstream orchestrator + // phase spawns `gbrain init --migrate-only` as a subprocess (the + // legacy v0.11.0 phase A path), the child can race the parent's + // lock release and hit a 30s timeout. The orchestrators handle + // schema lifecycle internally on PGLite (phase A routes in-process), + // so the warning here adds no information for PGLite users. + const skipPreflight = cfg.engine === 'pglite'; + if (!skipPreflight) { + const eng = await createEngine(toEngineConfig(cfg)); + await eng.connect(toEngineConfig(cfg)); + const verStr = await eng.getConfig('version'); + const schemaVer = parseInt(verStr || '1', 10); + await eng.disconnect(); + if (schemaVer < LATEST_VERSION) { + console.warn( + `\n⚠️ Schema version ${schemaVer} is behind latest ${LATEST_VERSION}.\n` + + ` Schema migrations run automatically on next connectEngine() / initSchema().\n` + + ` To run them now: gbrain init --migrate-only\n`, + ); + } } } } catch { diff --git a/src/commands/migrations/v0_11_0.ts b/src/commands/migrations/v0_11_0.ts index cf5563cf7..e9c3ee685 100644 --- a/src/commands/migrations/v0_11_0.ts +++ b/src/commands/migrations/v0_11_0.ts @@ -58,9 +58,30 @@ export interface PendingHostWorkEntry { // Phase A — Schema // ----------------------------------------------------------------------- -function phaseASchema(opts: OrchestratorOpts): OrchestratorPhaseResult { +async function phaseASchema(opts: OrchestratorOpts): Promise { if (opts.dryRun) return { name: 'schema', status: 'skipped', detail: 'dry-run' }; try { + // v0.36.x #1100: route PGLite through an in-process schema apply rather + // than `execSync('gbrain init --migrate-only')`. The subprocess inherits + // HOME and tries to acquire the same file lock the parent process is + // holding (or briefly released and the on-disk artifact has not finished + // settling), which deadlocks until the 30s lock timeout fires. The + // structural fix is to not spawn a subprocess for work the parent can + // do directly — Postgres tolerates concurrent connections, so the + // legacy execSync path stays for Postgres callers. + const { loadConfig, toEngineConfig } = await import('../../core/config.ts'); + const cfg = loadConfig(); + if (cfg?.engine === 'pglite') { + const { createEngine } = await import('../../core/engine-factory.ts'); + const eng = await createEngine(toEngineConfig(cfg)); + try { + await eng.connect(toEngineConfig(cfg)); + await eng.initSchema(); + } finally { + try { await eng.disconnect(); } catch { /* best-effort */ } + } + return { name: 'schema', status: 'complete' }; + } execSync('gbrain init --migrate-only' + childGlobalFlags(), { stdio: 'inherit', timeout: 60_000, env: process.env }); return { name: 'schema', status: 'complete' }; } catch (e) { @@ -413,7 +434,7 @@ function phaseFInstall(opts: OrchestratorOpts): OrchestratorPhaseResult { async function orchestrator(opts: OrchestratorOpts): Promise { const phases: OrchestratorPhaseResult[] = []; - const a = phaseASchema(opts); + const a = await phaseASchema(opts); phases.push(a); if (a.status === 'failed') { console.error(`Phase A (schema) failed: ${a.detail}. Aborting; re-run after fixing.`);