mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08: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:
@@ -110,7 +110,11 @@ pub(super) fn stored_thread_from_rollout_item(
|
||||
item.git_origin_url.clone(),
|
||||
);
|
||||
let source = item.source.unwrap_or(SessionSource::Unknown);
|
||||
let preview = item.first_user_message.clone().unwrap_or_default();
|
||||
let preview = item
|
||||
.preview
|
||||
.clone()
|
||||
.or_else(|| item.first_user_message.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(StoredThread {
|
||||
thread_id,
|
||||
|
||||
@@ -267,6 +267,7 @@ mod tests {
|
||||
let mut metadata = builder.build(config.default_model_provider_id.as_str());
|
||||
metadata.title = "needle title".to_string();
|
||||
metadata.first_user_message = Some("plain preview".to_string());
|
||||
metadata.preview = metadata.first_user_message.clone();
|
||||
runtime
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
|
||||
@@ -280,11 +280,16 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
.ok()
|
||||
.map(|meta_line| meta_line.meta);
|
||||
let forked_from_id = session_meta.as_ref().and_then(|meta| meta.forked_from_id);
|
||||
let preview = metadata
|
||||
.preview
|
||||
.clone()
|
||||
.or_else(|| metadata.first_user_message.clone())
|
||||
.unwrap_or_default();
|
||||
StoredThread {
|
||||
thread_id: metadata.id,
|
||||
rollout_path: Some(metadata.rollout_path),
|
||||
forked_from_id,
|
||||
preview: metadata.first_user_message.clone().unwrap_or_default(),
|
||||
preview,
|
||||
name,
|
||||
model_provider: if metadata.model_provider.is_empty() {
|
||||
store.config.default_model_provider_id.clone()
|
||||
@@ -1009,8 +1014,9 @@ mod tests {
|
||||
builder.cwd = external.path().join("workspace");
|
||||
builder.cli_version = Some("sqlite-cli".to_string());
|
||||
let mut metadata = builder.build(config.default_model_provider_id.as_str());
|
||||
metadata.title = "SQLite title".to_string();
|
||||
metadata.first_user_message = Some("SQLite preview".to_string());
|
||||
metadata.preview = Some("optimize the benchmark".to_string());
|
||||
metadata.first_user_message = Some("next normal prompt".to_string());
|
||||
metadata.title = "next normal prompt".to_string();
|
||||
metadata.model = Some("sqlite-model".to_string());
|
||||
runtime
|
||||
.upsert_thread(&metadata)
|
||||
@@ -1028,9 +1034,12 @@ mod tests {
|
||||
|
||||
assert_eq!(thread.thread_id, thread_id);
|
||||
assert_eq!(thread.rollout_path, Some(rollout_path));
|
||||
assert_eq!(thread.preview, "SQLite preview");
|
||||
assert_eq!(thread.first_user_message.as_deref(), Some("SQLite preview"));
|
||||
assert_eq!(thread.name.as_deref(), Some("SQLite title"));
|
||||
assert_eq!(thread.preview, "optimize the benchmark");
|
||||
assert_eq!(
|
||||
thread.first_user_message.as_deref(),
|
||||
Some("next normal prompt")
|
||||
);
|
||||
assert_eq!(thread.name, None);
|
||||
assert_eq!(thread.model_provider, "sqlite-provider");
|
||||
assert_eq!(thread.model.as_deref(), Some("sqlite-model"));
|
||||
assert_eq!(thread.cwd, external.path().join("workspace"));
|
||||
|
||||
Reference in New Issue
Block a user