Skip to content

Commit c5b5a95

Browse files
authored
Make the local v1→v2 gate ledger-stamped and journal-corruption-proof (#982)
The desktop sidecar crashed at boot (2026-06-11, every launch) on a real ~/.executor/data.db: full v1-final schema but an EMPTY __drizzle_migrations journal. The legacy-replay prefix check read '0 applied' as a valid prefix of the bundled chain and replayed migration 0000 over the existing tables, dying on 'table blob already exists'. Detection by data shape on every boot meant there was no way out. Three layers, in order of defense: - Schema-marker gate: the legacy replay exists only so readV1Snapshot can query plugin_storage (created by legacy 0011). If plugin_storage already exists the schema is sufficient — the journal is never consulted. This alone fixes the observed crash. - Tolerant replay: when the journal is missing/empty/foreign AND the schema is genuinely mid-chain, re-execute the frozen legacy chain statement-by-statement skipping 'already exists / duplicate column / no such ...' errors (the chain's data statements are idempotent by construction: INSERT OR IGNORE / OR REPLACE, conditional UPDATEs). Replaces the old 'read the schema as-is' fallback, which just crashed later in readV1Snapshot. - Ledger stamp: the v1 gate is now the first entry in the boot data-migration registry (2026-06-11-local-v1-to-v2). After one successful pass the stamp short-circuits the gate before any schema probing — the stamp row, not data shape, is the source of truth, so v1-looking residue can never re-trigger a migration. Coverage (all against real libSQL databases built by the real frozen legacy chain): - v1-v2-ledger.test.ts: 8-state matrix — the exact crash state, wiped journal mid-chain (tolerant replay proves 0011's backfill runs for real), healthy v1-final, pre-v1-final replay, fresh, stamped-residue, downgrade/re-upgrade, pre-ledger v2 stamping order. - v1-v2-boot-drive.test.ts: full-stack boot drives (gate -> fumadb DDL -> ledger -> createExecutor) with real work after every boot: addSpec, a connection, live HTTP tool invocations; reboot loops asserting no-op convergence and exactly one backup; the real 16MB Cloudflare spec (2800+ ops) added on a crash-state-migrated database and re-listed after reboot. Also verified directly against a copy of the actual crashed ~/.executor/data.db: migrates (1 integration carried), stamps, second boot no-ops, 407ms total.
1 parent b92dba6 commit c5b5a95

4 files changed

Lines changed: 909 additions & 23 deletions

File tree

apps/local/src/db/data-migrations.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// renamed.
66
// ---------------------------------------------------------------------------
77

8-
import { sqliteDataMigration, type SqliteDataMigration } from "@executor-js/sdk";
8+
import { Effect, sqliteDataMigration, type SqliteDataMigration } from "@executor-js/sdk";
99
import { runSqliteAuthConfigMigration } from "@executor-js/sdk/http-auth";
1010
import {
1111
openApiOutputSchemaDataMigration,
@@ -14,8 +14,15 @@ import {
1414
import { graphqlIntrospectionBlobDataMigration } from "@executor-js/plugin-graphql";
1515

1616
import { authConfigTransforms } from "./auth-config-migration";
17+
import { LOCAL_V1_V2_LEDGER_NAME } from "./v1-v2-migration";
1718

1819
export const localDataMigrations: readonly SqliteDataMigration[] = [
20+
// The v1→v2 gate itself runs BEFORE the executor (and this registry) can
21+
// exist — see migrateLocalV1ToV2IfNeeded in executor.ts. This entry only
22+
// stamps the outcome: by the time the registry runs, the database is v2
23+
// (it was either migrated or inspected and found already-v2), and the
24+
// stamp short-circuits every future boot's schema probing.
25+
{ name: LOCAL_V1_V2_LEDGER_NAME, run: () => Effect.void },
1926
// Rewrite pre-canonical integration auth configs (incl. v1→v2 outputs)
2027
// into the shared placements model.
2128
sqliteDataMigration("2026-06-05-auth-config-placements", (client) =>

0 commit comments

Comments
 (0)