ensure thread.history_mode is immutable (#30261)

## Description

This PR makes `thread.history_mode` immutable after the thread's
canonical first `SessionMeta` has been written. Later same-thread
`SessionMeta` lines are compatibility metadata writes, not a new thread
definition.

Without this, an older binary could append a `SessionMeta` that omits
`history_mode`; when a newer binary replays it, serde defaults that
missing field to `legacy` and SQLite could downgrade a paginated thread.

## Why

`history_mode` is the persisted thread storage contract.
Paginated-thread fail-closed behavior and SQLite memory filtering depend
on it staying aligned with canonical rollout metadata, especially when
multiple Codex binary versions can touch the same local rollout.

## What changed

- Stop generic rollout metadata replay from overwriting `history_mode`
from later `SessionMeta` items.
- Remove `history_mode` from `ThreadMetadataPatch`, so mutable metadata
sync and app-server metadata updates cannot rewrite it.
- When local metadata sync has to recreate a missing SQLite row, recover
`history_mode` from the rollout's canonical first `SessionMeta` instead
of from a mutable patch.
- Keep the in-memory thread store using the created thread's canonical
`history_mode` instead of metadata patches.
- Fill the one remaining core test `CreateThreadParams` initializer with
the new `history_mode` field; Bazel CI caught this after the parent
history-mode PR landed.

## Validation

- `just fmt`
- `just test -p codex-thread-store`
- `just test -p codex-state
session_meta_does_not_set_model_or_reasoning_effort`
This commit is contained in:
Owen Lin authored and GitHub committed 2026-06-26 12:32:31 -07:00
1 parent cf36c688b3
commit 812cd2bb57
7 files changed
+226 -66

No files matched your search

+19 -22
View File
@@ -218,17 +218,22 @@ mod tests {
})
.await
.expect("register rollout path");
store
.update_thread_metadata(UpdateThreadMetadataParams {
thread_id,
patch: ThreadMetadataPatch {
history_mode: Some(ThreadHistoryMode::Paginated),
..Default::default()
},
include_archived: false,
})
.await
.expect("seed paginated metadata");
{
let mut state = store.state.lock().await;
state
.created_threads
.get_mut(&thread_id)
.expect("created thread")
.history_mode = ThreadHistoryMode::Paginated;
let Some(RolloutItem::SessionMeta(meta_line)) = state
.histories
.get_mut(&thread_id)
.and_then(|history| history.first_mut())
else {
panic!("canonical session meta");
};
meta_line.meta.history_mode = ThreadHistoryMode::Paginated;
}
let thread = store
.read_thread(ReadThreadParams {
@@ -784,9 +789,7 @@ fn stored_thread_from_state(
source: metadata
.and_then(|metadata| metadata.source.clone())
.unwrap_or_else(|| created.source.clone()),
history_mode: metadata
.and_then(|metadata| metadata.history_mode)
.unwrap_or(created.history_mode),
history_mode: created.history_mode,
thread_source: metadata
.and_then(|metadata| metadata.thread_source.clone())
.unwrap_or_else(|| created.thread_source.clone()),
@@ -811,15 +814,9 @@ fn history_mode_from_state(
thread_id: ThreadId,
) -> ThreadHistoryMode {
state
.metadata_updates
.created_threads
.get(&thread_id)
.and_then(|metadata| metadata.history_mode)
.or_else(|| {
state
.created_threads
.get(&thread_id)
.map(|thread| thread.history_mode)
})
.map(|thread| thread.history_mode)
.unwrap_or_default()
}