migrate-rollouts leaves persisted subagent threads with empty projected history
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.
- Create a legacy rollout with
SessionSource::SubAgent, a compacted checkpoint, and one child-local user/assistant turn. - Make the thread persisted and visible to
thread/list. - Run
codex migrate-rollouts --apply --thread 00000000-0000-7000-8000-000000000002against the temporary home. - 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:
- The migration passes its final
expected_ordinaltorewrite_subagent_history_boundary. - That helper stores the value as
subagent_history_start_ordinal. - Materialization suppresses records below the boundary (source).
- The current migration test asserts both
boundary == lines.len()and an empty projected turn list (test).
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.
3 Comments
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:
I’m specifically interested in whether discovery sees the non-empty rollout and how
doctorclassifies 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
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_ordinalis 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_boundarystamping that value verbatim into the headSessionMeta(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_contextproduces the inherited-context prefix thatwrite_bounded_subagent_rolloutwrites 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 theRolloutMigrationKind::Subagentbranch.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-rolloutsre-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 aremigrate-rollouts --applyartifacts 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/readprojects exactly M records — plus the repair-pass variant against a store already migrated with the end-of-file boundary.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.