From acfb844852aa286f11deed80605fc5ca8024ba90 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Sat, 8 Aug 2026 05:42:29 +0200 Subject: [PATCH] fix(db): resolve migration version 135 numbering collision (#9745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two files both claimed migration version 135: 135_connection_runtime_state.sql (#9449, landed 2026-08-07) and 135_migrate_model_capability_max_token.sql (#8908, landed 2026-08-05). #9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50. This is not cosmetic: getMigrationFiles() throws "Migration version collision detected" the moment ANY code path first touches the database (getDbInstance() -> runMigrations()), which means a completely fresh install/deploy from this branch cannot even boot — confirmed live against a freshly built container while testing unrelated live-verification tooling. Renumbered the later-landing file to 140 (the next free slot) and added the matching isSchemaAlreadyApplied("140") retroactive guard in migrationRunner.ts, so a DB that already ran this migration under the old 135 number isn't treated as needing a fresh application. This matches the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file (also caused by the same recurring branch-before-merge numbering race). Test plan: - TDD: new tests/unit/migration-135-numbering-collision.test.ts (2/2) — spins up a hermetic fresh DB and confirms getDbInstance() applies every real on-disk migration without throwing, plus confirms both formerly-135 migrations' effects are present. Confirmed failing (reproducing the exact live crash) with the pre-fix colliding filenames restored, passing after the rename. - npm run typecheck:core — clean - npm run lint — clean - npm run check:file-size — clean (migrationRunner.ts rebaselined 1084->1094 for the new guard case) - Full migration-runner + migration-numbering test suites (64 tests across 6 files) — all pass, no regressions --- config/quality/file-size-baseline.json | 3 +- src/lib/db/migrationRunner.ts | 10 +++ ...e.sql => 140_connection_runtime_state.sql} | 0 .../migration-135-numbering-collision.test.ts | 67 +++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) rename src/lib/db/migrations/{135_connection_runtime_state.sql => 140_connection_runtime_state.sql} (100%) create mode 100644 tests/unit/migration-135-numbering-collision.test.ts diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 5c2af5ab25..e0895cdd15 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,4 +1,5 @@ { + "_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.", "_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.", "_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent’s conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR’s own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.", "_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.", @@ -268,7 +269,7 @@ "src/lib/tokenHealthCheck.ts": 1021, "src/lib/db/apiKeys.ts": 1529, "src/lib/db/core.ts": 1637, - "src/lib/db/migrationRunner.ts": 1084, + "src/lib/db/migrationRunner.ts": 1094, "src/lib/db/models.ts": 1097, "src/lib/db/providers.ts": 1034, "src/lib/memory/retrieval.ts": 1073, diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index c5470b6b1e..ba25e660a4 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -472,6 +472,16 @@ function isSchemaAlreadyApplied( return hasColumn(db, "version_manager", "auto_restart_adopted"); case "138": return hasColumn(db, "upstream_proxy_config", "fallback_backend"); + case "140": + // Retroactive guard for the connection_runtime_state migration renumbered + // 135 -> 140 (#9449 landed onto the slot already taken by #8908's + // 135_migrate_model_capability_max_token.sql — the same recurring + // numbering-race class as the 135/136 -> 137/138 renumber above). A DB + // that already ran this under the old 135 number has the table, and a + // bare CREATE TABLE re-run would otherwise just no-op (IF NOT EXISTS) + // but still burn a version-tracking slot mismatch — guard it the same + // way as the other renumbers for consistency. + return hasTable(db, "connection_runtime_state"); default: return false; } diff --git a/src/lib/db/migrations/135_connection_runtime_state.sql b/src/lib/db/migrations/140_connection_runtime_state.sql similarity index 100% rename from src/lib/db/migrations/135_connection_runtime_state.sql rename to src/lib/db/migrations/140_connection_runtime_state.sql diff --git a/tests/unit/migration-135-numbering-collision.test.ts b/tests/unit/migration-135-numbering-collision.test.ts new file mode 100644 index 0000000000..1a6474f450 --- /dev/null +++ b/tests/unit/migration-135-numbering-collision.test.ts @@ -0,0 +1,67 @@ +/** + * Regression test for a migration version-numbering collision: two files, + * 135_connection_runtime_state.sql (#9449, landed 2026-08-07) and + * 135_migrate_model_capability_max_token.sql (#8908, landed 2026-08-05), + * both claimed version "135" — #9449 branched before #8908 merged and never + * got renumbered before landing on release/v3.8.50. + * + * This is not a cosmetic issue: `getMigrationFiles()` throws + * "Migration version collision detected" the moment ANY code path first + * touches the database (getDbInstance() -> runMigrations()), which means a + * completely fresh install/deploy from this branch cannot even boot — + * confirmed live against a freshly built container. + * + * Fix: renumbered the later-landing file to 140 (the next free slot) and + * added the matching isSchemaAlreadyApplied("140") retroactive guard + * (migrationRunner.ts), matching the established pattern already used for + * the prior 135/136 -> 137/138 renumber in the same file. + */ +import { test, before, after } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-migration-135-")); +const originalDataDir = process.env.DATA_DIR; +process.env.DATA_DIR = TEST_DATA_DIR; + +let core: typeof import("../../src/lib/db/core.ts"); + +before(async () => { + core = await import("../../src/lib/db/core.ts"); +}); + +after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + if (originalDataDir === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = originalDataDir; +}); + +test("a fresh install applies all real on-disk migrations without a version collision", () => { + // getDbInstance() runs every migration file under src/lib/db/migrations/ + // against a brand-new, empty SQLite file — the exact scenario a fresh + // deploy hits. Before the fix this threw synchronously here. + assert.doesNotThrow(() => core.getDbInstance()); +}); + +test("both formerly-135 migrations' tables exist after a fresh install", () => { + const db = core.getDbInstance(); + const tableNames = ( + db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all() as Array<{ + name: string; + }> + ).map((row) => row.name); + + // From 135_connection_runtime_state.sql (renumbered to 140). + assert.ok( + tableNames.includes("connection_runtime_state"), + "connection_runtime_state table must exist" + ); + // From 135_migrate_model_capability_max_token.sql (kept at 135) — this + // migration only mutates existing model_capabilities rows, so its + // observable effect is that the migration completed, not a new table; + // provider_connections already exists as its target table. + assert.ok(tableNames.includes("provider_connections"), "sanity: base schema applied"); +});