mirror of
https://github.com/openai/codex.git
synced 2026-11-01 16:17:58 +00:00
Use goal preview metadata for goal-first threads (#21981)
Fixes #20792 ## Why `/goal`-first threads are valid resumable threads, but they can be missing from `codex resume` and app recents because discovery depends on metadata derived from a normal first user message. PR #21489 attempted to fix this by using the goal objective as `first_user_message`. Review feedback pointed out that `first_user_message` does more than provide visible text today: it gates listing, supplies preview text, and participates in deciding whether a later title should surface as a distinct thread name. Reusing it for the goal objective could leave a `/goal`-first thread with `first_user_message=<goal>` and `title=<later prompt>`, even though the goal should only provide the initial visible preview. This PR follows that feedback by and keeps the `first_user_message` as is but introduces a new `preview` field to separate concerns. The `preview` field is populated from the first user message or the goal objective. We can extend it in the future to include other sources. ## What Changed - Added internal thread `preview` metadata in `codex-state`, including a SQLite migration that backfills from `first_user_message` and from existing `thread_goals` objectives when needed. - Treated `ThreadGoalUpdated` as preview-bearing metadata so goal-first threads can be listed and searched without mutating `first_user_message`. - Updated rollout listing, state queries, thread-store conversion, and app-server mapping to use preview metadata while continuing to expose the existing public `preview` field. - Preserved title/name distinctness behavior around literal `first_user_message`, so a later normal prompt after `/goal` does not surface as a separate name just because the goal supplied the initial preview. - Preserved compatibility for older/internal metadata writes by deriving preview from `first_user_message` when explicit preview metadata is absent. ## Verification - Manually verified that a thread that starts with a `/goal <objective>` shows up in the resume picker.
This commit is contained in:
@@ -2182,7 +2182,7 @@ mod tests {
|
||||
|
||||
assert_eq!(response.thread.id, thread_id.to_string());
|
||||
assert_eq!(response.thread.path, None);
|
||||
assert_eq!(response.thread.preview, "before rollback");
|
||||
assert_eq!(response.thread.preview, "fallback preview");
|
||||
assert_eq!(response.thread.name.as_deref(), Some("Rollback thread"));
|
||||
assert_eq!(response.thread.status, ThreadStatus::NotLoaded);
|
||||
assert_eq!(response.thread.turns.len(), 1);
|
||||
|
||||
@@ -3745,7 +3745,7 @@ pub(crate) fn thread_from_stored_thread(
|
||||
id: thread_id.clone(),
|
||||
session_id: thread_id,
|
||||
forked_from_id: thread.forked_from_id.map(|id| id.to_string()),
|
||||
preview: thread.first_user_message.unwrap_or(thread.preview),
|
||||
preview: thread.preview,
|
||||
ephemeral: false,
|
||||
model_provider: if thread.model_provider.is_empty() {
|
||||
fallback_provider.to_string()
|
||||
@@ -3787,7 +3787,7 @@ fn summary_from_stored_thread(
|
||||
ConversationSummary {
|
||||
conversation_id: thread.thread_id,
|
||||
path,
|
||||
preview: thread.first_user_message.unwrap_or(thread.preview),
|
||||
preview: thread.preview,
|
||||
// Preserve millisecond precision from the thread store so thread/list cursors
|
||||
// round-trip the same ordering key used by pagination queries.
|
||||
timestamp: Some(
|
||||
@@ -3818,6 +3818,7 @@ fn summary_from_state_db_metadata(
|
||||
conversation_id: ThreadId,
|
||||
path: PathBuf,
|
||||
first_user_message: Option<String>,
|
||||
preview: Option<String>,
|
||||
timestamp: String,
|
||||
updated_at: String,
|
||||
model_provider: String,
|
||||
@@ -3831,7 +3832,7 @@ fn summary_from_state_db_metadata(
|
||||
git_branch: Option<String>,
|
||||
git_origin_url: Option<String>,
|
||||
) -> ConversationSummary {
|
||||
let preview = first_user_message.unwrap_or_default();
|
||||
let preview = preview.or(first_user_message).unwrap_or_default();
|
||||
let source = serde_json::from_str(&source)
|
||||
.or_else(|_| serde_json::from_value(serde_json::Value::String(source.clone())))
|
||||
.unwrap_or(codex_protocol::protocol::SessionSource::Unknown);
|
||||
@@ -3865,6 +3866,7 @@ fn summary_from_thread_metadata(metadata: &ThreadMetadata) -> ConversationSummar
|
||||
metadata.id,
|
||||
metadata.rollout_path.clone(),
|
||||
metadata.first_user_message.clone(),
|
||||
metadata.preview.clone(),
|
||||
metadata
|
||||
.created_at
|
||||
.to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
|
||||
@@ -1086,6 +1086,7 @@ mod thread_processor_behavior_tests {
|
||||
conversation_id,
|
||||
PathBuf::from("/tmp/rollout.jsonl"),
|
||||
Some("hi".to_string()),
|
||||
/*preview*/ None,
|
||||
"2025-09-05T16:53:11Z".to_string(),
|
||||
"2025-09-05T16:53:12Z".to_string(),
|
||||
"test-provider".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user