Files
codex/codex-rs/thread-store
T
Zanie Blue 6a9a49b334 Avoid rereading rollout history during cold resume (#27031)
## Summary

- reuse the history-bearing `StoredThread` loaded while probing for a
running thread
- avoid rereading and reparsing the rollout when that probe finds no
active process
- reload after shutting down a loaded thread because shutdown may flush
newer rollout items
- add a regression test that verifies cold resume performs one
history-bearing store read

## Problem

`thread/resume` first reads the persisted thread with history while
checking whether the thread is
already running. When no running process exists, cold resume currently
falls through to
`resume_thread_from_rollout`, which reads and parses the same history
again.

That duplicate work grows with rollout size and remains on the
synchronous resume path even when
the caller requests `excludeTurns`.

## Background

The duplicate read was introduced by #24528, which fixed resume
overrides for idle cached
threads. To support resumes specified by rollout path,
`resume_running_thread` began loading the
stored thread with history so it could resolve the canonical thread ID
and determine whether a
cached `CodexThread` was already loaded.

That history is needed when the loaded-thread path handles the request.
On a cold miss, however,
the function's boolean result could only report that no loaded thread
handled the request. It
discarded the history-bearing `StoredThread`, and the normal cold-resume
path immediately loaded
and parsed the same rollout again.

This change preserves the idle cached-thread behavior from #24528 while
allowing the cold-resume
path to reuse the probe result.

## Performance

I benchmarked real retained rollouts using isolated `CODEX_HOME`
directories, explicit rollout
paths, debug builds of the commit and its exact parent, and alternating
parent/patch order. The
table below uses `thread/resume` with `excludeTurns: true`; response
payload sizes were identical.

| Rollout size | Records | Parent median | Patch median | Median paired
saving |
| ---: | ---: | ---: | ---: | ---: |
| 6 MB | 3,574 | 541 ms | 441 ms | 132 ms |
| 30 MB | 15,220 | 1.505 s | 1.041 s | 701 ms |
| 60 MB | 31,453 | 2.644 s | 1.742 s | 970 ms |
| 149 MB | 100,874 | 10.506 s | 7.156 s | 3.350 s |
| 559 MB | 259,734 | 27.759 s | 16.725 s | 9.836 s |

The absolute saving increases with thread size, as expected when
removing one complete JSONL
history read and parse. Total resume time is also content-dependent, so
the relationship is not
perfectly linear.

I also tested full-history resume with `excludeTurns: false`. The
response payload was
byte-identical between variants, and the same size-dependent improvement
remained visible:

| Rollout size | Parent median | Patch median | Median paired saving |
| ---: | ---: | ---: | ---: |
| 6 MB | 1.052 s | 904 ms | 270 ms |
| 30 MB | 2.667 s | 1.762 s | 924 ms |
| 60 MB | 8.464 s | 6.272 s | 3.680 s |
| 149 MB | 26.719 s | 12.118 s | 14.601 s |
| 559 MB | 40.359 s | 25.475 s | 16.590 s |

## Validation

- `just test -p codex-app-server
cold_thread_resume_reuses_non_local_history_probe`
- `just fix -p codex-app-server -p codex-thread-store`
- `just fmt`
6a9a49b334 ยท 2026-06-09 11:16:27 -05:00
History
..
2026-04-14 13:51:00 -07:00

Thread Store

codex-thread-store is the storage boundary for Codex threads. It defines the ThreadStore trait plus local and in-memory implementations. Other storage implementations may live outside this repository.

Responsibilities

  • ThreadStore::append_items is the raw canonical history append API. It does not infer metadata from item contents.
  • ThreadStore::update_thread_metadata is the only thread metadata write API. It accepts a single literal metadata patch shape, regardless of whether the caller is applying a user/API mutation or facts derived above the store from appended history.
  • LiveThread is the preferred API for active session persistence. It owns a per-thread metadata sync helper, applies the rollout persistence policy, appends canonical history, and then sends metadata patches through ThreadStore::update_thread_metadata.
  • ThreadManager routes metadata mutations for loaded and cold threads through one entrypoint. Loaded threads use their LiveThread; cold threads go directly to the store.
  • LocalThreadStore persists history through codex-rollout JSONL files and persists queryable metadata through the SQLite state database when available. Local explicit metadata mutations also maintain JSONL/name-index compatibility so reading old or SQLite-less local storage keeps working.
  • RolloutRecorder is the local JSONL writer. It writes already-canonical items for ThreadStore::append_items; it no longer decides metadata updates for live thread-store appends.
  • core/session creates or resumes LiveThread handles and does not need to know whether persistence is backed by local files or another store.

Direction

New metadata observation semantics should live above ThreadStore. Stores persist explicit metadata fields, but raw history appends remain history-only.