Skip to content

fix(history): resolve a rollout's session_meta by thread id - #3056

Closed
ntdatt812 wants to merge 1 commit into
lidge-jun:devfrom
ntdatt812:fix/3026-forked-rollout-restore
Closed

fix(history): resolve a rollout's session_meta by thread id#3056
ntdatt812 wants to merge 1 commit into
lidge-jun:devfrom
ntdatt812:fix/3026-forked-rollout-restore

Conversation

@ntdatt812

@ntdatt812 ntdatt812 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #3026.

The pair that can never validate

A forked rollout trails its parent thread's session_meta. Three call sites folded the file with the id-agnostic readLatestSessionMeta, so all three answered with the parent's provider:

site consequence
updateSessionMeta (line 878) id mismatch → skip the file entirely
snapshotRolloutForRestore (line 552) history_backup_rollout_postimage_mismatch
assertRestoreReadback (line 631) history_backup_rollout_readback_mismatch

The reporter identified the two validators. The writer is the same defect on the write side, and it is where the unrecoverable state is created: routing flips the database row but skips the rollout, so the manifest records an entry whose file never received the OpenCodex post-image. Neither restore branch can match it, and preflightRestoreTargets is all-or-nothing — a handful of forked threads blocks every entry, ocx stop exits 1 on every invocation, and ocx update aborts at its gate. Nothing clears on retry.

Fixing only the validators does not restore a forked thread: the writer still skips, requireDurableProvider is unmet, and restore throws history_backup_rollout_unrestorable. I confirmed that by running it.

The fix

Resolve the record by id at all three sites, via a readLatestSessionMetaForId wrapper around the readLatestSessionMetaForIdFromText already used by compensateConcurrentSessionMetaAppend.

For an unforked rollout the last record is this thread's, so behaviour is byte-identical. The file's identity stays anchored by readFirstLineProviderValue / inspectFirstLineProvider, which already require line 1 to carry entry.id.

⚠️ One existing test changed its expectation

does not append when the latest session_meta belongs to a different thread id asserted files: 0 — the skip this PR removes. Its comment gives the reason: "no misleading append for a foreign id". That concern was about cloning the foreign record, and resolving by id addresses it directly: we append this thread's own metadata, which the app honors, rather than a copy of a record it would discard.

I rewrote the test rather than delete it, and it is now strictly stronger — it pins the append's id, that the original bytes remain a prefix, and that the foreign line survives unchanged exactly once:

expect(latestSessionMetaPayload(rollout)).toMatchObject({ id: "thread-1", model_provider: "opencodex" });
expect(after.split("\n").filter(Boolean).filter(line => line === foreignLine)).toHaveLength(1);

Please look at this hunk first — it is the one judgement call in the PR. If you would rather keep forked rollouts unwritable, the alternative is to stop flipping the database row for them too, so no manifest entry is ever recorded; say the word and I will send that instead.

Tests

Three new tests, plus the rewritten one:

  • restores a forked rollout that trails its parent thread's session_meta — the reported end-to-end path
  • leaves a foreign trailing session_meta untouched while restoring its own
  • consumes the manifest when a foreign-id session_meta lands after restore readback — the last-moment race, via setBeforeHistoryBackupConsumeForTests. Its same-id sibling (keeps the manifest when a newer same-id rollout provider lands after readback) still fails closed, so the pair now pins both halves of that comment.

Each site was reverted individually to confirm the tests actually hold it:

reverted site result
snapshotRolloutForRestore 2 fail
assertRestoreReadback 1 fail
updateSessionMeta 3 fail
bun test tests/codex-history-provider.test.ts        48 pass, 0 fail
bun test tests/codex-history-*.test.ts \
     tests/codex-native-residue.test.ts \
     tests/history-migration-guardian.test.ts        168 pass, 2 skip, 0 fail
bun x tsc --noEmit                                   clean

Not reproduced on macOS — I am on Windows and built the forked rollout from the fixture instead. The defect is filesystem-agnostic.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • Bug Fixes

    • Improved rollout restoration when session metadata from another thread or fork appears in the history.
    • Ensured the correct thread’s metadata is selected and restored.
    • Prevented unrelated metadata records from being incorrectly treated as integrity errors or overwritten.
  • Tests

    • Added coverage for restoring rollouts with foreign or trailing session metadata records.

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fd4bdcdb-c913-49dd-9895-34e96734db47

📥 Commits

Reviewing files that changed from the base of the PR and between 71bd7be and 25ce1ed.

📒 Files selected for processing (2)
  • src/codex/history-provider.ts
  • tests/codex-history-provider.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The history provider now selects the latest session_meta record for the expected thread ID. Restore validation and metadata updates use this lookup. Tests cover forked rollouts and foreign trailing metadata.

Changes

History metadata handling

Layer / File(s) Summary
Thread-filtered metadata integration
src/codex/history-provider.ts
The provider folds metadata from newest to oldest and selects records matching the canonical thread ID. Restore snapshot validation, restore readback validation, and updateSessionMeta use the filtered lookup.
Foreign metadata test coverage
tests/codex-history-provider.test.ts
Tests verify that foreign trailing metadata remains unchanged, forked rollouts restore successfully, and the canonical thread receives the correct metadata.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 25ce1

This localized fix updates thread-specific session metadata resolution and includes targeted tests; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: lidge-j

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: resolving rollout session_meta by thread ID. It is concise and specific.
Linked Issues check ✅ Passed The PR satisfies issue #3026 by applying ID-aware session_meta lookup in src/codex/history-provider.ts for updateSessionMeta, snapshotRolloutForRestore, and assertRestoreReadback. The tests …
Out of Scope Changes check ✅ Passed The changes are within scope. The implementation updates the writer and both restore validation paths that issue #3026 identifies, and the test changes validate the corrected forked-rollout behavior. …
Full details: Linked Issues check

Explanation

The PR satisfies issue #3026 by applying ID-aware session_meta lookup in src/codex/history-provider.ts for updateSessionMeta, snapshotRolloutForRestore, and assertRestoreReadback. The tests cover fork restoration, foreign metadata preservation, and post-readback behavior. The related has_user_event defect is explicitly separate and is not required for this PR.

Full details: Out of Scope Changes check

Explanation

The changes are within scope. The implementation updates the writer and both restore validation paths that issue #3026 identifies, and the test changes validate the corrected forked-rollout behavior. No unrelated code changes are described.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 31, 2026
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed; the review readiness checklist is complete.

Review readiness checklist

  • ✅ All CI tests are green on my local testing.
  • ✅ I pushed my PR to the latest dev commit.
  • ✅ I resolved all correct Codex and CodeRabbit findings.
  • ✅ My PR is ready for review.

4/4 boxes ticked.

This pull request is already Ready for Review.
The review-ready label marks this PR as ready; review automation runs independently.
Maintainers: @lidge-jun @Ingwannu

Hygiene

Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft August 31, 2026 08:14
@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 73 / 80

이 PR은 이슈 #3026을 고칩니다. Codex 앱에서 포크/브랜치로 만든 세션의 롤아웃 파일에는, 자기 스레드의 session_meta 뒤에 부모 스레드session_meta가 한 줄 더 붙습니다. 앱(codex-rs apply_session_meta_from_item)은 payload id가 자기 스레드 id가 아니면 그 줄을 그냥 무시합니다. 그런데 지금 dev HEAD(afb11755d)의 src/codex/history-provider.ts에는 세 곳이 id를 보지 않고 파일에서 맨 마지막 session_meta만 읽습니다. 그 세 곳은 updateSessionMeta(대략 878줄 근처, 쓰기), snapshotRolloutForRestore(대략 552줄, 복원 전 스냅샷), assertRestoreReadback(대략 631줄, 복원 후 재확인)입니다.

쓰는 쪽이 id가 안 맞으면 파일을 건너뛰고, DB의 threads.model_provider만 바꿉니다. 그러면 매니페스트에는 “이 파일에 OpenCodex post-image가 있어야 한다”는 기록이 남는데, 파일에는 그 이미지가 없습니다. 복원 검증기는 부모의 provider를 보고 history_backup_rollout_postimage_mismatch를 던집니다. preflightRestoreTargets는 매니페스트 항목을 전부 통과해야 하므로, 포크된 스레드 몇 개만 있어도 ocx stop이 매번 실패하고, ocx update는 stop이 0이 아니면 게이트에서 막힙니다. #3008처럼 “앱이 DB를 잡고 있어서 잠깐 실패”가 아니라, 재시도해도 풀리지 않는 영구 막힘입니다. 리포터는 389개 중 6개가 이런 상태였다고 했습니다.

고치는 방법은 이미 있는 readLatestSessionMetaForIdFromText를 파일 경로용으로 감싼 readLatestSessionMetaForId를 만들고, 위 세 곳을 전부 그걸로 바꾸는 것입니다. 포크가 아닌 일반 롤아웃에서는 “마지막 줄 = 자기 id”라서 바이트 단위로 같은 결과가 나와야 합니다. 파일 정체성(첫 줄이 자기 id인지)은 그대로 readFirstLineProviderValue / inspectFirstLineProvider가 잡습니다. 검증기만 고치면 쓰기가 계속 스킵해서 history_backup_rollout_unrestorable가 남으므로, 세 곳을 같이 고친 선택이 맞습니다. 테스트도 포크 복원 E2E, 부모 줄 보존, 복원 직후 외국 id 레이스 consume 세 개를 새로 넣고, 예전 “외국 id면 append 안 함” 테스트는 “자기 id로 append 한다”로 바꿔서 더 세게 잠갔습니다. 사이트별로 되돌리면 실패 개수가 갈라진다는 표도 PR 본문에 있어, 회귀 잠금이 실제로 세 경로를 가리키는지 확인하기 좋습니다.

현재 dev는 히스토리/백업 무결성과 stop·update 게이트 쪽이 계속 손보는 구간입니다. 같은 작성자의 #3040은 “stop 종료 코드 대신 프록시가 내려갔는지로 update를 게이트”하는 #3008 계열이고, 이번 PR은 #3026의 원인을 고칩니다. 둘은 보완 관계이지 중복이 아닙니다. PR은 아직 draft이고 체크리스트 네 칸이 비어 있으며, 지금은 hygiene / enforce-target / label 쪽만 초록이고 전체 테스트 스위트 결과는 아직 안 보입니다. 로컬로는 작성자가 history 관련 테스트와 tsc를 통과했다고 적었습니다.

라인 552 - snapshotRolloutForRestorereadLatestSessionMeta(id 무시)를 쓰던 자리를 readLatestSessionMetaForId(path, entry.id)로 바꿉니다. 포크 롤아웃에서 부모 provider를 post-image로 오인하던 직접 원인이라 방향이 맞습니다.
라인 631 - assertRestoreReadback도 같은 교체입니다. 읽기 쪽만 고치고 쓰기 쪽을 남겨 두면 매니페스트·파일 불일치가 다시 생기므로, 세 곳 동시 교체가 필요합니다.
라인 875 근처(PR diff의 updateSessionMeta) - 예전에는 마지막 줄 id가 다르면 파일 전체를 스킵했습니다. 이제는 자기 id 줄을 찾아 그 메타를 복제·패치합니다. “외국 레코드를 복제해 앱이 버릴 줄을 쓰지 않는다”는 예전 걱정은 id로 resolve하면 해소됩니다. 다만 자기 id session_meta가 파일에 아예 없으면 null이 되어 스킵되며, 그때도 DB만 바뀌면 같은 불일치가 남을 수 있습니다(극단 케이스).
tests/codex-history-provider.test.ts 189 근처 - 기대값을 files: 0에서 files: 1로 바꾼 판단 호출입니다. 새 어서션은 append id·prefix 보존·외국 줄 1회 유지를 잠가서, 예전 테스트보다 강합니다. 메인테이너가 “포크 롤아웃은 계속 쓰기 금지”를 원하면 이 한 곳이 갈림표입니다.
src/codex/history-provider.ts parseThreadFieldsFromRolloutText - 이번 PR 범위 밖이지만, 여전히 id 무시 last-writer-wins로 provider/source를 뽑습니다. 포크 롤아웃에서 스레드 필드를 재구성할 때 부모 id/provider를 집을 수 있는 잔여 경로입니다. #3026 복원 경로와는 별도 이슈로 남겨도 됩니다.
PR 상태 - draft + 체크리스트 미체크 + 전체 CI 미확인입니다. 내용이 좋아도 지금 바로 머지 대상은 아닙니다.

메인테이너의 판단이 필요한 지점

  • 포크 롤아웃에 자기 id 메타를 append하는 쪽(이번 PR) vs DB도 같이 안 뒤집어서 매니페스트에 안 올리는 쪽(작성자가 말한 대안) 중 무엇을 제품 정책으로 할지
  • parseThreadFieldsFromRolloutText / readThreadFieldsFromRollout의 id-agnostic fold를 같은 캠페인으로 묶을지, 후속 이슈로 분리할지
  • fix(update): gate the update on the proxy being down, not on stop's exit code #3040(update 게이트)과 이 PR을 같은 stop/update 안정화 묶음으로 순서만 정할지, 각각 독립 머지할지
  • draft 체크리스트와 전체 테스트 초록을 머지 전 필수 조건으로 둘지

너의 추천
체크리스트를 채우고 history 관련 테스트(bun test tests/codex-history-provider.test.ts 및 작성자가 적은 묶음)와 전체 PR 게이트가 초록이 되면, 테스트 기대값 변경을 받아들인 채로 dev에 머지하는 쪽이 맞습니다. #3026 원인이 세 호출 부위에 정확히 걸려 있고, 검증기만 고치면 안 된다는 설명도 일관됩니다. 머지 후 #3026을 닫고, parseThreadFieldsFromRolloutText id 스코프는 별도 이슈로 열어 두면 됩니다. #3040은 보완재이니 이 PR과 충돌하지 않습니다. draft인 동안에는 Ready로 올리기 전에 한 번 더 push·CI만 확인하면 됩니다.

이 댓글은 grok-bot이 작성했습니다

@ntdatt812
ntdatt812 marked this pull request as ready for review August 31, 2026 10:06
@github-actions
github-actions Bot marked this pull request as draft August 31, 2026 10:07
@ntdatt812
ntdatt812 force-pushed the fix/3026-forked-rollout-restore branch from 9e54b60 to 0e892fc Compare August 31, 2026 13:39
A forked or branched session appends the SOURCE thread's session_meta after
its own. codex-rs `apply_session_meta_from_item` discards any record whose
payload id is not the canonical thread id, so that trailing line is ordinary
rollout content -- but three call sites folded the file with the id-agnostic
`readLatestSessionMeta` and answered with the foreign thread's provider.

The writer skipped such a file outright, while the database row still flipped.
That pair is unrestorable: the manifest expects an OpenCodex post-image the
file never received, so `snapshotRolloutForRestore` throws
history_backup_rollout_postimage_mismatch. `preflightRestoreTargets` is
all-or-nothing, so a handful of forked threads blocks every entry, `ocx stop`
exits 1 on every invocation, and `ocx update` aborts at its gate. The state
never clears on retry.

Resolve the record by id in the writer and in both validators. The append now
carries this thread's own metadata, which the app honors, instead of cloning a
foreign record it would discard.

Fixes lidge-jun#3026
@ntdatt812
ntdatt812 force-pushed the fix/3026-forked-rollout-restore branch from 0e892fc to 25ce1ed Compare August 31, 2026 13:42
@ntdatt812
ntdatt812 marked this pull request as ready for review August 31, 2026 13:46

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved on exact head 25ce1edc8941d7821ae2fe8e489a81a1407b4586 for the ID-aware forked-rollout repair.

The three changed boundaries now agree with Codex's own fold: updateSessionMeta, restore preflight, and restore readback all select the latest session_meta for the canonical thread ID, while foreign trailing records remain byte-preserved and same-ID concurrent writes still fail closed. The focused regression suite passed locally under isolated homes (48 pass, 0 fail). Repository typecheck currently reports the same three pre-existing fetch(..., { timeout }) errors on this head and on current origin/dev, so they are not introduced by this diff.

This approval covers the first, forked-session_meta defect in #3026 only. The issue's separate has_user_event drift case remains unresolved, and parseThreadFieldsFromRolloutText still performs an ID-agnostic last-record fold in a different reconstruction path. Please avoid closing those as fixed: change Fixes #3026 to scoped wording or keep/reopen #3026 with those remaining cases (a dedicated follow-up is also fine).

Before merge, rebase the currently-behind head onto latest dev and require exact-head cross-platform CI. No merge is authorized by this review alone.

@lidge-jun

Copy link
Copy Markdown
Owner

Landed via #3103 at 6123be31f5fbfc20d1aaa7996d5423a2728fbafc.

원본 PR은 maintainer merge-train으로 이미 dev에 반영되었으므로 landed-via-maintainer로 닫습니다.

@lidge-jun lidge-jun added the landed-via-maintainer Original PR closed after landing via a maintainer merge train label Aug 31, 2026
@lidge-jun lidge-jun closed this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working landed-via-maintainer Original PR closed after landing via a maintainer merge train review-ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants