Add rollout migration tooling and background migration (#37348)

## What changed

- Add `codex migrate-rollouts` with dry-run inspection by default, explicit `--apply`, thread filtering, optional I/O throttling, progress output, and JSON or verbose reports.
- Add the disabled-by-default `background_paginated_rollout_migration` feature to migrate legacy local sessions at startup before rollout compression begins.
- Persist a migration cursor and skipped-file fingerprints so later startups avoid full rescans while retrying changed files and recovering pending migrations.
- Coordinate migration with live writers and emit metrics for manual and startup runs.

## Testing

- Cover startup cursor advancement and lookback, pending migration recovery, live-writer coordination, changed empty rollouts, and progress reporting.

GitOrigin-RevId: 276ac506c50ebec5140fd319faca1bb998172061
This commit is contained in:
Owen Lin
2026-08-07 01:26:42 +00:00
committed by copyberry
parent 69b6152c17
commit 4bb7ee3472
21 changed files with 1569 additions and 24 deletions
+4
View File
@@ -3,6 +3,7 @@ mod graph;
mod log;
mod memories;
mod queued_item;
mod rollout_migration_state;
mod thread_goal;
mod thread_metadata;
@@ -18,6 +19,9 @@ pub use memories::Stage1JobClaimOutcome;
pub use memories::Stage1Output;
pub use memories::Stage1StartupClaimParams;
pub use queued_item::QueuedUserSubmissionRecord;
pub use rollout_migration_state::RolloutMigrationCursor;
pub use rollout_migration_state::RolloutMigrationSkippedRollout;
pub use rollout_migration_state::RolloutMigrationState;
pub use thread_goal::ThreadGoal;
pub use thread_goal::ThreadGoalStatus;
pub use thread_metadata::Anchor;
@@ -0,0 +1,67 @@
//! SQLite-backed bookkeeping types shared by rollout migrations.
//!
//! A migration cursor says “everything through this creation-ordered thread was checked.” A
//! skipped rollout stores enough file fingerprint information to tell whether a previously
//! unmigratable rollout is still unchanged.
//!
//! These types are intentionally generic so future rollout migrations can reuse the same state
//! shape without depending on legacy -> paginated policy.
use anyhow::Result;
use sqlx::Row;
use sqlx::sqlite::SqliteRow;
/// Creation-ordered thread frontier checked by one rollout migration.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RolloutMigrationCursor {
pub thread_created_at: i64,
pub thread_id: String,
}
/// Persisted progress for one rollout migration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RolloutMigrationState {
pub last_checked_thread: Option<RolloutMigrationCursor>,
}
impl RolloutMigrationState {
pub(crate) fn try_from_row(row: &SqliteRow) -> Result<Self> {
let thread_created_at = row.try_get("last_checked_thread_created_at")?;
let thread_id = row.try_get("last_checked_thread_id")?;
let last_checked_thread = match (thread_created_at, thread_id) {
(Some(thread_created_at), Some(thread_id)) => Some(RolloutMigrationCursor {
thread_created_at,
thread_id,
}),
(None, None) => None,
_ => {
return Err(anyhow::anyhow!(
"rollout migration state has incomplete last checked thread"
));
}
};
Ok(Self {
last_checked_thread,
})
}
}
/// An unchanged rollout that one migration can safely skip.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RolloutMigrationSkippedRollout {
pub rollout_path: String,
pub rollout_size_bytes: i64,
pub rollout_modified_at_ns: i64,
pub skip_reason: String,
}
impl RolloutMigrationSkippedRollout {
pub(crate) fn try_from_row(row: &SqliteRow) -> Result<Self> {
Ok(Self {
rollout_path: row.try_get("rollout_path")?,
rollout_size_bytes: row.try_get("rollout_size_bytes")?,
rollout_modified_at_ns: row.try_get("rollout_modified_at_ns")?,
skip_reason: row.try_get("skip_reason")?,
})
}
}