mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +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:
1 parent
96836e15ed
commit
f10ddc3f13
18 files changed
+499
-33
No files matched your search
@@ -102,6 +102,8 @@ RETURNING
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
self.set_thread_preview_if_empty(thread_id, objective)
|
||||
.await?;
|
||||
thread_goal_from_row(&row)
|
||||
}
|
||||
|
||||
@@ -151,6 +153,10 @@ RETURNING
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
if row.is_some() {
|
||||
self.set_thread_preview_if_empty(thread_id, objective)
|
||||
.await?;
|
||||
}
|
||||
row.map(|row| thread_goal_from_row(&row)).transpose()
|
||||
}
|
||||
|
||||
@@ -271,6 +277,29 @@ WHERE thread_id = ?
|
||||
self.get_thread_goal(thread_id).await
|
||||
}
|
||||
|
||||
async fn set_thread_preview_if_empty(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
preview: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let preview = preview.trim();
|
||||
if preview.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE threads
|
||||
SET preview = ?
|
||||
WHERE id = ? AND preview = ''
|
||||
"#,
|
||||
)
|
||||
.bind(preview)
|
||||
.bind(thread_id.to_string())
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn pause_active_thread_goal(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
@@ -471,6 +500,12 @@ mod tests {
|
||||
Some(goal.clone()),
|
||||
runtime.get_thread_goal(thread_id).await.unwrap()
|
||||
);
|
||||
let metadata = runtime
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.expect("thread metadata should load")
|
||||
.expect("thread should exist");
|
||||
assert_eq!(metadata.preview.as_deref(), Some("hello"));
|
||||
|
||||
let updated = runtime
|
||||
.update_thread_goal(
|
||||
@@ -512,6 +547,41 @@ mod tests {
|
||||
assert!(!runtime.delete_thread_goal(thread_id).await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replace_thread_goal_sets_preview_when_empty() {
|
||||
let runtime = test_runtime().await;
|
||||
let thread_id = test_thread_id();
|
||||
let mut metadata = test_thread_metadata(
|
||||
runtime.codex_home(),
|
||||
thread_id,
|
||||
runtime.codex_home().join("workspace"),
|
||||
);
|
||||
metadata.preview = None;
|
||||
metadata.first_user_message = None;
|
||||
runtime
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
.expect("test thread should be upserted");
|
||||
|
||||
runtime
|
||||
.replace_thread_goal(
|
||||
thread_id,
|
||||
"optimize the benchmark",
|
||||
crate::ThreadGoalStatus::Active,
|
||||
/*token_budget*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("goal replacement should succeed");
|
||||
|
||||
let metadata = runtime
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.expect("thread metadata should load")
|
||||
.expect("thread should exist");
|
||||
assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark"));
|
||||
assert_eq!(metadata.first_user_message, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replace_thread_goal_applies_budget_limit_immediately() {
|
||||
let runtime = test_runtime().await;
|
||||
|
||||
Reference in new issue
Block a user