migrate-rollouts leaves persisted subagent threads with empty projected history

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 rollout fixture, and a doctor report would include unrelated local-state information.

What issue are you seeing?

After migrating a persisted legacy subagent, its raw rollout can still contain history while thread/read and resume surfaces show an empty thread.

For a migrated subagent, the migration currently writes subagent_history_start_ordinal equal to expected_ordinal, which is the end of the rewritten rollout. Projection then hides every record below that boundary. Since no existing record is at or beyond the end-of-file boundary, none of the migrated history is exposed as thread turns or items.

This is easy to notice with a named or otherwise persisted delegation thread that is still visible in thread/list: the thread exists and its rollout is non-empty, but opening it shows no previous turns.

What steps can reproduce the bug?

I used an isolated temporary Codex home and a synthetic legacy subagent:

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

  1. Create a legacy rollout with SessionSource::SubAgent, a compacted checkpoint, and one child-local user/assistant turn.
  2. Make the thread persisted and visible to thread/list.
  3. Run codex migrate-rollouts --apply --thread 00000000-0000-7000-8000-000000000002 against the temporary home.
  4. Inspect the migrated session metadata, then read or resume the thread through the app-server.

The migrated metadata has subagent_history_start_ordinal == number of rollout lines. The retained rollout is non-empty, but the projected thread has no active turns.

What is the expected behavior?

Migration should keep inherited or bootstrap context hidden where necessary, while preserving the subagent's own displayable turns for thread/read and thread/resume.

The boundary should point to the first displayable child-local record instead of always pointing to the end of the rewritten file. If ephemeral workers and persisted user-facing subagents intentionally need different behavior, the migration should distinguish those cases.

Additional information

I traced the behavior to these paths:

That test makes the migration-layer behavior clear, but I am not sure it captures the expected app-server behavior for a persisted thread that a user can reopen. A focused regression test could migrate a named synthetic subagent, exercise thread/read or thread/resume, and verify that its child-local turn remains visible while inherited context stays hidden.

I found this while looking at the same migration path as #38761, but kept it in a separate report because this issue is about subagent history projection rather than legacy name preservation.

If this is not the intended behavior for persisted subagents, I'm happy to work on the focused fix and regression test if a maintainer invites a PR.

View original on GitHub ↗

3 Comments

shleder · 11 days ago

This is a direct persisted-history projection case for codex-rescue: the migrated rollout still contains child-local history, while the projected thread appears empty. I’m field-testing whether Rescue can distinguish durable rollout contents from broken/empty derived presentation without rewriting the source.

If you still have an affected migrated thread or a disposable synthetic fixture, could you run:

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

I’m specifically interested in whether discovery sees the non-empty rollout and how doctor classifies the projection/history-boundary mismatch. If it reports healthy because this projection layer is currently outside its checks, that is useful gap evidence too—no repair claim implied.

Please share only sanitized output, versions and exit codes. No raw rollout/SQLite, child payloads, prompts, credentials, or private paths. Repo: https://github.com/shleder/codex-rescue

jdcodes1 · 10 days ago

Verified on main @ 1f41cc5d92, and the live-creation path shows exactly what the migration should have written — the bug is a conflation of two different ordinals.

The intended semantics. When a live subagent thread is created, subagent_history_start_ordinal is set to the inherited-prefix length + 1 — the ordinal where the subagent's own visible history begins, just past the parent context it inherited:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/thread-store/src/live_thread.rs#L115-L131

What the migration writes instead. For a migrated subagent, the boundary is set to expected_ordinal — the value returned by writing the rollout, i.e. the ordinal at the end of the entire rewritten file:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/thread-store/src/local/rollout_migration.rs#L553-L561

with rewrite_subagent_history_boundary stamping that value verbatim into the head SessionMeta (rollout_migration/publish.rs#L157-L179). Since projection hides everything below the boundary and no record sits at or past end-of-file, the migrated thread projects as empty — your report, mechanically.

Fix. The correct boundary is available at the same call site: select_bounded_context produces the inherited-context prefix that write_bounded_subagent_rollout writes first, so the boundary should be that prefix's item count + 1 (mirroring the live-creation formula), not the end-of-file ordinal. One value swap in the RolloutMigrationKind::Subagent branch.

Retroactive repair is possible. As you note, the raw rewritten rollout still contains the full history — only the head record's boundary is wrong. A repair pass (or a check in migrate-rollouts re-runs) can recompute the boundary with the corrected rule and rewrite the head line, restoring already-migrated subagent threads without data loss — same recoverability situation as the name regression in #38761, and worth shipping together since both are migrate-rollouts --apply artifacts users can't fix themselves.

Test shape: migrate a synthetic persisted subagent with N inherited-context items and M own-history records; assert the stamped boundary equals N+1 and thread/read projects exactly M records — plus the repair-pass variant against a store already migrated with the end-of-file boundary.

shleder · 1 day ago

Verified mechanism matches what I see locally: two ordinal notions conflated during live creation. One addition for affected users: post-migration damage from this conflation is detectable from the rollout alone - vetto rescue diagnose <file> flags duplicate/regressed persisted ordinals read-only, which helps separate threads that can be repaired-by-resync from ones with genuinely missing history.