From f765b811258ea1a7b3fc0ad0d11299e3f42dda98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=BA=90=E6=B3=89?= Date: Mon, 4 May 2026 16:47:39 -0700 Subject: [PATCH] fix(bootstrap): cover v0.26.3 mcp_request_log columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `applyForwardReferenceBootstrap()` covered `pages.deleted_at` (v0.26.5) but missed the v0.26.3 columns on `mcp_request_log` that SCHEMA_SQL forward-references via `idx_mcp_log_agent_time(agent_name, …)`. Brains at schema v31 (any v0.22.7+ install that hadn't yet run v0.26.3 migrations) crashed during `gbrain init --migrate-only`: column "agent_name" does not exist …before migration v33 (`admin_dashboard_columns_v0_26_3`) had a chance to add the column. The bootstrap is the structural fix for this class (see `test/schema-bootstrap-coverage.test.ts` preamble for #239, #243, #266, #357, #366, #374, #375, #378, #395, #396 history). Changes: - `src/core/postgres-engine.ts#applyForwardReferenceBootstrap`: probe for `mcp_request_log.{agent_name, params, error_message}`, ALTER TABLE ADD COLUMN IF NOT EXISTS the missing ones. Migration v33 still runs after and remains idempotent. - `src/core/pglite-engine.ts#applyForwardReferenceBootstrap`: mirror. - `test/schema-bootstrap-coverage.test.ts`: extend `REQUIRED_BOOTSTRAP_COVERAGE` with 3 entries; both test cases drop the new columns before running bootstrap so the contract is enforced. - `test/e2e/postgres-bootstrap.test.ts`: regression case mutating a fresh-LATEST brain to v31 mcp_request_log shape (drops 3 columns + index), asserts initSchema succeeds and reaches LATEST_VERSION. Bootstrap test (PGLite, hermetic): 2/2 pass. Postgres bootstrap E2E: 3/3 pass (33ms for new case against pgvector:pg16). Typecheck: clean. Repro for users stuck on this: ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS agent_name TEXT, ADD COLUMN IF NOT EXISTS params JSONB, ADD COLUMN IF NOT EXISTS error_message TEXT; …then `gbrain init --migrate-only` proceeds normally. After this PR the bootstrap handles it automatically. Co-authored-by: Cursor --- src/core/pglite-engine.ts | 46 ++++++++++++++++++++++- src/core/postgres-engine.ts | 51 ++++++++++++++++++++++++-- test/e2e/postgres-bootstrap.test.ts | 35 ++++++++++++++++++ test/schema-bootstrap-coverage.test.ts | 19 ++++++++++ 4 files changed, 146 insertions(+), 5 deletions(-) diff --git a/src/core/pglite-engine.ts b/src/core/pglite-engine.ts index 16ec300cc..ccf171879 100644 --- a/src/core/pglite-engine.ts +++ b/src/core/pglite-engine.ts @@ -213,6 +213,9 @@ export class PGLiteEngine implements BrainEngine { * - `content_chunks.symbol_name` column (indexed by `idx_chunks_symbol_name`) — v0.19 * - `content_chunks.language` column (indexed by `idx_chunks_language`) — v0.19 * - `pages.deleted_at` column (indexed by `pages_deleted_at_purge_idx`) — v0.26.5 + * - `mcp_request_log.agent_name` column (indexed by `idx_mcp_log_agent_time`) — v0.26.3 + * - `mcp_request_log.params` column (referenced by SCHEMA_SQL replay) — v0.26.3 + * - `mcp_request_log.error_message` column (referenced by SCHEMA_SQL replay) — v0.26.3 * * **Maintenance contract:** when a future migration adds a column-with-index * or new-table-with-FK referenced by PGLITE_SCHEMA_SQL, extend this method @@ -240,7 +243,15 @@ export class PGLiteEngine implements BrainEngine { EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='content_chunks' AND column_name='symbol_name') AS symbol_name_exists, EXISTS (SELECT 1 FROM information_schema.columns - WHERE table_schema='public' AND table_name='content_chunks' AND column_name='language') AS language_exists + WHERE table_schema='public' AND table_name='content_chunks' AND column_name='language') AS language_exists, + EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema='public' AND table_name='mcp_request_log') AS mcp_log_exists, + EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema='public' AND table_name='mcp_request_log' AND column_name='agent_name') AS mcp_log_agent_name_exists, + EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema='public' AND table_name='mcp_request_log' AND column_name='params') AS mcp_log_params_exists, + EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema='public' AND table_name='mcp_request_log' AND column_name='error_message') AS mcp_log_error_message_exists `); const probe = rows[0] as { pages_exists: boolean; @@ -252,6 +263,10 @@ export class PGLiteEngine implements BrainEngine { chunks_exists: boolean; symbol_name_exists: boolean; language_exists: boolean; + mcp_log_exists: boolean; + mcp_log_agent_name_exists: boolean; + mcp_log_params_exists: boolean; + mcp_log_error_message_exists: boolean; }; const needsPagesBootstrap = probe.pages_exists && !probe.source_id_exists; @@ -260,9 +275,22 @@ export class PGLiteEngine implements BrainEngine { const needsChunksBootstrap = probe.chunks_exists && (!probe.symbol_name_exists || !probe.language_exists); const needsPagesDeletedAt = probe.pages_exists && !probe.deleted_at_exists; + // v0.26.3: idx_mcp_log_agent_time in PGLITE_SCHEMA_SQL crashes if + // agent_name doesn't exist yet. v33 (admin_dashboard_columns_v0_26_3) + // also adds these but bootstrap runs first. + const needsMcpLogBootstrap = probe.mcp_log_exists + && (!probe.mcp_log_agent_name_exists + || !probe.mcp_log_params_exists + || !probe.mcp_log_error_message_exists); // Fresh installs (no tables yet) and modern brains both no-op. - if (!needsPagesBootstrap && !needsLinksBootstrap && !needsChunksBootstrap && !needsPagesDeletedAt) return; + if ( + !needsPagesBootstrap + && !needsLinksBootstrap + && !needsChunksBootstrap + && !needsPagesDeletedAt + && !needsMcpLogBootstrap + ) return; console.log(' Pre-v0.21 brain detected, applying forward-reference bootstrap'); @@ -320,6 +348,20 @@ export class PGLiteEngine implements BrainEngine { ALTER TABLE pages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; `); } + + if (needsMcpLogBootstrap) { + // v33 (admin_dashboard_columns_v0_26_3) adds these three columns to + // mcp_request_log AND backfills agent_name. Bootstrap only adds the + // column shells so PGLITE_SCHEMA_SQL's + // `CREATE INDEX idx_mcp_log_agent_time ON mcp_request_log(agent_name, ...)` + // doesn't crash on a v0.22.7-shape mcp_request_log. v33 runs later via + // runMigrations and is idempotent. + await this.db.exec(` + ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS agent_name TEXT; + ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS params JSONB; + ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS error_message TEXT; + `); + } } async withReservedConnection(fn: (conn: ReservedConnection) => Promise): Promise { diff --git a/src/core/postgres-engine.ts b/src/core/postgres-engine.ts index 8f84ffa5c..3d1fa3bd2 100644 --- a/src/core/postgres-engine.ts +++ b/src/core/postgres-engine.ts @@ -153,6 +153,9 @@ export class PostgresEngine implements BrainEngine { * - `content_chunks.symbol_name` column (indexed by `idx_chunks_symbol_name`) — v0.19 * - `content_chunks.language` column (indexed by `idx_chunks_language`) — v0.19 * - `pages.deleted_at` column (indexed by `pages_deleted_at_purge_idx`) — v0.26.5 + * - `mcp_request_log.agent_name` column (indexed by `idx_mcp_log_agent_time`) — v0.26.3 + * - `mcp_request_log.params` column (referenced by SCHEMA_SQL replay) — v0.26.3 + * - `mcp_request_log.error_message` column (referenced by SCHEMA_SQL replay) — v0.26.3 * * Keep this in sync with the PGLite version; covered by * `test/schema-bootstrap-coverage.test.ts` (PGLite side) and @@ -174,6 +177,10 @@ export class PostgresEngine implements BrainEngine { chunks_exists: boolean; symbol_name_exists: boolean; language_exists: boolean; + mcp_log_exists: boolean; + mcp_log_agent_name_exists: boolean; + mcp_log_params_exists: boolean; + mcp_log_error_message_exists: boolean; }[]>` SELECT EXISTS (SELECT 1 FROM information_schema.tables @@ -193,7 +200,15 @@ export class PostgresEngine implements BrainEngine { EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'content_chunks' AND column_name = 'symbol_name') AS symbol_name_exists, EXISTS (SELECT 1 FROM information_schema.columns - WHERE table_schema = current_schema() AND table_name = 'content_chunks' AND column_name = 'language') AS language_exists + WHERE table_schema = current_schema() AND table_name = 'content_chunks' AND column_name = 'language') AS language_exists, + EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = current_schema() AND table_name = 'mcp_request_log') AS mcp_log_exists, + EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = current_schema() AND table_name = 'mcp_request_log' AND column_name = 'agent_name') AS mcp_log_agent_name_exists, + EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = current_schema() AND table_name = 'mcp_request_log' AND column_name = 'params') AS mcp_log_params_exists, + EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = current_schema() AND table_name = 'mcp_request_log' AND column_name = 'error_message') AS mcp_log_error_message_exists `; const probe = probeRows[0]!; @@ -205,8 +220,22 @@ export class PostgresEngine implements BrainEngine { // v0.26.5: pages_deleted_at_purge_idx in SCHEMA_SQL crashes if the column // doesn't exist yet. Migration v34 also adds it, but bootstrap runs first. const needsPagesDeletedAt = probe.pages_exists && !probe.deleted_at_exists; - - if (!needsPagesBootstrap && !needsLinksBootstrap && !needsChunksBootstrap && !needsPagesDeletedAt) return; + // v0.26.3: idx_mcp_log_agent_time in SCHEMA_SQL crashes if agent_name + // doesn't exist yet. Migration v33 also adds it, but bootstrap runs first. + // We bootstrap params + error_message together because v33's UPDATE/SELECT + // statements that backfill agent_name reference all three columns. + const needsMcpLogBootstrap = probe.mcp_log_exists + && (!probe.mcp_log_agent_name_exists + || !probe.mcp_log_params_exists + || !probe.mcp_log_error_message_exists); + + if ( + !needsPagesBootstrap + && !needsLinksBootstrap + && !needsChunksBootstrap + && !needsPagesDeletedAt + && !needsMcpLogBootstrap + ) return; console.log(' Pre-v0.21 brain detected, applying forward-reference bootstrap'); @@ -263,6 +292,22 @@ export class PostgresEngine implements BrainEngine { ALTER TABLE pages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; `); } + + if (needsMcpLogBootstrap) { + // v33 (admin_dashboard_columns_v0_26_3) adds these three columns to + // mcp_request_log AND backfills agent_name via an UPDATE that joins on + // oauth_clients/access_tokens. Bootstrap only adds the column shells + // so SCHEMA_SQL's `CREATE INDEX idx_mcp_log_agent_time ON + // mcp_request_log(agent_name, ...)` doesn't crash on a v0.22.7-shape + // mcp_request_log. v33 runs later via runMigrations and is idempotent + // (its `ADD COLUMN IF NOT EXISTS` statements no-op, the backfill + // UPDATE finds NULL agent_name rows and fills them). + await conn.unsafe(` + ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS agent_name TEXT; + ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS params JSONB; + ALTER TABLE mcp_request_log ADD COLUMN IF NOT EXISTS error_message TEXT; + `); + } } async transaction(fn: (engine: BrainEngine) => Promise): Promise { diff --git a/test/e2e/postgres-bootstrap.test.ts b/test/e2e/postgres-bootstrap.test.ts index 6480fdc71..c5e39da83 100644 --- a/test/e2e/postgres-bootstrap.test.ts +++ b/test/e2e/postgres-bootstrap.test.ts @@ -90,4 +90,39 @@ describe.skipIf(skip)('PostgresEngine forward-reference bootstrap (E2E)', () => await engine.initSchema(); expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION)); }); + + test('PostgresEngine.initSchema bootstraps mcp_request_log v0.26.3 columns on a v31 brain', async () => { + // Regression for the v0.26.3 mcp_request_log forward-reference miss: + // a brain stuck at v31 has the v0.22.7-shape mcp_request_log (5 cols, no + // agent_name / params / error_message), but SCHEMA_SQL line ~420 declares + // `CREATE INDEX idx_mcp_log_agent_time ON mcp_request_log(agent_name, ...)`. + // Without bootstrap coverage, init crashes with `column "agent_name" does + // not exist` BEFORE migration v33 (admin_dashboard_columns_v0_26_3) runs. + await engine.initSchema(); + const conn = (engine as any).sql; + + // Mutate to pre-v0.26.3 mcp_request_log shape. + await conn.unsafe(` + DROP INDEX IF EXISTS idx_mcp_log_agent_time; + DROP INDEX IF EXISTS idx_mcp_log_time_agent; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS agent_name CASCADE; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS params CASCADE; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS error_message CASCADE; + `); + await engine.setConfig('version', '31'); + + // Bootstrap → SCHEMA_SQL → runMigrations chain. Must not crash. + await engine.initSchema(); + + expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION)); + + // Verify the bootstrapped columns exist after upgrade. + const cols = await conn` + SELECT column_name FROM information_schema.columns + WHERE table_schema = current_schema() + AND table_name = 'mcp_request_log' + AND column_name IN ('agent_name', 'params', 'error_message') + `; + expect(cols).toHaveLength(3); + }); }); diff --git a/test/schema-bootstrap-coverage.test.ts b/test/schema-bootstrap-coverage.test.ts index d09c0ce8d..ef335b4cb 100644 --- a/test/schema-bootstrap-coverage.test.ts +++ b/test/schema-bootstrap-coverage.test.ts @@ -63,6 +63,14 @@ const REQUIRED_BOOTSTRAP_COVERAGE: ForwardReference[] = [ // v0.26.5 — forward-referenced by `CREATE INDEX pages_deleted_at_purge_idx // ON pages (deleted_at) WHERE deleted_at IS NOT NULL`. { kind: 'column', table: 'pages', column: 'deleted_at' }, + // v0.26.3 — forward-referenced by `CREATE INDEX idx_mcp_log_agent_time + // ON mcp_request_log(agent_name, created_at DESC)`. + { kind: 'column', table: 'mcp_request_log', column: 'agent_name' }, + // v0.26.3 — admin dashboard request log columns. Not directly indexed, but + // referenced by the same v33 migration's UPDATE backfill, so the schema-blob + // replay assumes they're present alongside agent_name. + { kind: 'column', table: 'mcp_request_log', column: 'params' }, + { kind: 'column', table: 'mcp_request_log', column: 'error_message' }, ]; test('applyForwardReferenceBootstrap covers every forward reference declared in REQUIRED_BOOTSTRAP_COVERAGE', async () => { @@ -95,6 +103,12 @@ test('applyForwardReferenceBootstrap covers every forward reference declared in DROP INDEX IF EXISTS pages_deleted_at_purge_idx; ALTER TABLE pages DROP COLUMN IF EXISTS deleted_at; + + DROP INDEX IF EXISTS idx_mcp_log_agent_time; + DROP INDEX IF EXISTS idx_mcp_log_time_agent; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS agent_name; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS params; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS error_message; `); // Run bootstrap in isolation (NOT initSchema). This is what we're testing. @@ -150,6 +164,11 @@ test('after bootstrap, PGLITE_SCHEMA_SQL replays without crashing on missing for ALTER TABLE links DROP COLUMN IF EXISTS origin_page_id; DROP INDEX IF EXISTS pages_deleted_at_purge_idx; ALTER TABLE pages DROP COLUMN IF EXISTS deleted_at; + DROP INDEX IF EXISTS idx_mcp_log_agent_time; + DROP INDEX IF EXISTS idx_mcp_log_time_agent; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS agent_name; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS params; + ALTER TABLE mcp_request_log DROP COLUMN IF EXISTS error_message; `); // Bootstrap, then schema replay. Either step crashing fails the test.