agalwood/Motrix · critical · StaleSchemaError

legacy_table_present

legacy_table_present

Error message

The versioned database schema is incompatible with this build: legacy table `task_metadata` is still present. This unpublished build updates the canonical v1 schema directly, so an existing local development database can have a valid version marker while its substantive tables are stale.

Action: delete the database file and restart the app to recreate it on the current v1 schema.
  rm '${dbPath}'

What it means

StaleSchemaError reason 'legacy_table_present', thrown by Guard B in migrate() at line 342 when sqlite_master still contains a 'task_metadata' table. task_metadata is the pre-Plan-A legacy schema; its presence means this DB predates the Plan A rewrite. Even though schema_version may report >=1, the legacy tables would cause v2+ migrations to crash with cryptic 'no such table' errors, so this guard fails fast.

Source

Thrown at src/core/session/migrations/index.ts:343

      db
        .prepare(
          "SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_metadata'"
        )
        .get() !== undefined
    const hasTransferSchema =
      db
        .prepare(
          "SELECT 1 FROM sqlite_master WHERE type='table' AND name='transfer_totals'"
        )
        .get() !== undefined &&
      db
        .prepare(
          "SELECT 1 FROM sqlite_master WHERE type='table' AND name='transfer_buckets'"
        )
        .get() !== undefined
    const dbPath = (db as unknown as { name: string }).name
    if (hasLegacySchema) {
      throw new StaleSchemaError('legacy_table_present', dbPath)
    }
    if (!hasNewSchema) {
      throw new StaleSchemaError('new_tables_missing', dbPath)
    }
    const taskColumns = new Set(
      (
        db.prepare('PRAGMA table_info(tasks)').all() as Array<{ name: string }>
      ).map((column) => column.name)
    )
    if (
      !taskColumns.has('task_type') ||
      !taskColumns.has('finished_at') ||
      !taskColumns.has('error_message') ||
      !taskColumns.has('error_code') ||
      !taskColumns.has('error_detail_key') ||
      !taskColumns.has('error_detail_params') ||
      !taskColumns.has('diagnosis_revision')
    ) {

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Delete the DB file (path in dbPath) and restart — migrate() builds the Plan A v1 schema cleanly.
  2. If legacy data must be preserved, run a pre-Plan-A build to export, then import into the new schema after reset.
  3. Do not DROP task_metadata alone — its sibling tables and schema_version row are also legacy; full reset is the supported path.

Example fix

# before: legacy task_metadata table still present
# after
  rm '${dbPath}'
# restart; migrate() creates the Plan A canonical schema from v1
Defensive patterns

Strategy: try-catch

Type guard

function isStaleSchemaError(e: unknown): e is StaleSchemaError { return e instanceof StaleSchemaError; }

Try / catch

try {
  migrate(db);
} catch (e) {
  if (e instanceof StaleSchemaError && e.reason === 'legacy_table_present') {
    // pre-Plan-A DB detected — reset; offer data export via pre-Plan-A build if needed
  } else throw e;
}

Prevention

When it happens

Trigger: migrate() Guard B (current > 0) queries for 'task_metadata'; if present, throws at line 343. Fires before any new-table/column checks.

Common situations: Long-time user upgrading across the Plan A rewrite boundary; a dev machine with an old DB from before the rewrite; CI fixture generated pre-Plan-A never cleaned up.

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/f472e477761cb986. Report an issue: GitHub.