deer-flow/docs/database-forward-revision-recovery.md
RongJie G 06c827903a
feat(persistence): add expand-phase thread incarnation storage (#5216)
* feat(persistence): expand thread incarnation storage

Add nullable thread and MCP task incarnation columns while preserving mixed-version writes. New thread records receive stable incarnation IDs, and new task rows copy the matching owned or shared thread incarnation without changing any read, claim, session, or deletion behavior.

* test(persistence): pin incarnation rollback compatibility

* test(api): pin internal thread response boundary

* fix(persistence): rebase incarnation rollout after projects

---------

Co-authored-by: CorgiBoyG <CorgiBoyG@users.noreply.github.com>
2026-09-09 15:52:09 +08:00

4.2 KiB

Recovering the original thread-incarnation database revision

The Projects build requires projects and threads_meta.project_id. An older deployment may have stamped 0019_thread_incarnations on a database containing only 0018_oauth_identity_pg_partial plus two nullable VARCHAR(32) columns: threads_meta.incarnation and mcp_tasks.thread_incarnation. That shape cannot serve this build's repositories. Startup now rejects it without changing the schema or revision, and reports the missing tables/columns.

Normal databases on the known migration chain in this tree upgrade automatically. The procedure below is only for the exact original incarnation rollout shape. Current and future builds that know the reused 0019_thread_incarnations id first check the fixed canonical-0019 table/column floor, then upgrade to their local head. The published 0020 rollback binary does not use this fixed snapshot: it validates its own ORM floor before skipping the unknown revision. Current tests remove 0019 from the mocked local revision set to exercise the same unknown-revision path, but they still run the current fixed-floor validator. Because the fixed floor is not derived from current ORM metadata, future columns are not required before their migration runs.

Offline migration

  1. Stop every Gateway, scheduler, and other process writing to the database. Take a restorable database backup and rehearse these steps on a copy.

  2. Verify there is exactly one alembic_version row, containing 0019_thread_incarnations. Inspect the owning deployment's migration and actual database schema: it must be the local 0018 schema plus only the two nullable columns above, with no added defaults, constraints, tables, indexes, or data backfills. Neither projects nor threads_meta.project_id may already exist for this recovery path. If the shape differs, use a migration reviewed for that deployment; do not use the commands below.

  3. From this checkout's backend/, set DEERFLOW_RECOVERY_DATABASE_URL to the target async SQLAlchemy URL (sqlite+aiosqlite:////absolute/path/database.db or postgresql+asyncpg://…). For Postgres, also set DEERFLOW_RECOVERY_POSTGRES_SCHEMA to the configured application schema, if one is used. Keep credentials out of shell history.

  4. Rebase the version marker to the verified common parent and run the normal migrations. purge=True replaces the version row, not application data; it is necessary because the same revision id was previously deployed with a different parent.

    uv run python - <<'PY'
    import asyncio
    import os
    
    from alembic import command
    from sqlalchemy.ext.asyncio import create_async_engine
    
    from deerflow.persistence.bootstrap import _get_alembic_config
    
    engine = create_async_engine(os.environ["DEERFLOW_RECOVERY_DATABASE_URL"])
    cfg = _get_alembic_config(
        engine,
        postgres_schema=os.environ.get("DEERFLOW_RECOVERY_POSTGRES_SCHEMA", ""),
    )
    command.stamp(cfg, "0018_oauth_identity_pg_partial", purge=True)
    command.upgrade(cfg, "head")
    asyncio.run(engine.dispose())
    PY
    

    This applies 0019_projects, 0020_threads_meta_project_id, 0021_batch_acceptance, and the idempotent 0019_thread_incarnations head, preserving the two incarnation columns and their existing values. Do not stamp directly to head: that would skip the Projects and batch-acceptance DDL and reproduce the missing-schema failure.

  5. Confirm the version is 0019_thread_incarnations, the project table and membership column/index exist, and existing incarnation values are retained. Start this build, verify existing conversations load and a new conversation can be created, then resume service. Rolling back is supported only to the audited 0020_threads_meta_project_id compatibility build.

Bootstrap never performs this re-stamp itself. The regression in backend/tests/test_persistence_forward_revision_compat.py constructs the original schema, verifies startup rejection, and exercises the recovery while checking thread reads/inserts and preservation of incarnation data. The incarnation migration chains from 0021_batch_acceptance and handles these already-present nullable columns idempotently.