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
2026-06-26 12:32:31 -07:00
committed by GitHub
parent cf36c688b3
commit 812cd2bb57
7 changed files with 226 additions and 66 deletions
+5 -2
View File
@@ -56,7 +56,7 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi
}
metadata.id = meta_line.meta.id;
metadata.source = enum_to_string(&meta_line.meta.source);
metadata.history_mode = meta_line.meta.history_mode;
// Later SessionMeta lines do not redefine the canonical history_mode.
metadata.thread_source = meta_line.meta.thread_source.clone();
metadata.agent_nickname = meta_line.meta.agent_nickname.clone();
metadata.agent_role = meta_line.meta.agent_role.clone();
@@ -178,6 +178,7 @@ mod tests {
use codex_protocol::protocol::ThreadGoal;
use codex_protocol::protocol::ThreadGoalStatus;
use codex_protocol::protocol::ThreadGoalUpdatedEvent;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
use codex_protocol::protocol::UserMessageEvent;
@@ -516,6 +517,7 @@ mod tests {
#[test]
fn session_meta_does_not_set_model_or_reasoning_effort() {
let mut metadata = metadata_for_test();
metadata.history_mode = ThreadHistoryMode::Paginated;
let thread_id = metadata.id;
apply_rollout_item(
@@ -540,7 +542,7 @@ mod tests {
dynamic_tools: None,
selected_capability_roots: Vec::new(),
memory_mode: None,
history_mode: Default::default(),
history_mode: ThreadHistoryMode::Legacy,
multi_agent_version: None,
context_window: None,
},
@@ -551,6 +553,7 @@ mod tests {
assert_eq!(metadata.model, None);
assert_eq!(metadata.reasoning_effort, None);
assert_eq!(metadata.history_mode, ThreadHistoryMode::Paginated);
}
fn metadata_for_test() -> ThreadMetadata {