Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,13 @@ impl ExternalAgentSessionImporter {
},
};
rollout_items.retain(is_persisted_rollout_item);
let title = title
let name = title
.as_deref()
.and_then(codex_core::util::normalize_thread_name);
let metadata = ThreadMetadataPatch {
title,
name: name.map(Some),
Comment thread
anaiskillian marked this conversation as resolved.
Comment thread
anaiskillian marked this conversation as resolved.
preview: first_user_message.clone(),
title: first_user_message.clone(),
model_provider: Some(model_provider),
created_at: Some(now),
updated_at: Some(now),
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/external-agent-sessions/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub(crate) fn load_session_for_import_with_content_sha256(
.iter()
.find(|message| message.role == MessageRole::User)
.map(|message| summarize_for_label(&message.text));
let title = parsed.source_title.or_else(|| first_user_message.clone());
let title = parsed.source_title;
let rollout_items = rollout_items_from_messages(messages);
if rollout_items.is_empty() {
return Ok(None);
Expand Down
1 change: 0 additions & 1 deletion codex-rs/rollout/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,6 @@ pub(crate) async fn backfill_sessions_with_lease(
let memory_mode = outcome.memory_mode.unwrap_or_else(|| "enabled".to_string());
if let Ok(Some(existing_metadata)) = runtime.get_thread(metadata.id).await {
metadata.prefer_existing_git_info(&existing_metadata);
metadata.prefer_existing_explicit_title(&existing_metadata);
}
if rollout.archived && metadata.archived_at.is_none() {
let fallback_archived_at = metadata.updated_at;
Expand Down
40 changes: 17 additions & 23 deletions codex-rs/rollout/src/state_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,6 @@ pub async fn reconcile_rollout(
metadata.cwd = normalize_cwd_for_state_db(&metadata.cwd);
if let Ok(Some(existing_metadata)) = ctx.get_thread(metadata.id).await {
metadata.prefer_existing_git_info(&existing_metadata);
metadata.prefer_existing_explicit_title(&existing_metadata);
}
match archived_only {
Some(true) if metadata.archived_at.is_none() => {
Expand Down Expand Up @@ -577,29 +576,24 @@ pub async fn read_repair_rollout_path(
&& let Ok(Some(metadata)) = ctx.get_thread(thread_id).await
{
saw_existing_metadata = true;
let mut repaired = metadata.clone();
repaired.rollout_path = rollout_path.to_path_buf();
repaired.cwd = normalize_cwd_for_state_db(&repaired.cwd);
match archived_only {
Some(true) if repaired.archived_at.is_none() => {
repaired.archived_at = Some(repaired.updated_at);
}
Some(false) => {
repaired.archived_at = None;
let cwd = normalize_cwd_for_state_db(&metadata.cwd);
let archive_state = match archived_only {
Some(true) => codex_state::ThreadArchiveState::Archived,
Some(false) => codex_state::ThreadArchiveState::Active,
None => codex_state::ThreadArchiveState::Preserve,
};
match ctx
.repair_thread_rollout_location(thread_id, rollout_path, cwd.as_path(), archive_state)
.await
{
Ok(true) => return,
Ok(false) => return,
Err(err) => {
warn!(
"state db read-repair update failed for {}: {err}",
rollout_path.display()
);
}
Some(true) | None => {}
}
if repaired == metadata {
return;
}
warn!("state db discrepancy during read_repair_rollout_path: upsert_needed (fast path)");
if let Err(err) = ctx.upsert_thread(&repaired).await {
warn!(
"state db read-repair upsert failed for {}: {err}",
rollout_path.display()
);
} else {
return;
}
}

Expand Down
7 changes: 4 additions & 3 deletions codex-rs/rollout/src/state_db_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async fn try_init_times_out_waiting_for_stuck_startup_backfill() -> anyhow::Resu
}

#[tokio::test]
async fn reconcile_rollout_preserves_existing_explicit_title() -> anyhow::Result<()> {
async fn reconcile_rollout_preserves_existing_explicit_name() -> anyhow::Result<()> {
let home = TempDir::new().expect("temp dir");
let thread_id = ThreadId::new();
let rollout_path = write_rollout_with_user_message(home.path(), thread_id, "Hey")?;
Expand All @@ -122,7 +122,7 @@ async fn reconcile_rollout_preserves_existing_explicit_title() -> anyhow::Result
.metadata;
assert_eq!(metadata.title, "Hey");
assert_eq!(metadata.first_user_message.as_deref(), Some("Hey"));
metadata.title = "math".to_string();
metadata.name = codex_state::ThreadName::Explicit("math".to_string());
runtime.upsert_thread(&metadata).await?;

reconcile_rollout(
Expand All @@ -140,7 +140,8 @@ async fn reconcile_rollout_preserves_existing_explicit_title() -> anyhow::Result
.get_thread(thread_id)
.await?
.expect("thread should exist");
assert_eq!(persisted.title, "math");
assert_eq!(persisted.title, "Hey");
assert_eq!(persisted.name.explicit(), Some("math"));
assert_eq!(persisted.first_user_message.as_deref(), Some("Hey"));
Ok(())
}
Expand Down
28 changes: 28 additions & 0 deletions codex-rs/state/migrations/0040_threads_name.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
ALTER TABLE threads ADD COLUMN name TEXT;
ALTER TABLE threads ADD COLUMN name_state TEXT NOT NULL DEFAULT 'legacy_unknown'
CHECK (name_state IN ('legacy_unknown', 'unnamed', 'cleared', 'explicit'));
ALTER TABLE threads ADD COLUMN title_snapshot TEXT NOT NULL DEFAULT '';
Comment thread
anaiskillian marked this conversation as resolved.
ALTER TABLE threads ADD COLUMN title_state TEXT NOT NULL DEFAULT 'legacy_unknown'
CHECK (title_state IN ('legacy_unknown', 'derived'));

UPDATE threads
SET name = title,
name_state = 'explicit',
title_snapshot = title
Comment on lines +9 to +11

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not keep migrated names as derived titles

For pre-0040 rows whose title was a user-assigned name, this migration copies it into name but leaves the same value in title/title_snapshot. The incremental rollout path starts from existing metadata and only fills title when it is empty, so the next write records that old name as the derived title; if the user later clears the explicit name, exact-title resume/search can still match the old name. In the migration, move the history-derived value into title (for example from first_user_message) or clear title/title_snapshot for rows promoted to explicit names.

AGENTS.md reference: AGENTS.md:L103-L111

Useful? React with 👍 / 👎.

WHERE title <> ''
AND (first_user_message = '' OR trim(title) <> trim(first_user_message));

UPDATE threads
SET title_snapshot = title
WHERE name_state = 'legacy_unknown';

CREATE TRIGGER threads_title_snapshot_after_insert
AFTER INSERT ON threads
WHEN NEW.title_snapshot = ''
AND NEW.title <> ''
AND NEW.title_state = 'legacy_unknown'
BEGIN
UPDATE threads
SET title_snapshot = NEW.title
WHERE id = NEW.id;
Comment on lines +25 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed names for older-binary inserts

When an older Codex binary inserts a thread after this migration, it does not know about name, so the new columns take their defaults. This trigger only fills title_snapshot, leaving name_state='legacy_unknown'; the next new-binary upsert sees title == title_snapshot and converts the row to unnamed, dropping any user-assigned legacy title written by that older binary. The recency migration already supports this mixed-version flow, so this trigger should also classify distinct legacy titles into name/name_state.

AGENTS.md reference: AGENTS.md:L103-L111

Useful? React with 👍 / 👎.

END;
1 change: 1 addition & 0 deletions codex-rs/state/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ mod tests {
cwd: PathBuf::from("/tmp"),
cli_version: "0.0.0".to_string(),
title: String::new(),
name: crate::ThreadName::Unnamed,
preview: None,
sandbox_policy: "read-only".to_string(),
approval_mode: "on-request".to_string(),
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ pub use model::Stage1JobClaim;
pub use model::Stage1JobClaimOutcome;
pub use model::Stage1Output;
pub use model::Stage1StartupClaimParams;
pub use model::ThreadArchiveState;
pub use model::ThreadGoal;
pub use model::ThreadGoalStatus;
pub use model::ThreadListItem;
pub use model::ThreadListPage;
pub use model::ThreadMetadata;
pub use model::ThreadMetadataBuilder;
pub use model::ThreadName;
pub use model::ThreadsPage;
pub use runtime::ExternalAgentConfigImportDetailsRecord;
pub use runtime::ExternalAgentConfigImportFailureRecord;
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/state/src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ pub use thread_metadata::BackfillStats;
pub use thread_metadata::ExtractionOutcome;
pub use thread_metadata::SortDirection;
pub use thread_metadata::SortKey;
pub use thread_metadata::ThreadArchiveState;
pub use thread_metadata::ThreadListItem;
pub use thread_metadata::ThreadListPage;
pub use thread_metadata::ThreadMetadata;
pub use thread_metadata::ThreadMetadataBuilder;
pub use thread_metadata::ThreadName;
pub use thread_metadata::ThreadsPage;

pub(crate) use agent_job::AgentJobItemRow;
Expand Down
Loading
Loading