Skip to content

migrate-rollouts makes session_index-only thread names disappear #38761

Open

Description

@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:

    {"id":"00000000-0000-7000-8000-000000000001","thread_name":"named legacy thread","updated_at":"2026-08-15T00:00:00Z"}
  3. Read or list the thread and confirm that its name is named legacy thread.

  4. Run codex migrate-rollouts --apply --thread 00000000-0000-7000-8000-000000000001 against the temporary home.

  5. 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.

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on Aug 15, 2026
  2. jdcodes1 commented on Aug 18, 2026

    @jdcodes1

    Verified your four-step mechanism on main @ 1f41cc5 — 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:

    if thread.history_mode == ThreadHistoryMode::Legacy
    && let Ok(Some(name)) =
    find_thread_name_by_id(store.config.codex_home.as_path(), &thread.thread_id).await
    && !name.trim().is_empty()
    {
    set_thread_name(&mut thread, name);
    }

    (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.jsonl → migrate-rollouts --apply → assert read/list/search still surface the name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIbugSomething isn't workingsessionIssues involving session (thread) management, resuming, forking, naming, archiving

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions