Thread metadata can store full first user message as title, causing huge active titles
What version of Codex are you using?
- Codex.app: observed
26.429.30905from the running app sample/process metadata - Codex CLI:
codex-cli 0.128.0 - Platform: macOS
26.4.1build25E253,arm64
What issue are you seeing?
Codex stores the full first user message as the thread title in ~/.codex/state_5.sqlite for some CLI/ACP/VS Code-origin sessions. This creates extremely large active thread titles and appears to make the Electron app sluggish when switching chats / rendering the thread picker.
Local evidence from state_5.sqlite:
select count(*) as rows,
sum(length(title)) as title_chars,
sum(length(first_user_message)) as first_msg_chars,
max(length(title)) as max_title,
max(length(first_user_message)) as max_first_msg
from threads
where archived=0;
Result:
134|14610549|14614033|675773|675773
The pathological subset is concentrated in RepoPrompt/ACP-like sessions:
select count(*) as rows,
sum(case when title=first_user_message then 1 else 0 end) as exact_dupes,
sum(length(title)) as title_chars,
sum(length(first_user_message)) as first_msg_chars
from threads
where archived=0
and source='vscode'
and cwd='/'
and title like '<codex reminder>%';
Result:
56|56|13944507|13944507
So 56 active rows have title == first_user_message, totaling ~13.9 MB of title text. 39 active rows have titles over 100 KB; the largest title is 675,773 characters.
Sample shape, sanitized:
source=vscode
cwd=/
title length=675773
first_user_message length=675773
title prefix=<codex reminder>You are operating in text only mode...
The actual rollout transcript is already stored separately via rollout_path, so the SQLite title field should not need to contain the entire first prompt.
Why this looks like a Codex metadata extraction bug
In codex-rs/state/src/extract.rs, apply_event_msg sets metadata.title directly from the stripped full user message when the title is empty:
EventMsg::UserMessage(user) => {
if metadata.first_user_message.is_none() {
metadata.first_user_message = user_message_preview(user);
}
if metadata.title.is_empty() {
let title = strip_user_message_prefix(user.message.as_str());
if !title.is_empty() {
metadata.title = title.to_string();
}
}
}
user_message_preview also returns the full stripped message. Then codex-rs/state/src/runtime/threads.rs upserts both title and first_user_message into the threads table.
This differs from the external-agent session import path, which already has SESSION_TITLE_MAX_LEN: usize = 120 and uses summarize_for_label() to cap fallback labels.
Expected behavior
threads.titleshould be a bounded display label, not the raw first user message.- The fallback title path in
state/src/extract.rsshould use the same short-label logic or another hard cap. - Ideally
first_user_messageshould either be bounded as preview data or excluded from hot UI list paths when very large. - Existing oversized titles should not cause the app to become sluggish when switching chats or rendering the thread list.
Actual behavior
- Large ACP/VS Code-origin sessions can leave
threads.titleas a hundreds-of-KB prompt. - The title is duplicated into
first_user_messagefor those rows. - Codex.app chat switching/thread navigation becomes sluggish; the live Electron renderer was observed with high CPU and a multi-GB physical footprint while investigating this state.
Reproduction sketch
- Start a Codex session through a CLI/ACP/VS Code client that sends a very large first user message and does not explicitly set a short thread name.
- Allow Codex to record/backfill thread metadata into
~/.codex/state_5.sqlite. - Inspect
threads.titlefor that thread. - Observe that the title can be the entire first user message rather than a bounded label.
Related
Possibly related to broad app startup/thread-list performance reports such as #16158, but this issue is specifically about the unbounded thread title metadata path.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗