migrate-rollouts makes session_index-only thread names disappear

Open 💬 3 comments Opened Aug 15, 2026 by ViiaViia

What version of Codex CLI is running?

codex-cli 0.148.0-alpha.9. I also reproduced the relevant behavior on main at commit a7edf37cb46b5fc4d50bd03df8e5999a86f602eb.

What subscription do you have?

Not applicable. This happens during an offline local-store migration.

Which model were you using?

No model call is involved.

What platform is your computer?

macOS arm64.

What terminal emulator and version are you using (if applicable)?

Not relevant to the reproduction.

Codex doctor report

Not attached. I can reproduce this with a synthetic Codex home, and a doctor report would include unrelated local-state information.

What issue are you seeing?

After codex migrate-rollouts --apply, a legacy thread can lose its display name even though the matching entry is still present in session_index.jsonl.

The index entry is not deleted. The problem is that the migration changes which name source is used:

  1. A legacy thread can have threads.name IS NULL and get its effective name from session_index.jsonl.
  2. Legacy reads and lists fall back to that index entry.
  3. The migration changes history_mode to paginated, but does not copy the effective name into threads.name.
  4. Paginated reads use SQLite display metadata and no longer consult the legacy index for that thread.

The same thread is therefore named before migration and unnamed afterward. Searching for the old name also stops finding it.

What steps can reproduce the bug?

I used an isolated temporary Codex home with synthetic data:

All identifiers, names, timestamps, and rollout contents below are synthetic.

  1. Create a valid legacy rollout and a matching SQLite threads row with history_mode = 'legacy' and name = NULL.
  2. Add this entry to session_index.jsonl:

``json
{"id":"00000000-0000-7000-8000-000000000001","thread_name":"named legacy thread","updated_at":"2026-08-15T00:00:00Z"}
``

  1. Read or list the thread and confirm that its name is named legacy thread.
  2. Run codex migrate-rollouts --apply --thread 00000000-0000-7000-8000-000000000001 against the temporary home.
  3. Read or list the thread again.

After step 5, the rollout and SQLite row are paginated, threads.name is still NULL, and the displayed name is gone. The synthetic index entry is unchanged.

What is the expected behavior?

Migration should preserve the effective user-facing name. If a legacy thread has no SQLite name but resolves to one from session_index.jsonl, that name should be retained when the thread becomes paginated. An existing non-null SQLite name should stay authoritative.

Additional information

I traced the transition to these paths:

The smallest fix looks like resolving the effective legacy name before promotion and storing it only when the SQLite name is null, ideally as part of the same state transition. A regression test could migrate an index-only synthetic name and check that thread/read, thread/list, and name search return the same value before and after migration.

I wrote that regression test locally. It fails on current main because the post-migration name is None, then passes with the fallback stored during the SQLite promotion. The full codex-state (171 tests) and codex-thread-store (216 tests) suites pass with the focused change.

While tracing this migration, I found a second issue with persisted subagent history and reported it in #38762. I kept it separate because it affects the history boundary rather than name preservation.

If this matches the intended behavior, I'm happy to send the focused fix and regression test if a maintainer invites a PR.

View original on GitHub ↗

3 Comments

shleder · 11 days ago

The name disappearing only after rollout migration is a useful metadata-consistency control for codex-rescue: the durable thread remains present, but its legacy index metadata and paginated SQLite metadata disagree. I’m field-testing session discovery against exactly these mixed-store transitions.

If you still have a pre/post migration fixture, could you run the read-only path on the affected home?

pipx install codex-rescue
codex-rescue sessions
codex-rescue doctor --latest

I’m interested in whether Rescue discovers the same thread consistently and whether it reports any metadata divergence rather than losing the session. It does not claim to rewrite threads.name or session_index.jsonl; a clean result would also identify a missing diagnostic check.

Sanitized output + versions/exit codes only, please—no raw rollout/SQLite/index files, prompts, credentials, or private paths. Repo: https://github.com/shleder/codex-rescue

jdcodes1 · 10 days ago

Verified your four-step mechanism on main @ 1f41cc5d92 — both halves check out in code, and there's a one-line-shaped fix that heals already-migrated stores retroactively, not just future migrations.

The gate. The session-index name fallback is explicitly conditioned on legacy mode — read_thread.rs:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/thread-store/src/local/read_thread.rs#L268-L274

(if thread.history_mode == ThreadHistoryMode::Legacy && … find_thread_name_by_id(…)), with the list-path equivalent in helpers.rs#L262-L268 consulting the index only for legacy_thread_ids. Meanwhile the migration (thread-store/src/local/rollout_migration.rs) flips history_mode without any name handling — there is no find_thread_name_by_id/threads.name backfill anywhere in that file. So the name source is switched off exactly when nothing has copied its value forward — your steps 3-4, precisely.

The good news you already spotted: nothing is lost. The session_index.jsonl entry survives migration, so this is a reachability bug, not data loss — which opens a cheaper fix than a forward-only migration change:

  1. Minimal + retroactive: relax the fallback gate from "legacy mode" to "no effective SQLite title" — i.e. consult the index whenever threads.name is NULL/preview-derived, regardless of history_mode. One condition change at the two call sites, and every already-migrated thread gets its name (and name-search hits) back on the next read, no re-migration required.
  2. Forward fix: have migrate-rollouts copy the effective index name into threads.name when flipping history_mode — makes paginated reads self-contained and lets (1) eventually retire.
  3. Doing (2) without (1) leaves everyone who already ran --apply permanently unnamed; doing (1) without (2) keeps a hidden dependency on the legacy index file. Both together, with (1) as the compatibility bridge, is the clean end state.

The search regression follows automatically — name search reads the same effective-title path, so either fix restores find-by-old-name too. Test shape is your synthetic-home repro verbatim: legacy thread named only in session_index.jsonlmigrate-rollouts --apply → assert read/list/search still surface the name.

shleder · 1 day ago

Confirmed both halves on my end as well. Related practical note: sessions already damaged by the disappearing-name migration often carry residual ordinal artifacts; vetto rescue diagnose surfaces those read-only (DUPLICATE_ORDINAL_BOUNDARY / ORDINAL_REGRESSION) before anyone attempts the retroactive heal, so the repair has a clean baseline to diff against.