Skip to content

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

Merged
lidge-jun merged 9 commits into
devfrom
codex/3026-forked-rollout-restore
Aug 31, 2026
Merged

fix(history): resolve a rollout's session_meta by thread id (#3026)#3103
lidge-jun merged 9 commits into
devfrom
codex/3026-forked-rollout-restore

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #3026. Carries @ntdat812's 0af9c0fd5 from #3056 for the id-aware half, then adds the second half the issue reports.

The id-aware half. readLatestSessionMeta returned the last session_meta line regardless of whose thread id it carried. A forked rollout has more than one, so restore read a sibling thread's metadata, rejected the entry, and one bad entry fails the whole manifest — wedging every ocx stop and ocx update. The reporter measured 6 of 389 entries affected.

The has_user_event half. That field has two writers. Routing derives the post-image value from first_user_message at snapshot time; restore recomputed it from the message as it stands now. A row routed while its message was null, then given a first message by the user, looked exactly like OpenCodex's own write — and restore put it back to 0.

The manifest entry now records whether the message was non-empty at snapshot time. A boolean only, never the text: the manifest is a file on disk and the message is user content. Ownership is then decided by which tuple the row wears and a per-entry relabel marker written before the routing write and resolved after it, so a crash between the two leaves an honest unknown that the observed row resolves.

Some states genuinely cannot be decided — an OpenAI-origin entry wearing its original tuple with 0 → 1 drift, whose route would itself have written a 1, is reachable both by "routing never landed and the user typed" and by "routing landed and legacy recovery pulled it back". Those refuse, which is what dev already does there. A guess would either erase real activity or fabricate it, and the refusal now says so rather than reporting a generic integrity failure.

Manifests written before these fields exist keep the current-row reading and restore exactly as they do today. Refusing them would brick the population this fix exists to repair.

Seven adversarial review rounds, findings 3 → 2 → 1 → 1 → 2 → 2 → 0. They caught, among others: a stale committed marker surviving into a new attempt, a regression that never reached the code it claimed to test, and an over-broad refusal that rejected a decidable case.

Plan: devlog/_plan/260831_prio70_train_round2/030_wp3_forked_rollout_restore.md.

Verification

bun test tests/codex-history-provider.test.ts tests/codex-history-job.test.ts
  -> 78 pass / 0 fail / 319 expect()
bun run typecheck -> exit 0

A sixteen-row classifier matrix covers every cell, plus end-to-end regressions for the null-then-typed history, the route-then-legacy-recovery history, a surviving manifest re-snapshotted by the next attempt, the ambiguous reroute refusing, and a legacy v1 manifest restoring unchanged. Each behavioral regression was driven red first.

Checklist

  • Targets dev
  • Behavior change carries focused regression tests, each driven red first
  • No user-facing surface changed, so no docs-site/ update is required
  • No credentials, request bodies, or account identifiers added
  • No GUI change, so no screenshot applies

Summary by CodeRabbit

  • Bug Fixes

    • Improved history restore and rerouting reliability, including safer handling of ambiguous or incomplete routing states.
    • Preserved user-event status more accurately during restores, including legacy history scenarios.
    • Ensured session metadata is read from the correct thread.
    • Added clearer operator messages when history integrity checks prevent a transition.
  • Improvements

    • Enhanced backup manifest validation and compatibility with newer restore metadata.
    • Added integrity details to history failure results for more actionable troubleshooting.

ntdatt812 and others added 8 commits September 1, 2026 02:14
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 #3026
Carries @ntdat812's 25ce1ed for the id-aware half of #3026 and adds the
second half the issue reports.

has_user_event has two writers, so a final state cannot establish who wrote
it. Routing derives the post-image value from first_user_message AT SNAPSHOT
TIME, but restore recomputed it from the message as it is now. A row routed
while its message was null, then given a first message by the user, looked
exactly like OpenCodex's own write - and restore put it back to 0.

The manifest entry now records whether the message was non-empty when the
snapshot was taken. A boolean only, never the text: the manifest is a file on
disk and the message is user content.

Ownership is decided by which tuple the row wears. The recorded original and
the expected post-image are unambiguous. Drift from 0 to 1 on the routed
tuple is the user's, because a row wearing that tuple was written by
OpenCodex and anything on top of it followed. Drift on the original tuple is
decided by a per-entry relabel marker written before the routing write and
resolved after it, so a crash between the two leaves an honest unknown that
the observed row resolves.

One cell genuinely cannot be decided: an OpenAI-origin entry whose routing
would have produced a 1, wearing the original tuple, with the marker still
pending. Routing-never-landed-plus-activity and routing-landed-then-legacy-
recovery produce an identical row and nothing durable separates them. That
refuses, which is what dev already does there. A guess would either erase
real activity or fabricate it.

Manifests written before these fields exist keep the current-row reading and
restore exactly as they do today - refusing them would brick the population
the fix exists to repair.
Review of the has_user_event work found three holes.

A surviving manifest means a previous route/restore cycle did not consume it,
and its relabel marker describes that attempt. Carrying a stale "committed"
into a new routing attempt let a later restore treat it as proof that
OpenCodex authored an event flag the user had since set. Every new attempt
now reopens the marker as pending; the recorded snapshot itself stays, since
it is the original provenance.

Manifest v2 existed only in the type. The validator still rejected version 2
and the writer still emitted version 1, so the new fields were being written
into a manifest that claimed not to have them. The validator now accepts 1
and 2 and checks both optional fields when present - an unvalidated
hadFirstUserMessage: "false" is truthy and would have selected the wrong
restore verdict.

The classifier's D branch matched any opencodex row, so C and D both matched
an exec-origin row and D won only by being tested first. D is now the tuple
routing actually produces: routeOpenai keeps the source, routeExec moves exec
to cli.

The plan asked for a state-transition matrix and I had shipped two cases.
There are now sixteen, one per cell, and writing them found the exec-origin
post-image case where routeExec always writes 1 - so that row is B, not D.
Review found the reopen fix incomplete and its regression vacuous.

The regression let an ordinary restore consume the manifest, so the second
route saw no existing entry and the reopen code never ran. It now forces the
consume to fail with setBeforeHistoryBackupConsumeForTests, which is the only
way a manifest actually survives into a second routing attempt.

That exposed the real gap. Reopening the marker was not enough: the entry
also carries hadFirstUserMessage and hasUserEvent, and both described the
PREVIOUS attempt. The stale message flag made the new routed row match the
expected post-image, and the stale event value restored the thread to a state
two events old - a restore that had already happened, plus whatever the user
did afterwards. Both are re-recorded for the new attempt. The provenance
tuple stays, because it is the original and a routed row must never overwrite
it; the event baseline is only refreshed when the row is back at that tuple.

Manifest promotion was also incomplete: only a brand-new manifest started at
version 2, so an existing v1 kept writing the new fields under a schema that
did not declare them. Any manifest written forward is now v2.

The regression asserts the whole sequence: route, failed consume, user types,
route again, restore - and the user's first message survives. Red without the
refreshed baseline.
…line

Third review round found the baseline refresh resting on the same inference
the classifier already refuses: tuple equality does not establish ownership.
Route to opencodex, legacy recovery back to openai, and the row wears the
original tuple with a 1 that OpenCodex wrote - refreshing there adopts that
as the user's baseline and the next restore keeps an event the user never
generated.

So the refresh now requires proof, and the proof is a relabel marker reading
"none". Nothing wrote that value before; a restore that lands and passes its
readback now records it on the surviving manifest, which is exactly the state
where a later attempt needs to tell "already restored" from "still
mid-route". Absent the proof the recorded baseline is kept: one event stale
still restores to a state the user was in, while a wrong refresh silently
rewrites what OpenCodex owns.

The write happens after finalization fails rather than before consuming it -
rewriting the manifest ahead of the consume changes the fingerprint the
consume checks, which broke twelve tests when I tried it that way.

Adds the legacy-return row to the classifier matrix, which is the history the
audits kept circling: committed marker, expected-event-1 route, original
tuple, restores to 0.
Fourth review round: the "none" proof is best-effort, so a failed write
leaves the entry reading committed while the row has drifted. Keeping the
recorded baseline erases the user's event; refreshing it preserves one
OpenCodex authored. That is another undecidable cell, and the reviewer was
right that neither reading is the safe default.

A reroute in that state now refuses with history_apply_ambiguous_reroute
rather than committing to an interpretation. Nothing is written, the manifest
stays for a human, and the refusal surfaces as an integrity failure with zero
rows applied.

Adds the end-to-end legacy-return regression the reviewer asked for: route
writes opencodex/vscode/1, legacy recovery pulls it back to openai/vscode/1,
and the row now wears its original tuple carrying an event OpenCodex wrote.
The classifier row alone could not express this history - it needed the real
route and the real recovery. The assertion is the contract rather than one
implementation: the 1 must not survive as the user's, so either restore
returns it to 0 or the layer refuses and keeps the manifest.

Drops the duplicate classifier row that added no coverage over its neighbour.
Fifth review round found the refusal too broad and the legacy-return test
green for the wrong reason.

The guard rejected every unproven event mismatch, including the case where
the previous route would have written 0 - there an observed 1 can only be the
user's, which is the expected-event-0 cell the classifier deliberately
preserves. Refusal is now conditioned on whether the prior route could itself
have authored the difference.

The end-to-end legacy-return test passed because the fixture's rollout omits
source, so restore refused during rollout preflight before the classifier
mattered - a broken classifier would have passed it too. It now writes the
matching session_meta so the real path runs, and asserts the exact outcome:
the 1 returns to 0 and the manifest is consumed.

Two smaller things: leftover debug logging is gone, and the specific
integrity code now travels with the result. failureReason: "integrity" alone
reads as "retry or run doctor", and history_apply_ambiguous_reroute is not
retryable - it means two histories produced the same row and the manifest
needs a human.
… reason out

Sixth review round found the reroute guard collapsed to a single flag and the
integrity code stopping at the worker boundary.

The condition now names all five cells. A 1 to 0 drift is always foreign -
nothing in this system clears the flag, so a baseline that moved down is a
decision the manifest does not own. A 0 to 1 drift is the user's when the
entry is exec-origin (routeExec moves source to cli and legacy recovery does
not move it back, so it was never routed away and back), when a "none" marker
proves the previous relabel was undone, or when the previous route would have
written 0. It refuses when the previous route would have written 1, and when
a legacy entry records nothing about it - undefined is not false.

integrityCode existed on the sync result and went nowhere: HistoryWorkerResult
and CodexHistoryJobOutcome had no field for it and classifyWorkerResult
dropped it, so an operator still read "failed integrity checks; run doctor".
It is typed and forwarded through both, and an ambiguous reroute now says
what it is - the manifest cannot prove whether an earlier relabel was undone,
nothing changed, resolve it manually rather than retrying. Retrying reaches
the same refusal.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 31, 2026 18:34
@github-actions github-actions Bot added the bug Something isn't working label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T18:38:21.929372Z 30b1aa4 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 81704c00-ae2f-4a98-823b-84488d588469

📥 Commits

Reviewing files that changed from the base of the PR and between 9d122dd and 009e847.

📒 Files selected for processing (7)
  • devlog/_plan/260831_prio70_train_round2/030_wp3_forked_rollout_restore.md
  • src/codex/history-job.ts
  • src/codex/history-manifest.ts
  • src/codex/history-provider.ts
  • src/codex/history-worker.ts
  • tests/codex-history-job.test.ts
  • tests/codex-history-provider.test.ts

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


📝 Walkthrough

Walkthrough

The change adds manifest provenance fields, classifies restore-time event drift, scopes rollout metadata reads by thread ID, preserves post-routing user activity, and propagates specific integrity codes to job failure messages. Tests cover relabel states, legacy manifests, forked rollouts, and ambiguous reroutes.

Changes

History integrity flow

Layer / File(s) Summary
Manifest provenance and routing snapshots
src/codex/history-manifest.ts, src/codex/history-provider.ts, tests/codex-history-provider.test.ts
Version 2 manifests record hadFirstUserMessage and relabel. Routing refreshes surviving entries, marks relabels as pending, commits successful writes, and resets markers after restore finalization failure. Validation accepts version 1 and version 2 manifests.
Restore classification and fork metadata
src/codex/history-provider.ts, tests/codex-history-provider.test.ts
restoredUserEventFor classifies original, routed, and drifted row shapes. Restore preserves later user activity. Session metadata reads and updates select the latest record for the thread ID, including forked rollouts with foreign trailing records.
Integrity error propagation and guidance
src/codex/history-provider.ts, src/codex/history-worker.ts, src/codex/history-job.ts, tests/codex-history-job.test.ts, tests/codex-history-provider.test.ts, devlog/_plan/260831_prio70_train_round2/030_wp3_forked_rollout_restore.md
Integrity results carry specific codes through the worker and job outcome. Ambiguous reroutes receive manual-resolution messaging. Tests verify the new error code and operator guidance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 009e8

The change correctly restores history by thread and preserves snapshot-time user activity, but an integrity failure during restore finalization can leave provenance metadata unresolved after restoration has already occurred, potentially blocking later history operations. The PR is mergeable with explicit owner awareness and follow-up for this bounded recovery-state risk.

Sequence Diagram(s)

sequenceDiagram
  participant HistoryJob
  participant HistoryWorker
  participant HistoryProvider
  participant BackupManifest
  participant StateDatabase
  participant Rollout
  HistoryJob->>HistoryWorker: run history transition
  HistoryWorker->>HistoryProvider: apply or restore history
  HistoryProvider->>BackupManifest: read provenance and relabel state
  HistoryProvider->>StateDatabase: classify and update thread rows
  HistoryProvider->>Rollout: read session_meta for thread ID
  HistoryProvider-->>HistoryWorker: success or integrityCode
  HistoryWorker-->>HistoryJob: failed outcome with historyIntegrityCode
Loading

Suggested reviewers: ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.84% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 6 files. (1 skipped: … 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 accurately identifies the primary history-restore fix: resolving rollout session_meta by thread ID. It is concise, specific, and directly related to the changes in src/codex/history-provider…
Linked Issues check ✅ Passed The pull request addresses both coding defects in issue #3026. It resolves session_meta by thread ID during snapshot and readback, preserves has_user_event state using snapshot metadata and relabel tr…
Out of Scope Changes check ✅ Passed The changes remain within the linked issue scope. The manifest schema updates, integrity-code propagation, restore classification, marker handling, operator messaging, documentation, and tests directl…
Full details: Title check

Explanation

The title accurately identifies the primary history-restore fix: resolving rollout session_meta by thread ID. It is concise, specific, and directly related to the changes in src/codex/history-provider.ts and issue #3026.

Full details: Linked Issues check

Explanation

The pull request addresses both coding defects in issue #3026. It resolves session_meta by thread ID during snapshot and readback, preserves has_user_event state using snapshot metadata and relabel tracking, refuses ambiguous reroutes, supports legacy manifests, and adds regression coverage.

Full details: Out of Scope Changes check

Explanation

The changes remain within the linked issue scope. The manifest schema updates, integrity-code propagation, restore classification, marker handling, operator messaging, documentation, and tests directly support forked-rollout restore reliability and has_user_event preservation.

Full details: Docstring Coverage

Explanation

Docstring coverage is 36.84% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 6 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/3026-forked-rollout-restore

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.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 75 / 80

이 PR은 지금 dev 에 남은 round-2 70대 구멍 중 하나인 #3026을 닫습니다. 포크된 롤아웃이 히스토리 복구를 한 줄 때문에 통째로 막고, ocx stopocx update 가 영원히 1로 끝나는 버그입니다. 지금 HEAD는 9d122ddb1 이고 방금 올라간 것은 제공자 마크 유닛을 닫는 문서(#3101)입니다. 패키지는 2.39.0 입니다. 런타임 쪽 남은 round-2는 이 이슈와 #3029 소진된 5시간 풀 선택, #3008 히스토리만 있는 stop abort, #3019 WHAM 401 우회입니다. 베이스는 dev, 헤드 브랜치는 codex/3026-forked-rollout-restore @ 009e84733, 라벨은 bug 입니다. 생산 파일 네 개와 테스트 두 개, 계획 문서 한 개입니다. +623 / -29.

첫 번째 구멍은 지금 HEAD에 그대로 있습니다. src/codex/history-provider.tsreadLatestSessionMeta 는 파일의 마지막 session_meta 줄을 아이디와 상관없이 돌려줍니다(HEAD 663-678줄). 같은 파일 680-688줄에는 이미 아이디를 보는 readLatestSessionMetaForIdFromText 가 있는데, 복구 길은 그걸 쓰지 않습니다. 포크/브랜치 롤아웃은 자기 줄 뒤에 부모 스레드의 session_meta 를 붙입니다. 앱(codex-rs apply_session_meta_from_item)은 아이디가 다른 줄을 버립니다. 그런데 HEAD의 snapshotRolloutForRestore (552줄), assertRestoreReadback (631줄), updateSessionMeta (878줄) 세 곳은 전부 아이디를 무시하는 함수를 부릅니다. 두 매처 rolloutMatchesRestoreTuple / rolloutMatchesExpectedPostImagepayload.id === entry.id 를 요구하므로, 맨 끝 줄이 부모면 항상 거절됩니다. 거절 코드는 history_backup_rollout_postimage_mismatch 입니다. preflightRestoreTargets (HEAD 586-596줄) 는 항목을 하나라도 거절하면 매니페스트 전체를 멈춥니다. 그래서 389개 중 6개만 포크여도 ocx stop 이 매번 1이고, ocx updatesrc/update/index.tsstop.status !== 0 게이트에서 멈춥니다. #3008(앱이 DB를 잡고 있는 잠깐 실패)과 달리 재시도해도 풀리지 않습니다.

작성 길도 같은 구멍입니다. HEAD updateSessionMeta 는 마지막 줄의 아이디가 다르면 파일을 건너뜁니다(887-889줄). DB 행은 opencodex 로 뒤집히고 롤아웃은 그대로입니다. 지금 HEAD 테스트 does not append when the latest session_meta belongs to a different thread id (tests/codex-history-provider.test.ts 192-210줄) 가 그 동작을 files: 0 으로 잠가 두었습니다. 그게 바로 복구 불가능한 짝입니다. 이번 PR은 그 테스트를 appends this thread's own session_meta when the latest one belongs to a different thread id 로 바꿉니다. 자기 스레드의 메타를 붙이고, 부모 줄은 그대로 둡니다.

두 번째 구멍도 HEAD에 있습니다. has_user_event 의 작성자가 둘입니다. 라우팅 routeOpenai 는 스냅샷 당시 first_user_message 로 포스트이미지를 만듭니다(HEAD 1184줄 hasFirstUserMessage(row.first_user_message) ? 1 : row.has_user_event). 복구의 rowMatchesExpectedPostImage 는 지금 행의 메시지로 다시 계산합니다(HEAD 487-489줄). 메시지가 비어 있는 채로 라우팅된 뒤 사용자가 첫 글을 쓰면 행은 opencodex / vscode / 1 이 되고, 헬퍼는 그걸 OpenCodex가 쓴 포스트이미지라고 본 다음 매니페스트의 0으로 되돌립니다. OpenAI 원본인데 릴레이블하지 않은 채 사용자만 글을 쓴 행은 포스트이미지 분기도 타지 못해 history_backup_postimage_mismatch 로 전체 매니페스트를 막습니다. 기존 테스트 maps resumable Codex threads back to openai (HEAD 261줄) 는 OpenCodex가 0을 1로 올린 값을 다시 0으로 되돌리는 계약을 잠가 두므로, 플래그를 무조건 보존하면 그 테스트가 깨집니다.

이번 PR은 두 구멍을 같이 닫습니다. 커밋은 아홉 개입니다. 첫 커밋 0af9c0fd5@ntdatt812 의 원본 #3056 아이디를 보는 반쪽입니다. 그 위에 메인테이너가 has_user_event 반쪽, 매니페스트 v2, 살아남은 매니페스트 재스냅샷, relabel: none 증명, 애매한 재라우트 거절, 거절 범위 축소, integrityCode 전달, 계획 문서를 올렸습니다. 적대 리뷰 일곱 라운드(3 → 2 → 1 → 1 → 2 → 2 → 0)가 커밋 메시지에 남아 있습니다.

아이디 반쪽은 경로 래퍼 readLatestSessionMetaForId 를 PR 819-821줄에 만들고, 세 호출 지점을 바꿉니다. snapshotRolloutForRestore 695줄, assertRestoreReadback 774줄, updateSessionMeta 1039줄. 작성기는 더 이상 외국 줄을 보고 건너뛰지 않고, 이 스레드의 메타를 붙입니다. 앱이 버리는 부모 줄을 복제하지 않습니다.

이벤트 반쪽은 매니페스트에 불리언만 남깁니다. 본문은 절대 안 씁니다. src/codex/history-manifest.tsCodexHistoryBackupEntryhadFirstUserMessage?: booleanrelabel?: "pending" | "committed" | "none" 이 생기고, version 은 1 | 2 입니다. 검증기는 1과 2를 받고, 문자 "false" 같은 진실값 함정을 막습니다. 새 매니페스트와 앞으로 쓰는 매니페스트는 v2입니다. v1은 지금 HEAD와 같은 현재-행 읽기를 유지합니다. 그 인구를 거절하면 #3026이 고치려는 바로 그 디스크를 벽돌로 만듭니다.

소유권은 restoredUserEventFor (PR 611-643줄) 가 정합니다. A는 기록된 원본 그대로, B는 기대 포스트이미지라서 매니페스트 값이 권위, C는 원본 튜플에서 0→1, D는 라우팅된 튜플에서 0→1 입니다. D는 출처가 필요 없습니다. 그 튜플을 입은 행은 OpenCodex가 쓴 행입니다. C만 애매하고, 그것도 라우팅이 1을 썼을 때만 그렇습니다. 라우팅이 안 되고 사용자가 친 경우와, 라우팅 후 레거시 복구가 원본 튜플로 되돌린 경우가 같은 행을 만듭니다. 그 칸은 null 을 돌려 거절합니다. relabel: none 은 사용자 활동, committed 이면서 기대 이벤트가 1이면 OpenCodex 것(0으로 복구), 기대가 0이면 사용자 것(1 보존), exec 출처는 레거시 복구가 원본 튜플로 못 돌아가므로 1을 보존합니다. 1→0 역방향은 어떤 마커에서도 거절합니다. 이 시스템은 플래그를 내리는 작성자가 없습니다.

라우팅 전에 rememberOriginalrelabel: pending 을 쓰고, 라우팅이 끝난 뒤 1395-1399줄에서 committed 로 고칩니다. 그 사이 충돌은 pending 으로 남고, 복구 때 행을 보고 풉니다. 복구는 성공했는데 매니페스트 소비만 실패하면 1517-1523줄이 최선을 다해 relabel: none 을 남깁니다. 다음 라우팅이 그 증명을 보고 베이스라인을 새로 찍습니다. 증명이 없고 이벤트가 갈라진 재라우트는 history_apply_ambiguous_reroute 로 한 줄도 쓰지 않고 거절합니다. 그 코드는 CodexHistorySyncResult.integrityCodeHistoryWorkerResultCodexHistoryJobOutcome.historyIntegrityCode 로 올라가고, describeHistoryJobFailure (PR history-job.ts 267-268줄) 는 ocx doctor 가 아니라 사람이 매니페스트를 보라고 말합니다. 재시도하면 같은 거절이 나옵니다.

테스트는 구멍 두 개를 빨강으로 먼저 잠급니다. restores a forked rollout that trails its parent thread's session_meta (PR 912줄) 는 HEAD에서 history_backup_rollout_postimage_mismatch 로 빨강입니다. preserves a first user message that arrived after routing (#3026) (PR 337줄) 는 HEAD와 #3056 헤드 둘 다에서 빨강이어야 두 번째 반쪽이 있다는 증명이 됩니다. 분류기 표 restore classifier state matrix 는 본문이 16칸이라고 했지만 배열은 17행입니다(A 1, B 4, C 6, D 1, 역방향 2, 외국 튜플 2). 끝에서 끝까지 회귀는 살아남은 매니페스트 재스냅샷, 증명 없는 재라우트 거절, 레거시 복구가 튜플만 되돌린 뒤 1을 0으로 되돌리기, v1 매니페스트가 예전처럼 복구되기입니다. 본문 검증 숫자는 bun test tests/codex-history-provider.test.ts tests/codex-history-job.test.ts → 78 pass / 0 fail / 319 expect(), bun run typecheck exit 0 입니다. 소스의 test( 54회 중 표 루프 1회가 런타임 17개를 만드니 제공자 파일 70 + 잡 파일 8 = 78로 맞습니다. 이 상자의 체크아웃은 깨끗한 dev 라서 여기서 다시 돌리지는 않았습니다. CI 샤드가 그 게이트입니다.

types.ts/config.ts 분할과는 무관합니다. 손대는 파일은 히스토리 복구 경로뿐입니다. close-don't-rebase 대상이 아닙니다. #3056은 아이디 반쪽만 있는 원본이고 아직 OPEN입니다. 이번 것이 rebase+두 번째 반쪽 착륙입니다. 머지 전에 #3056을 닫지 마세요. 머지 뒤에 Landed via #3103 at <commit> 댓글과 landed-via-maintainer 로 닫으세요. 점수는 75입니다. 계획 파일도 75를 적어 두었고, 지금 HEAD 70대 남은 일 목록의 이 항목을 실제로 닫습니다. 80은 아닙니다. 프리플라이트는 여전히 전부-아니면-전무이고, 정말 결정할 수 없는 한 칸과 v1 매니페스트의 C칸은 예전처럼 전체를 막습니다. 그건 추측보다 안전한 거절입니다. 리포터가 잰 6/389 포크 구멍은 이 패치로 사라집니다.

라인 552 / 631 / 878 (HEAD src/codex/history-provider.ts) - readLatestSessionMeta 가 아이디를 보지 않습니다. 포크 롤아웃의 맨 끝 줄은 부모 아이디라서 세 길이 전부 실패하거나 파일을 건너뜁니다. 구멍이 지금 HEAD에 있습니다.
라인 680-688 (HEAD) - 아이디를 보는 접기는 이미 있습니다. 배선만 안 되어 있었습니다. 이번 PR은 경로 래퍼를 만들어 세 호출을 바꿉니다.
라인 887-889 (HEAD updateSessionMeta) - 아이디가 다른 마지막 줄이면 파일을 건너뜁니다. DB만 뒤집혀 복구 불가능한 짝이 됩니다. HEAD 테스트 192-210줄이 files: 0 으로 그 계약을 잠가 두었습니다.
라인 487-489 (HEAD rowMatchesExpectedPostImage) - 포스트이미지 이벤트를 지금 행의 first_user_message 로 다시 계산합니다. 스냅샷 때 빈 메시지였다가 사용자가 친 1을 OpenCodex가 쓴 값으로 보고 0으로 지웁니다.
라인 586-596 (HEAD preflightRestoreTargets) - 한 항목 거절이 매니페스트 전체입니다. 포크 6개가 stop/update를 막은 이유입니다. 이번 PR도 이 구조는 유지합니다. 포크 항목이 통과하게 바꿉니다.
라인 819-821 (PR 새 파일 번호) - readLatestSessionMetaForId 래퍼. snapshotRolloutForRestore 695, assertRestoreReadback 774, updateSessionMeta 1039 가 이걸 씁니다. 작성기는 자기 스레드 메타를 붙입니다.
라인 577-578 (PR rowMatchesExpectedPostImage) - entry.hadFirstUserMessage ?? hasFirstUserMessage(row.first_user_message). v2는 스냅샷 불리언, v1은 지금 HEAD와 같은 현재-행 읽기입니다.
라인 611-643 (PR restoredUserEventFor) - A/B/C/D 분류기. 프리플라이트 754줄과 복구 쓰기 1444줄이 이 반환값을 씁니다. null 이면 추측하지 않습니다.
라인 620 (PR) - ???: 가 괄호 없이 붙어 있습니다. JS에서 ?? 가 더 세서 (hadFirstUserMessage ?? hasFirst...) ? 1 : 0 이 됩니다. false 는 nullish가 아니라 0이 나옵니다. 의도는 맞습니다. 괄호를 치면 읽기만 더 쉽습니다.
라인 498-533 (PR rememberOriginal) - 살아남은 항목의 마커를 pending 으로 다시 열고 hadFirstUserMessage 를 이 시도 기준으로 다시 찍습니다. 원본 튜플은 덮지 않습니다. 이벤트 베이스라인은 방향·출처·none 증명이 있을 때만 고칩니다. 아니면 history_apply_ambiguous_reroute. 예외는 writeBackup 1307줄 앞에서 나므로 디스크는 그대로입니다.
라인 767-772 (PR assertRestoreReadback) - 복구 뒤 행의 has_user_event 로 튜플을 맞춰 제공자/출처만 확인하고, 분류기가 null 이 아닌지도 봅니다. 보존된 1이 읽기검증에서 다시 거절되지 않게 하려는 장치입니다.
라인 806-809 (PR) - 아이디를 무시하는 readLatestSessionMeta 는 아직 내보내져 있습니다. 복구/작성 세 곳은 더 이상 안 부릅니다. 나중에 누가 다시 꽂으면 같은 구멍이 생깁니다. 이번 범위에서 지울 필요는 없습니다.
라인 912 / 337 / 191 / 237 / 365 (PR tests/codex-history-provider.test.ts) - 포크 복구, 라우팅 뒤 첫 글 보존, 증명 없는 재라우트 거절, 레거시 복구 후 OpenCodex가 쓴 1을 0으로, v1 매니페스트 복구. 표 17행이 칸을 잠급니다.
라인 135-140 (PR tests/codex-history-job.test.ts) - 애매한 재라우트 문구에 ocx doctor 가 없고 수동 해결이 있습니다.
경로 #3056 - 원본 PR이 아직 OPEN입니다. 아이디 반쪽만 있고 dev 에 많이 뒤처져 있습니다. 이번 것이 착륙본입니다. 머지 전에 닫지 마세요.
경로 types.ts / config.ts - 이번 파일 목록에 없습니다. 분할 캠페인의 close-don't-rebase 대상이 아닙니다.

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

너의 추천
CI가 초록이면 #3103을 dev 에 squash merge 하세요. 지금 HEAD에 있는 아이디 무시 읽기와 현재-메시지 has_user_event 재계산을 둘 다 닫고, 리포터가 잰 포크 6/389가 stop/update를 막는 길을 끊습니다. types/config 분할과 무관하니 close-don't-rebase 대상이 아닙니다. 머지 직후 #3056에 Landed via #3103 at <commit> 를 달고 landed-via-maintainer 로 닫으세요. #3026이 Closes로 안 닫히면 같은 방식으로 닫으면 됩니다. 라벨은 바꾸지 않습니다. 머지 전에 #3056을 닫지 마세요.

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

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants