From 0f9eed3a6f479395520fcd7aceb9fc5b2f3f3754 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Fri, 20 Feb 2026 14:39:49 +0000 Subject: [PATCH] feat: add nick name to sub-agents (#12320) Adding random nick name to sub-agents. Used for UX At the same time, also storing and wiring the role of the sub-agent --- .../schema/json/ServerNotification.json | 28 ++ .../codex_app_server_protocol.schemas.json | 42 +++ .../v1/GetConversationSummaryResponse.json | 14 + .../json/v1/ListConversationsResponse.json | 14 + .../schema/json/v2/ThreadForkResponse.json | 28 ++ .../schema/json/v2/ThreadListResponse.json | 28 ++ .../schema/json/v2/ThreadReadResponse.json | 28 ++ .../schema/json/v2/ThreadResumeResponse.json | 28 ++ .../json/v2/ThreadRollbackResponse.json | 28 ++ .../schema/json/v2/ThreadStartResponse.json | 28 ++ .../json/v2/ThreadStartedNotification.json | 28 ++ .../json/v2/ThreadUnarchiveResponse.json | 28 ++ .../schema/typescript/SubAgentSource.ts | 2 +- .../schema/typescript/v2/Thread.ts | 8 + .../app-server-protocol/src/protocol/v2.rs | 4 + codex-rs/app-server/README.md | 5 +- .../app-server/src/codex_message_processor.rs | 86 +++++- codex-rs/app-server/src/filters.rs | 2 + codex-rs/app-server/src/thread_status.rs | 2 + codex-rs/app-server/tests/common/rollout.rs | 4 + .../app-server/tests/suite/v2/thread_list.rs | 4 + codex-rs/core/src/agent/agent_names.txt | 100 +++++++ codex-rs/core/src/agent/control.rs | 274 ++++++++++++++++-- codex-rs/core/src/agent/guards.rs | 182 +++++++++++- codex-rs/core/src/personality_migration.rs | 2 + codex-rs/core/src/rollout/list.rs | 14 +- codex-rs/core/src/rollout/metadata.rs | 6 + codex-rs/core/src/rollout/recorder.rs | 7 +- codex-rs/core/src/rollout/tests.rs | 26 ++ .../core/src/tools/handlers/multi_agents.rs | 52 ++-- .../core/tests/suite/personality_migration.rs | 4 + codex-rs/core/tests/suite/sqlite_state.rs | 2 + codex-rs/protocol/src/protocol.rs | 33 +++ .../0013_threads_agent_nickname.sql | 2 + codex-rs/state/src/extract.rs | 4 + codex-rs/state/src/model/thread_metadata.rs | 26 ++ codex-rs/state/src/runtime.rs | 14 +- codex-rs/state/src/runtime/memories.rs | 2 + codex-rs/tui/src/resume_picker.rs | 45 +-- 39 files changed, 1125 insertions(+), 109 deletions(-) create mode 100644 codex-rs/core/src/agent/agent_names.txt create mode 100644 codex-rs/state/migrations/0013_threads_agent_nickname.sql diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index f18531303..128f4fa60 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -6377,6 +6377,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -6524,6 +6538,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 39b21d0a5..ea4cd0bd3 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9431,6 +9431,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -15092,6 +15106,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -15218,6 +15246,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v1/GetConversationSummaryResponse.json b/codex-rs/app-server-protocol/schema/json/v1/GetConversationSummaryResponse.json index a54b7ddac..13cdf0b0f 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/GetConversationSummaryResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/GetConversationSummaryResponse.json @@ -123,6 +123,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" diff --git a/codex-rs/app-server-protocol/schema/json/v1/ListConversationsResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ListConversationsResponse.json index bb5a0f486..2e3ab7e0d 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ListConversationsResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ListConversationsResponse.json @@ -123,6 +123,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 2dfc446ac..dad39a512 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -716,6 +716,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -777,6 +791,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 2ab535625..80a1d42a2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -489,6 +489,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -550,6 +564,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 8aa8b0c4f..a4810d585 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -489,6 +489,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -550,6 +564,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 234376632..c4e1df847 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -716,6 +716,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -777,6 +791,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 482102928..f8cdcd744 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -489,6 +489,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -550,6 +564,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 1b617a850..099a65d21 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -716,6 +716,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -777,6 +791,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 6f016b0c7..9927bb2f3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -489,6 +489,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -550,6 +564,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index a49a3a8f9..011a97a34 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -489,6 +489,20 @@ "properties": { "thread_spawn": { "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, "depth": { "format": "int32", "type": "integer" @@ -550,6 +564,20 @@ }, "Thread": { "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, "cliVersion": { "description": "Version of the CLI that created the thread.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/typescript/SubAgentSource.ts b/codex-rs/app-server-protocol/schema/typescript/SubAgentSource.ts index 269a411be..df261bf3e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/SubAgentSource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/SubAgentSource.ts @@ -3,4 +3,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ThreadId } from "./ThreadId"; -export type SubAgentSource = "review" | "compact" | { "thread_spawn": { parent_thread_id: ThreadId, depth: number, } } | "memory_consolidation" | { "other": string }; +export type SubAgentSource = "review" | "compact" | { "thread_spawn": { parent_thread_id: ThreadId, depth: number, agent_nickname: string | null, agent_role: string | null, } } | "memory_consolidation" | { "other": string }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts index 523a02858..5e7a6b7af 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts @@ -43,6 +43,14 @@ cliVersion: string, * Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). */ source: SessionSource, +/** + * Optional random unique nickname assigned to an AgentControl-spawned sub-agent. + */ +agentNickname: string | null, +/** + * Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. + */ +agentRole: string | null, /** * Optional Git metadata captured when the thread was created. */ diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 6a1959bb2..fb2f86931 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2206,6 +2206,10 @@ pub struct Thread { pub cli_version: String, /// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). pub source: SessionSource, + /// Optional random unique nickname assigned to an AgentControl-spawned sub-agent. + pub agent_nickname: Option, + /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. + pub agent_role: Option, /// Optional Git metadata captured when the thread was created. pub git_info: Option, /// Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index fcae5b306..3d3cc18a9 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -224,6 +224,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per - `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`). - `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default). - `cwd` — restrict results to threads whose session cwd exactly matches this path. +- Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. Example: @@ -235,7 +236,7 @@ Example: } } { "id": 20, "result": { "data": [ - { "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111, "updatedAt": 1730831111, "status": { "type": "notLoaded" } }, + { "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111, "updatedAt": 1730831111, "status": { "type": "notLoaded" }, "agentNickname": "Atlas", "agentRole": "explorer" }, { "id": "thr_b", "preview": "Fix tests", "modelProvider": "openai", "createdAt": 1730750000, "updatedAt": 1730750000, "status": { "type": "notLoaded" } } ], "nextCursor": "opaque-token-or-null" @@ -271,7 +272,7 @@ When `nextCursor` is `null`, you’ve reached the final page. ### Example: Read a thread -Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. +Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. ```json { "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ea2701d63..44444494d 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -6658,6 +6658,8 @@ async fn read_summary_from_state_db_context_by_thread_id( metadata.cwd, metadata.cli_version, metadata.source, + metadata.agent_nickname, + metadata.agent_role, metadata.git_sha, metadata.git_branch, metadata.git_origin_url, @@ -6678,9 +6680,12 @@ async fn summary_from_thread_list_item( .unwrap_or_else(|| fallback_provider.to_string()); let cwd = it.cwd?; let cli_version = it.cli_version.unwrap_or_default(); - let source = it - .source - .unwrap_or(codex_protocol::protocol::SessionSource::Unknown); + let source = with_thread_spawn_agent_metadata( + it.source + .unwrap_or(codex_protocol::protocol::SessionSource::Unknown), + it.agent_nickname.clone(), + it.agent_role.clone(), + ); return Some(ConversationSummary { conversation_id: thread_id, path: it.path, @@ -6735,13 +6740,17 @@ fn summary_from_state_db_metadata( cwd: PathBuf, cli_version: String, source: String, + agent_nickname: Option, + agent_role: Option, git_sha: Option, git_branch: Option, git_origin_url: Option, ) -> ConversationSummary { let preview = first_user_message.unwrap_or_default(); - let source = serde_json::from_value(serde_json::Value::String(source)) + 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); + let source = with_thread_spawn_agent_metadata(source, agent_nickname, agent_role); let git_info = if git_sha.is_none() && git_branch.is_none() && git_origin_url.is_none() { None } else { @@ -6901,6 +6910,35 @@ fn map_git_info(git_info: &CoreGitInfo) -> ConversationGitInfo { } } +fn with_thread_spawn_agent_metadata( + source: codex_protocol::protocol::SessionSource, + agent_nickname: Option, + agent_role: Option, +) -> codex_protocol::protocol::SessionSource { + if agent_nickname.is_none() && agent_role.is_none() { + return source; + } + + match source { + codex_protocol::protocol::SessionSource::SubAgent( + codex_protocol::protocol::SubAgentSource::ThreadSpawn { + parent_thread_id, + depth, + agent_nickname: existing_agent_nickname, + agent_role: existing_agent_role, + }, + ) => codex_protocol::protocol::SessionSource::SubAgent( + codex_protocol::protocol::SubAgentSource::ThreadSpawn { + parent_thread_id, + depth, + agent_nickname: agent_nickname.or(existing_agent_nickname), + agent_role: agent_role.or(existing_agent_role), + }, + ), + _ => source, + } +} + fn parse_datetime(timestamp: Option<&str>) -> Option> { timestamp.and_then(|ts| { chrono::DateTime::parse_from_rfc3339(ts) @@ -6937,6 +6975,8 @@ fn build_thread_from_snapshot( path, cwd: config_snapshot.cwd.clone(), cli_version: env!("CARGO_PKG_VERSION").to_string(), + agent_nickname: config_snapshot.session_source.get_nickname(), + agent_role: config_snapshot.session_source.get_agent_role(), source: config_snapshot.session_source.clone().into(), git_info: None, turns: Vec::new(), @@ -6975,6 +7015,8 @@ pub(crate) fn summary_to_thread(summary: ConversationSummary) -> Thread { path: Some(path), cwd, cli_version, + agent_nickname: source.get_nickname(), + agent_role: source.get_agent_role(), source: source.into(), git_info, turns: Vec::new(), @@ -6986,6 +7028,7 @@ mod tests { use super::*; use anyhow::Result; use codex_protocol::protocol::SessionSource; + use codex_protocol::protocol::SubAgentSource; use pretty_assertions::assert_eq; use serde_json::json; use std::path::PathBuf; @@ -7131,6 +7174,41 @@ mod tests { Ok(()) } + #[test] + fn summary_from_state_db_metadata_preserves_agent_nickname() -> Result<()> { + let conversation_id = ThreadId::from_string("bfd12a78-5900-467b-9bc5-d3d35df08191")?; + let source = + serde_json::to_string(&SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?, + depth: 1, + agent_nickname: None, + agent_role: None, + }))?; + + let summary = summary_from_state_db_metadata( + conversation_id, + PathBuf::from("/tmp/rollout.jsonl"), + Some("hi".to_string()), + "2025-09-05T16:53:11Z".to_string(), + "2025-09-05T16:53:12Z".to_string(), + "test-provider".to_string(), + PathBuf::from("/"), + "0.0.0".to_string(), + source, + Some("atlas".to_string()), + Some("explorer".to_string()), + None, + None, + None, + ); + + let thread = summary_to_thread(summary); + + assert_eq!(thread.agent_nickname, Some("atlas".to_string())); + assert_eq!(thread.agent_role, Some("explorer".to_string())); + Ok(()) + } + #[tokio::test] async fn removing_one_listener_does_not_cancel_other_subscriptions_for_same_thread() -> Result<()> { diff --git a/codex-rs/app-server/src/filters.rs b/codex-rs/app-server/src/filters.rs index bd784c3dc..a59750961 100644 --- a/codex-rs/app-server/src/filters.rs +++ b/codex-rs/app-server/src/filters.rs @@ -133,6 +133,8 @@ mod tests { let spawn = CoreSessionSource::SubAgent(CoreSubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, + agent_nickname: None, + agent_role: None, }); assert!(source_kind_matches( diff --git a/codex-rs/app-server/src/thread_status.rs b/codex-rs/app-server/src/thread_status.rs index 7576791d3..301c4b1d0 100644 --- a/codex-rs/app-server/src/thread_status.rs +++ b/codex-rs/app-server/src/thread_status.rs @@ -622,6 +622,8 @@ mod tests { path: None, cwd: PathBuf::from("/tmp"), cli_version: "test".to_string(), + agent_nickname: None, + agent_role: None, source, git_info: None, turns: Vec::new(), diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index 14dce02c6..8122e7cd0 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -79,6 +79,8 @@ pub fn create_fake_rollout_with_source( originator: "codex".to_string(), cli_version: "0.0.0".to_string(), source, + agent_nickname: None, + agent_role: None, model_provider: model_provider.map(str::to_string), base_instructions: None, dynamic_tools: None, @@ -158,6 +160,8 @@ pub fn create_fake_rollout_with_text_elements( originator: "codex".to_string(), cli_version: "0.0.0".to_string(), source: SessionSource::Cli, + agent_nickname: None, + agent_role: None, model_provider: model_provider.map(str::to_string), base_instructions: None, dynamic_tools: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_list.rs b/codex-rs/app-server/tests/suite/v2/thread_list.rs index 46e1d170f..1f6e0627e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_list.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_list.rs @@ -582,6 +582,8 @@ async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<() CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, + agent_nickname: None, + agent_role: None, }), )?; @@ -643,6 +645,8 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> { CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, + agent_nickname: None, + agent_role: None, }), )?; let other_id = create_fake_rollout_with_source( diff --git a/codex-rs/core/src/agent/agent_names.txt b/codex-rs/core/src/agent/agent_names.txt new file mode 100644 index 000000000..6b6846193 --- /dev/null +++ b/codex-rs/core/src/agent/agent_names.txt @@ -0,0 +1,100 @@ +Atlas +Nova +Orion +Iris +Milo +Juno +Mars +Vesta +Luna +Sol +Eros +Nyx +Zeus +Hera +Ares +Ajax +Echo +Leto +Rhea +Gaia +Hades +Apollo +Pallas +Circe +Nereus +Triton +Selene +Helios +Castor +Pollux +Astra +Aura +Thalia +Clio +Erato +Euterpe +Urania +Calliope +Minos +Linus +Cato +Brutus +Seneca +Ovid +Virgil +Horace +Remus +Romulus +Titus +Lucian +Felix +Maximus +Octavia +Claudia +Livia +Aelia +Aurel +Cassia +Sabina +Flavia +Numa +Ceres +Diana +Venus +Pluto +Pan +Ramius +Aether +Chaos +Logos +Ethos +Tethys +Chiron +Talos +Icarus +Daedalus +Hyacinth +Adonis +Perseus +Theseus +Argos +Lemnos +Delos +Sparta +Attica +Corinth +Thebes +Ephyra +Achaea +Cyrene +Sidon +Tyre +Ilium +Etrus +Vercos +Aurex +Novian +Helion +Casson +Aurelix \ No newline at end of file diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 847bb7f76..a9ddde632 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -3,7 +3,9 @@ use crate::agent::guards::Guards; use crate::agent::status::is_final; use crate::error::CodexErr; use crate::error::Result as CodexResult; +use crate::find_thread_path_by_id_str; use crate::session_prefix::format_subagent_notification_message; +use crate::state_db; use crate::thread_manager::ThreadManagerState; use codex_protocol::ThreadId; use codex_protocol::protocol::Op; @@ -11,11 +13,20 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TokenUsage; use codex_protocol::user_input::UserInput; -use std::path::PathBuf; use std::sync::Arc; use std::sync::Weak; use tokio::sync::watch; +const AGENT_NAMES: &str = include_str!("agent_names.txt"); + +fn agent_nickname_list() -> Vec<&'static str> { + AGENT_NAMES + .lines() + .map(str::trim) + .filter(|name| !name.is_empty()) + .collect() +} + /// Control-plane handle for multi-agent operations. /// `AgentControl` is held by each session (via `SessionServices`). It provides capability to /// spawn new agents and the inter-agent communication layer. @@ -48,7 +59,24 @@ impl AgentControl { session_source: Option, ) -> CodexResult { let state = self.upgrade()?; - let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; + let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; + let session_source = match session_source { + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth, + agent_role, + .. + })) => { + let agent_nickname = reservation.reserve_agent_nickname(&agent_nickname_list())?; + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth, + agent_nickname: Some(agent_nickname), + agent_role, + })) + } + other => other, + }; let notification_source = session_source.clone(); // The same `AgentControl` is sent to spawn the thread. @@ -77,12 +105,51 @@ impl AgentControl { pub(crate) async fn resume_agent_from_rollout( &self, config: crate::config::Config, - rollout_path: PathBuf, + thread_id: ThreadId, session_source: SessionSource, ) -> CodexResult { let state = self.upgrade()?; - let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; + let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; + let session_source = match session_source { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth, + .. + }) => { + // Collab resume callers rebuild a placeholder ThreadSpawn source. Rehydrate the + // stored nickname/role from sqlite when available; otherwise leave both unset. + let (resumed_agent_nickname, resumed_agent_role) = + if let Some(state_db_ctx) = state_db::get_state_db(&config, None).await { + match state_db_ctx.get_thread(thread_id).await { + Ok(Some(metadata)) => (metadata.agent_nickname, metadata.agent_role), + Ok(None) | Err(_) => (None, None), + } + } else { + (None, None) + }; + let reserved_agent_nickname = resumed_agent_nickname + .as_deref() + .map(|agent_nickname| { + reservation.reserve_agent_nickname_with_preference( + &agent_nickname_list(), + Some(agent_nickname), + ) + }) + .transpose()?; + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth, + agent_nickname: reserved_agent_nickname, + agent_role: resumed_agent_role, + }) + } + other => other, + }; let notification_source = session_source.clone(); + let rollout_path = + find_thread_path_by_id_str(config.codex_home.as_path(), &thread_id.to_string()) + .await? + .ok_or_else(|| CodexErr::ThreadNotFound(thread_id))?; let resumed_thread = state .resume_thread_from_rollout_with_source( @@ -234,6 +301,8 @@ mod tests { use crate::agent::agent_status_from_event; use crate::config::Config; use crate::config::ConfigBuilder; + use crate::config_loader::LoaderOverrides; + use crate::features::Feature; use crate::session_prefix::SUBAGENT_NOTIFICATION_OPEN_TAG; use assert_matches::assert_matches; use codex_protocol::config_types::ModeKind; @@ -261,6 +330,12 @@ mod tests { let config = ConfigBuilder::default() .codex_home(home.path().to_path_buf()) .cli_overrides(cli_overrides) + .loader_overrides(LoaderOverrides { + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + macos_managed_config_requirements_base64: Some(String::new()), + ..LoaderOverrides::default() + }) .build() .await .expect("load default test config"); @@ -441,11 +516,7 @@ mod tests { let control = AgentControl::default(); let (_home, config) = test_config().await; let err = control - .resume_agent_from_rollout( - config, - PathBuf::from("/tmp/missing-rollout.jsonl"), - SessionSource::Exec, - ) + .resume_agent_from_rollout(config, ThreadId::new(), SessionSource::Exec) .await .expect_err("resume_agent should fail without a manager"); assert_eq!( @@ -717,12 +788,6 @@ mod tests { .spawn_agent(config.clone(), text_input("hello"), None) .await .expect("spawn_agent should succeed"); - let rollout_path = manager - .get_thread(resumable_id) - .await - .expect("thread should exist") - .rollout_path() - .expect("rollout path should exist"); let _ = control .shutdown_agent(resumable_id) .await @@ -734,7 +799,7 @@ mod tests { .expect("spawn_agent should succeed for active slot"); let err = control - .resume_agent_from_rollout(config, rollout_path, SessionSource::Exec) + .resume_agent_from_rollout(config, resumable_id, SessionSource::Exec) .await .expect_err("resume should respect max threads"); let CodexErr::AgentLimitReached { @@ -766,9 +831,8 @@ mod tests { ); let control = manager.agent_control(); - let missing_rollout = config.codex_home.join("sessions/missing-rollout.jsonl"); let _ = control - .resume_agent_from_rollout(config.clone(), missing_rollout, SessionSource::Exec) + .resume_agent_from_rollout(config.clone(), ThreadId::new(), SessionSource::Exec) .await .expect_err("resume should fail for missing rollout path"); @@ -795,6 +859,8 @@ mod tests { Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, + agent_nickname: None, + agent_role: Some("explorer".to_string()), })), ) .await @@ -812,4 +878,176 @@ mod tests { assert_eq!(wait_for_subagent_notification(&parent_thread).await, true); } + + #[tokio::test] + async fn spawn_thread_subagent_gets_random_nickname_in_session_source() { + let harness = AgentControlHarness::new().await; + let (parent_thread_id, _parent_thread) = harness.start_thread().await; + + let child_thread_id = harness + .control + .spawn_agent( + harness.config.clone(), + text_input("hello child"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_nickname: None, + agent_role: Some("explorer".to_string()), + })), + ) + .await + .expect("child spawn should succeed"); + + let child_thread = harness + .manager + .get_thread(child_thread_id) + .await + .expect("child thread should be registered"); + let snapshot = child_thread.config_snapshot().await; + + let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: seen_parent_thread_id, + depth, + agent_nickname, + agent_role, + }) = snapshot.session_source + else { + panic!("expected thread-spawn sub-agent source"); + }; + assert_eq!(seen_parent_thread_id, parent_thread_id); + assert_eq!(depth, 1); + assert!(agent_nickname.is_some()); + assert_eq!(agent_role, Some("explorer".to_string())); + } + + #[tokio::test] + async fn resume_thread_subagent_restores_stored_nickname_and_role() { + let (home, mut config) = test_config().await; + config.features.enable(Feature::Sqlite); + let manager = ThreadManager::with_models_provider_and_home_for_tests( + CodexAuth::from_api_key("dummy"), + config.model_provider.clone(), + config.codex_home.clone(), + ); + let control = manager.agent_control(); + let harness = AgentControlHarness { + _home: home, + config, + manager, + control, + }; + let (parent_thread_id, _parent_thread) = harness.start_thread().await; + + let child_thread_id = harness + .control + .spawn_agent( + harness.config.clone(), + text_input("hello child"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_nickname: None, + agent_role: Some("explorer".to_string()), + })), + ) + .await + .expect("child spawn should succeed"); + + let child_thread = harness + .manager + .get_thread(child_thread_id) + .await + .expect("child thread should exist"); + let mut status_rx = harness + .control + .subscribe_status(child_thread_id) + .await + .expect("status subscription should succeed"); + if matches!(status_rx.borrow().clone(), AgentStatus::PendingInit) { + timeout(Duration::from_secs(5), async { + loop { + status_rx + .changed() + .await + .expect("child status should advance past pending init"); + if !matches!(status_rx.borrow().clone(), AgentStatus::PendingInit) { + break; + } + } + }) + .await + .expect("child should initialize before shutdown"); + } + let original_snapshot = child_thread.config_snapshot().await; + let original_nickname = original_snapshot + .session_source + .get_nickname() + .expect("spawned sub-agent should have a nickname"); + let state_db = child_thread + .state_db() + .expect("sqlite state db should be available for nickname resume test"); + timeout(Duration::from_secs(5), async { + loop { + if let Ok(Some(metadata)) = state_db.get_thread(child_thread_id).await + && metadata.agent_nickname.is_some() + && metadata.agent_role.as_deref() == Some("explorer") + { + break; + } + sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("child thread metadata should be persisted to sqlite before shutdown"); + + let _ = harness + .control + .shutdown_agent(child_thread_id) + .await + .expect("child shutdown should submit"); + + let resumed_thread_id = harness + .control + .resume_agent_from_rollout( + harness.config.clone(), + child_thread_id, + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_nickname: None, + agent_role: None, + }), + ) + .await + .expect("resume should succeed"); + assert_eq!(resumed_thread_id, child_thread_id); + + let resumed_snapshot = harness + .manager + .get_thread(resumed_thread_id) + .await + .expect("resumed child thread should exist") + .config_snapshot() + .await; + let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: resumed_parent_thread_id, + depth: resumed_depth, + agent_nickname: resumed_nickname, + agent_role: resumed_role, + }) = resumed_snapshot.session_source + else { + panic!("expected thread-spawn sub-agent source"); + }; + assert_eq!(resumed_parent_thread_id, parent_thread_id); + assert_eq!(resumed_depth, 1); + assert_eq!(resumed_nickname, Some(original_nickname)); + assert_eq!(resumed_role, Some("explorer".to_string())); + + let _ = harness + .control + .shutdown_agent(resumed_thread_id) + .await + .expect("resumed child shutdown should submit"); + } } diff --git a/codex-rs/core/src/agent/guards.rs b/codex-rs/core/src/agent/guards.rs index 1964169cd..223373c6d 100644 --- a/codex-rs/core/src/agent/guards.rs +++ b/codex-rs/core/src/agent/guards.rs @@ -3,6 +3,8 @@ use crate::error::Result; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use rand::prelude::IndexedRandom; +use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; use std::sync::Mutex; @@ -17,10 +19,18 @@ use std::sync::atomic::Ordering; /// is). #[derive(Default)] pub(crate) struct Guards { - threads_set: Mutex>, + active_agents: Mutex, total_count: AtomicUsize, } +#[derive(Default)] +struct ActiveAgents { + threads_set: HashSet, + thread_agent_nicknames: HashMap, + used_agent_nicknames: HashSet, + nickname_reset_count: usize, +} + fn session_depth(session_source: &SessionSource) -> i32 { match session_source { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => *depth, @@ -52,28 +62,69 @@ impl Guards { Ok(SpawnReservation { state: Arc::clone(self), active: true, + reserved_agent_nickname: None, }) } pub(crate) fn release_spawned_thread(&self, thread_id: ThreadId) { let removed = { - let mut threads = self - .threads_set + let mut active_agents = self + .active_agents .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - threads.remove(&thread_id) + let removed = active_agents.threads_set.remove(&thread_id); + active_agents.thread_agent_nicknames.remove(&thread_id); + removed }; if removed { self.total_count.fetch_sub(1, Ordering::AcqRel); } } - fn register_spawned_thread(&self, thread_id: ThreadId) { - let mut threads = self - .threads_set + fn register_spawned_thread(&self, thread_id: ThreadId, agent_nickname: Option) { + let mut active_agents = self + .active_agents .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - threads.insert(thread_id); + active_agents.threads_set.insert(thread_id); + if let Some(agent_nickname) = agent_nickname { + active_agents + .used_agent_nicknames + .insert(agent_nickname.clone()); + active_agents + .thread_agent_nicknames + .insert(thread_id, agent_nickname); + } + } + + fn reserve_agent_nickname(&self, names: &[&str], preferred: Option<&str>) -> Option { + let mut active_agents = self + .active_agents + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let agent_nickname = if let Some(preferred) = preferred { + preferred.to_string() + } else { + if names.is_empty() { + return None; + } + let available_names: Vec<&str> = names + .iter() + .copied() + .filter(|name| !active_agents.used_agent_nicknames.contains(*name)) + .collect(); + if let Some(name) = available_names.choose(&mut rand::rng()) { + (*name).to_string() + } else { + active_agents.used_agent_nicknames.clear(); + active_agents.nickname_reset_count += 1; + names.choose(&mut rand::rng())?.to_string() + } + }; + active_agents + .used_agent_nicknames + .insert(agent_nickname.clone()); + Some(agent_nickname) } fn try_increment_spawned(&self, max_threads: usize) -> bool { @@ -98,11 +149,41 @@ impl Guards { pub(crate) struct SpawnReservation { state: Arc, active: bool, + reserved_agent_nickname: Option, } impl SpawnReservation { - pub(crate) fn commit(mut self, thread_id: ThreadId) { - self.state.register_spawned_thread(thread_id); + pub(crate) fn reserve_agent_nickname(&mut self, names: &[&str]) -> Result { + self.reserve_agent_nickname_with_preference(names, None) + } + + pub(crate) fn reserve_agent_nickname_with_preference( + &mut self, + names: &[&str], + preferred: Option<&str>, + ) -> Result { + let agent_nickname = self + .state + .reserve_agent_nickname(names, preferred) + .ok_or_else(|| { + CodexErr::UnsupportedOperation("no available agent nicknames".to_string()) + })?; + self.reserved_agent_nickname = Some(agent_nickname.clone()); + Ok(agent_nickname) + } + + pub(crate) fn commit(self, thread_id: ThreadId) { + self.commit_with_agent_nickname(thread_id, None); + } + + pub(crate) fn commit_with_agent_nickname( + mut self, + thread_id: ThreadId, + agent_nickname: Option, + ) { + let agent_nickname = self.reserved_agent_nickname.take().or(agent_nickname); + self.state + .register_spawned_thread(thread_id, agent_nickname); self.active = false; } } @@ -130,6 +211,8 @@ mod tests { let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: ThreadId::new(), depth: 1, + agent_nickname: None, + agent_role: None, }); let child_depth = next_thread_spawn_depth(&session_source); assert_eq!(child_depth, 2); @@ -232,4 +315,83 @@ mod tests { .expect("slot released after second thread removal"); drop(reservation); } + + #[test] + fn failed_spawn_keeps_nickname_marked_used() { + let guards = Arc::new(Guards::default()); + let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot"); + let agent_nickname = reservation + .reserve_agent_nickname(&["alpha"]) + .expect("reserve agent name"); + assert_eq!(agent_nickname, "alpha"); + drop(reservation); + + let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot"); + let agent_nickname = reservation + .reserve_agent_nickname(&["alpha", "beta"]) + .expect("unused name should still be preferred"); + assert_eq!(agent_nickname, "beta"); + } + + #[test] + fn agent_nickname_resets_used_pool_when_exhausted() { + let guards = Arc::new(Guards::default()); + let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot"); + let first_name = first + .reserve_agent_nickname(&["alpha"]) + .expect("reserve first agent name"); + let first_id = ThreadId::new(); + first.commit(first_id); + assert_eq!(first_name, "alpha"); + + let mut second = guards + .reserve_spawn_slot(None) + .expect("reserve second slot"); + let second_name = second + .reserve_agent_nickname(&["alpha"]) + .expect("name should be reused after pool reset"); + assert_eq!(second_name, "alpha"); + let active_agents = guards + .active_agents + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + assert_eq!(active_agents.nickname_reset_count, 1); + } + + #[test] + fn released_nickname_stays_used_until_pool_reset() { + let guards = Arc::new(Guards::default()); + + let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot"); + let first_name = first + .reserve_agent_nickname(&["alpha"]) + .expect("reserve first agent name"); + let first_id = ThreadId::new(); + first.commit(first_id); + assert_eq!(first_name, "alpha"); + + guards.release_spawned_thread(first_id); + + let mut second = guards + .reserve_spawn_slot(None) + .expect("reserve second slot"); + let second_name = second + .reserve_agent_nickname(&["alpha", "beta"]) + .expect("released name should still be marked used"); + assert_eq!(second_name, "beta"); + let second_id = ThreadId::new(); + second.commit(second_id); + guards.release_spawned_thread(second_id); + + let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot"); + let third_name = third + .reserve_agent_nickname(&["alpha", "beta"]) + .expect("pool reset should permit a duplicate"); + assert!(third_name == "alpha" || third_name == "beta"); + let active_agents = guards + .active_agents + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + assert_eq!(active_agents.nickname_reset_count, 1); + } } diff --git a/codex-rs/core/src/personality_migration.rs b/codex-rs/core/src/personality_migration.rs index 4ec78696b..c207295e9 100644 --- a/codex-rs/core/src/personality_migration.rs +++ b/codex-rs/core/src/personality_migration.rs @@ -172,6 +172,8 @@ mod tests { originator: "test_originator".to_string(), cli_version: "test_version".to_string(), source: SessionSource::Cli, + agent_nickname: None, + agent_role: None, model_provider: None, base_instructions: None, dynamic_tools: None, diff --git a/codex-rs/core/src/rollout/list.rs b/codex-rs/core/src/rollout/list.rs index 9b2b00c78..8c9ee7afe 100644 --- a/codex-rs/core/src/rollout/list.rs +++ b/codex-rs/core/src/rollout/list.rs @@ -39,7 +39,7 @@ pub struct ThreadsPage { } /// Summary information for a thread rollout file. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Default)] pub struct ThreadItem { /// Absolute path to the rollout file. pub path: PathBuf, @@ -57,6 +57,10 @@ pub struct ThreadItem { pub git_origin_url: Option, /// Session source from session metadata. pub source: Option, + /// Random unique nickname from session metadata for AgentControl-spawned sub-agents. + pub agent_nickname: Option, + /// Role (agent_role) from session metadata for AgentControl-spawned sub-agents. + pub agent_role: Option, /// Model provider from session metadata. pub model_provider: Option, /// CLI version from session metadata. @@ -87,6 +91,8 @@ struct HeadTailSummary { git_sha: Option, git_origin_url: Option, source: Option, + agent_nickname: Option, + agent_role: Option, model_provider: Option, cli_version: Option, created_at: Option, @@ -715,6 +721,8 @@ async fn build_thread_item( git_sha, git_origin_url, source, + agent_nickname, + agent_role, model_provider, cli_version, created_at, @@ -733,6 +741,8 @@ async fn build_thread_item( git_sha, git_origin_url, source, + agent_nickname, + agent_role, model_provider, cli_version, created_at, @@ -1017,6 +1027,8 @@ async fn read_head_summary(path: &Path, head_limit: usize) -> io::Result { if !summary.saw_session_meta { summary.source = Some(session_meta_line.meta.source.clone()); + summary.agent_nickname = session_meta_line.meta.agent_nickname.clone(); + summary.agent_role = session_meta_line.meta.agent_role.clone(); summary.model_provider = session_meta_line.meta.model_provider.clone(); summary.thread_id = Some(session_meta_line.meta.id); summary.cwd = Some(session_meta_line.meta.cwd.clone()); diff --git a/codex-rs/core/src/rollout/metadata.rs b/codex-rs/core/src/rollout/metadata.rs index 729887775..9196931da 100644 --- a/codex-rs/core/src/rollout/metadata.rs +++ b/codex-rs/core/src/rollout/metadata.rs @@ -47,6 +47,8 @@ pub(crate) fn builder_from_session_meta( session_meta.meta.source.clone(), ); builder.model_provider = session_meta.meta.model_provider.clone(); + builder.agent_nickname = session_meta.meta.agent_nickname.clone(); + builder.agent_role = session_meta.meta.agent_role.clone(); builder.cwd = session_meta.meta.cwd.clone(); builder.cli_version = Some(session_meta.meta.cli_version.clone()); builder.sandbox_policy = SandboxPolicy::new_read_only_policy(); @@ -512,6 +514,8 @@ mod tests { originator: "cli".to_string(), cli_version: "0.0.0".to_string(), source: SessionSource::default(), + agent_nickname: None, + agent_role: None, model_provider: Some("openai".to_string()), base_instructions: None, dynamic_tools: None, @@ -660,6 +664,8 @@ mod tests { originator: "cli".to_string(), cli_version: "0.0.0".to_string(), source: SessionSource::default(), + agent_nickname: None, + agent_role: None, model_provider: Some("test-provider".to_string()), base_instructions: None, dynamic_tools: None, diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index eab442743..7e639e382 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -391,6 +391,8 @@ impl RolloutRecorder { cwd: config.cwd.clone(), originator: originator().value, cli_version: env!("CARGO_PKG_VERSION").to_string(), + agent_nickname: source.get_nickname(), + agent_role: source.get_agent_role(), source, model_provider: Some(config.model_provider_id.clone()), base_instructions: Some(base_instructions), @@ -929,9 +931,12 @@ impl From for ThreadsPage { git_sha: item.git_sha, git_origin_url: item.git_origin_url, source: Some( - serde_json::from_value(Value::String(item.source)) + serde_json::from_str(item.source.as_str()) + .or_else(|_| serde_json::from_value(Value::String(item.source))) .unwrap_or(SessionSource::Unknown), ), + agent_nickname: item.agent_nickname, + agent_role: item.agent_role, model_provider: Some(item.model_provider), cli_version: Some(item.cli_version), created_at: Some(item.created_at.to_rfc3339_opts(SecondsFormat::Secs, true)), diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index b97edb0f6..0eabaee74 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -560,6 +560,8 @@ async fn test_list_conversations_latest_first() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some("2025-01-03T12-00-00".into()), @@ -574,6 +576,8 @@ async fn test_list_conversations_latest_first() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some("2025-01-02T12-00-00".into()), @@ -588,6 +592,8 @@ async fn test_list_conversations_latest_first() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some("2025-01-01T12-00-00".into()), @@ -695,6 +701,8 @@ async fn test_pagination_cursor() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some("2025-03-05T09-00-00".into()), @@ -709,6 +717,8 @@ async fn test_pagination_cursor() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some("2025-03-04T09-00-00".into()), @@ -759,6 +769,8 @@ async fn test_pagination_cursor() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some("2025-03-03T09-00-00".into()), @@ -773,6 +785,8 @@ async fn test_pagination_cursor() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some("2025-03-02T09-00-00".into()), @@ -814,6 +828,8 @@ async fn test_pagination_cursor() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some("2025-03-01T09-00-00".into()), @@ -894,6 +910,8 @@ async fn test_get_thread_contents() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some(ts.into()), @@ -1086,6 +1104,8 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { originator: "test_originator".into(), cli_version: "test_version".into(), source: SessionSource::VSCode, + agent_nickname: None, + agent_role: None, model_provider: Some("test-provider".into()), base_instructions: None, dynamic_tools: None, @@ -1203,6 +1223,8 @@ async fn test_stable_ordering_same_second_pagination() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some(ts.to_string()), @@ -1217,6 +1239,8 @@ async fn test_stable_ordering_same_second_pagination() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some(ts.to_string()), @@ -1258,6 +1282,8 @@ async fn test_stable_ordering_same_second_pagination() { git_sha: None, git_origin_url: None, source: Some(SessionSource::VSCode), + agent_nickname: None, + agent_role: None, model_provider: Some(TEST_PROVIDER.to_string()), cli_version: Some("test_version".to_string()), created_at: Some(ts.to_string()), diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index 38aa22b6c..dfc82ce52 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -156,7 +156,11 @@ mod spawn { .spawn_agent( config, input_items, - Some(thread_spawn_source(session.conversation_id, child_depth)), + Some(thread_spawn_source( + session.conversation_id, + child_depth, + role_name, + )), ) .await .map_err(collab_spawn_error); @@ -284,7 +288,6 @@ mod send_input { mod resume_agent { use super::*; use crate::agent::next_thread_spawn_depth; - use crate::rollout::find_thread_path_by_id_str; use std::sync::Arc; #[derive(Debug, Deserialize)] @@ -331,15 +334,7 @@ mod resume_agent { .await; let error = if matches!(status, AgentStatus::NotFound) { // If the thread is no longer active, attempt to restore it from rollout. - match try_resume_closed_agent( - &session, - &turn, - receiver_thread_id, - &args.id, - child_depth, - ) - .await - { + match try_resume_closed_agent(&session, &turn, receiver_thread_id, child_depth).await { Ok(resumed_status) => { status = resumed_status; None @@ -388,33 +383,16 @@ mod resume_agent { session: &Arc, turn: &Arc, receiver_thread_id: ThreadId, - receiver_id: &str, child_depth: i32, ) -> Result { - let rollout_path = find_thread_path_by_id_str( - turn.config.codex_home.as_path(), - receiver_id, - ) - .await - .map_err(|err| { - FunctionCallError::RespondToModel(format!( - "tool failed: failed to locate rollout for agent {receiver_thread_id}: {err}" - )) - })? - .ok_or_else(|| { - FunctionCallError::RespondToModel(format!( - "agent with id {receiver_thread_id} not found" - )) - })?; - let config = build_agent_resume_config(turn.as_ref(), child_depth)?; let resumed_thread_id = session .services .agent_control .resume_agent_from_rollout( config, - rollout_path, - thread_spawn_source(session.conversation_id, child_depth), + receiver_thread_id, + thread_spawn_source(session.conversation_id, child_depth, None), ) .await .map_err(|err| collab_agent_error(receiver_thread_id, err))?; @@ -733,10 +711,16 @@ fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError { } } -fn thread_spawn_source(parent_thread_id: ThreadId, depth: i32) -> SessionSource { +fn thread_spawn_source( + parent_thread_id: ThreadId, + depth: i32, + agent_role: Option<&str>, +) -> SessionSource { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth, + agent_nickname: None, + agent_role: agent_role.map(str::to_string), }) } @@ -1068,6 +1052,8 @@ mod tests { turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: session.conversation_id, depth: DEFAULT_AGENT_MAX_DEPTH, + agent_nickname: None, + agent_role: None, }); let invocation = invocation( @@ -1104,6 +1090,8 @@ mod tests { turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: session.conversation_id, depth: DEFAULT_AGENT_MAX_DEPTH, + agent_nickname: None, + agent_role: None, }); let invocation = invocation( @@ -1487,6 +1475,8 @@ mod tests { turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: session.conversation_id, depth: DEFAULT_AGENT_MAX_DEPTH, + agent_nickname: None, + agent_role: None, }); let invocation = invocation( diff --git a/codex-rs/core/tests/suite/personality_migration.rs b/codex-rs/core/tests/suite/personality_migration.rs index 2ca7aeeef..dfff16ea0 100644 --- a/codex-rs/core/tests/suite/personality_migration.rs +++ b/codex-rs/core/tests/suite/personality_migration.rs @@ -66,6 +66,8 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R originator: "test_originator".to_string(), cli_version: "test_version".to_string(), source: SessionSource::Cli, + agent_nickname: None, + agent_role: None, model_provider: None, base_instructions: None, dynamic_tools: None, @@ -107,6 +109,8 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re originator: "test_originator".to_string(), cli_version: "test_version".to_string(), source: SessionSource::Cli, + agent_nickname: None, + agent_role: None, model_provider: None, base_instructions: None, dynamic_tools: None, diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index b37683a43..8f0e5834e 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -123,6 +123,8 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { originator: "test".to_string(), cli_version: "test".to_string(), source: SessionSource::default(), + agent_nickname: None, + agent_role: None, model_provider: None, base_instructions: None, dynamic_tools: Some(dynamic_tools_for_hook), diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 476f90208..a759f1512 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1861,6 +1861,10 @@ pub enum SubAgentSource { ThreadSpawn { parent_thread_id: ThreadId, depth: i32, + #[serde(default)] + agent_nickname: Option, + #[serde(default, alias = "agent_type")] + agent_role: Option, }, MemoryConsolidation, Other(String), @@ -1879,6 +1883,26 @@ impl fmt::Display for SessionSource { } } +impl SessionSource { + pub fn get_nickname(&self) -> Option { + match self { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => { + agent_nickname.clone() + } + _ => None, + } + } + + pub fn get_agent_role(&self) -> Option { + match self { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. }) => { + agent_role.clone() + } + _ => None, + } + } +} + impl fmt::Display for SubAgentSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -1888,6 +1912,7 @@ impl fmt::Display for SubAgentSource { SubAgentSource::ThreadSpawn { parent_thread_id, depth, + .. } => { write!(f, "thread_spawn_{parent_thread_id}_d{depth}") } @@ -1912,6 +1937,12 @@ pub struct SessionMeta { pub cli_version: String, #[serde(default)] pub source: SessionSource, + /// Optional random unique nickname assigned to an AgentControl-spawned sub-agent. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_nickname: Option, + /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. + #[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")] + pub agent_role: Option, pub model_provider: Option, /// base_instructions for the session. This *should* always be present when creating a new session, /// but may be missing for older sessions. If not present, fall back to rendering the base_instructions @@ -1931,6 +1962,8 @@ impl Default for SessionMeta { originator: String::new(), cli_version: String::new(), source: SessionSource::default(), + agent_nickname: None, + agent_role: None, model_provider: None, base_instructions: None, dynamic_tools: None, diff --git a/codex-rs/state/migrations/0013_threads_agent_nickname.sql b/codex-rs/state/migrations/0013_threads_agent_nickname.sql new file mode 100644 index 000000000..80394c88b --- /dev/null +++ b/codex-rs/state/migrations/0013_threads_agent_nickname.sql @@ -0,0 +1,2 @@ +ALTER TABLE threads ADD COLUMN agent_nickname TEXT; +ALTER TABLE threads ADD COLUMN agent_role TEXT; diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index f8f9cb525..0ba3df679 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -37,6 +37,8 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi } metadata.id = meta_line.meta.id; metadata.source = enum_to_string(&meta_line.meta.source); + metadata.agent_nickname = meta_line.meta.agent_nickname.clone(); + metadata.agent_role = meta_line.meta.agent_role.clone(); if let Some(provider) = meta_line.meta.model_provider.as_deref() { metadata.model_provider = provider.to_string(); } @@ -216,6 +218,8 @@ mod tests { created_at, updated_at: created_at, source: "cli".to_string(), + agent_nickname: None, + agent_role: None, model_provider: "openai".to_string(), cwd: PathBuf::from("/tmp"), cli_version: "0.0.0".to_string(), diff --git a/codex-rs/state/src/model/thread_metadata.rs b/codex-rs/state/src/model/thread_metadata.rs index d29bf0760..9533b8fed 100644 --- a/codex-rs/state/src/model/thread_metadata.rs +++ b/codex-rs/state/src/model/thread_metadata.rs @@ -62,6 +62,10 @@ pub struct ThreadMetadata { pub updated_at: DateTime, /// The session source (stringified enum). pub source: String, + /// Optional random unique nickname assigned to an AgentControl-spawned sub-agent. + pub agent_nickname: Option, + /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. + pub agent_role: Option, /// The model provider identifier. pub model_provider: String, /// The working directory for the thread. @@ -101,6 +105,10 @@ pub struct ThreadMetadataBuilder { pub updated_at: Option>, /// The session source. pub source: SessionSource, + /// Optional random unique nickname assigned to the session. + pub agent_nickname: Option, + /// Optional role (agent_role) assigned to the session. + pub agent_role: Option, /// The model provider identifier, if known. pub model_provider: Option, /// The working directory for the thread. @@ -135,6 +143,8 @@ impl ThreadMetadataBuilder { created_at, updated_at: None, source, + agent_nickname: None, + agent_role: None, model_provider: None, cwd: PathBuf::new(), cli_version: None, @@ -163,6 +173,8 @@ impl ThreadMetadataBuilder { created_at, updated_at, source, + agent_nickname: self.agent_nickname.clone(), + agent_role: self.agent_role.clone(), model_provider: self .model_provider .clone() @@ -201,6 +213,12 @@ impl ThreadMetadata { if self.source != other.source { diffs.push("source"); } + if self.agent_nickname != other.agent_nickname { + diffs.push("agent_nickname"); + } + if self.agent_role != other.agent_role { + diffs.push("agent_role"); + } if self.model_provider != other.model_provider { diffs.push("model_provider"); } @@ -252,6 +270,8 @@ pub(crate) struct ThreadRow { created_at: i64, updated_at: i64, source: String, + agent_nickname: Option, + agent_role: Option, model_provider: String, cwd: String, cli_version: String, @@ -274,6 +294,8 @@ impl ThreadRow { created_at: row.try_get("created_at")?, updated_at: row.try_get("updated_at")?, source: row.try_get("source")?, + agent_nickname: row.try_get("agent_nickname")?, + agent_role: row.try_get("agent_role")?, model_provider: row.try_get("model_provider")?, cwd: row.try_get("cwd")?, cli_version: row.try_get("cli_version")?, @@ -300,6 +322,8 @@ impl TryFrom for ThreadMetadata { created_at, updated_at, source, + agent_nickname, + agent_role, model_provider, cwd, cli_version, @@ -319,6 +343,8 @@ impl TryFrom for ThreadMetadata { created_at: epoch_seconds_to_datetime(created_at)?, updated_at: epoch_seconds_to_datetime(updated_at)?, source, + agent_nickname, + agent_role, model_provider, cwd: PathBuf::from(cwd), cli_version, diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 4570532e6..3635bcb86 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -211,6 +211,8 @@ SELECT created_at, updated_at, source, + agent_nickname, + agent_role, model_provider, cwd, cli_version, @@ -310,6 +312,8 @@ SELECT created_at, updated_at, source, + agent_nickname, + agent_role, model_provider, cwd, cli_version, @@ -691,6 +695,8 @@ INSERT INTO threads ( created_at, updated_at, source, + agent_nickname, + agent_role, model_provider, cwd, cli_version, @@ -704,12 +710,14 @@ INSERT INTO threads ( git_sha, git_branch, git_origin_url -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET rollout_path = excluded.rollout_path, created_at = excluded.created_at, updated_at = excluded.updated_at, source = excluded.source, + agent_nickname = excluded.agent_nickname, + agent_role = excluded.agent_role, model_provider = excluded.model_provider, cwd = excluded.cwd, cli_version = excluded.cli_version, @@ -730,6 +738,8 @@ ON CONFLICT(id) DO UPDATE SET .bind(datetime_to_epoch_seconds(metadata.created_at)) .bind(datetime_to_epoch_seconds(metadata.updated_at)) .bind(metadata.source.as_str()) + .bind(metadata.agent_nickname.as_deref()) + .bind(metadata.agent_role.as_deref()) .bind(metadata.model_provider.as_str()) .bind(metadata.cwd.display().to_string()) .bind(metadata.cli_version.as_str()) @@ -3092,6 +3102,8 @@ VALUES (?, ?, ?, ?, ?) created_at: now, updated_at: now, source: "cli".to_string(), + agent_nickname: None, + agent_role: None, model_provider: "test-provider".to_string(), cwd, cli_version: "0.0.0".to_string(), diff --git a/codex-rs/state/src/runtime/memories.rs b/codex-rs/state/src/runtime/memories.rs index c2a7f1fe9..f2a50e3c9 100644 --- a/codex-rs/state/src/runtime/memories.rs +++ b/codex-rs/state/src/runtime/memories.rs @@ -95,6 +95,8 @@ SELECT created_at, updated_at, source, + agent_nickname, + agent_role, model_provider, cwd, cli_version, diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index c9aa9aece..6cb1387a4 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -1327,17 +1327,10 @@ mod tests { fn make_item(path: &str, ts: &str, preview: &str) -> ThreadItem { ThreadItem { path: PathBuf::from(path), - thread_id: None, first_user_message: Some(preview.to_string()), - cwd: None, - git_branch: None, - git_sha: None, - git_origin_url: None, - source: None, - model_provider: None, - cli_version: None, created_at: Some(ts.to_string()), updated_at: Some(ts.to_string()), + ..Default::default() } } @@ -1462,17 +1455,10 @@ mod tests { fn head_to_row_uses_first_user_message() { let item = ThreadItem { path: PathBuf::from("/tmp/a.jsonl"), - thread_id: None, first_user_message: Some("real question".to_string()), - cwd: None, - git_branch: None, - git_sha: None, - git_origin_url: None, - source: None, - model_provider: None, - cli_version: None, created_at: Some("2025-01-01T00:00:00Z".into()), updated_at: Some("2025-01-01T00:00:00Z".into()), + ..Default::default() }; let row = head_to_row(&item); assert_eq!(row.preview, "real question"); @@ -1483,31 +1469,17 @@ mod tests { // Construct two items with different timestamps and real user text. let a = ThreadItem { path: PathBuf::from("/tmp/a.jsonl"), - thread_id: None, first_user_message: Some("A".to_string()), - cwd: None, - git_branch: None, - git_sha: None, - git_origin_url: None, - source: None, - model_provider: None, - cli_version: None, created_at: Some("2025-01-01T00:00:00Z".into()), updated_at: Some("2025-01-01T00:00:00Z".into()), + ..Default::default() }; let b = ThreadItem { path: PathBuf::from("/tmp/b.jsonl"), - thread_id: None, first_user_message: Some("B".to_string()), - cwd: None, - git_branch: None, - git_sha: None, - git_origin_url: None, - source: None, - model_provider: None, - cli_version: None, created_at: Some("2025-01-02T00:00:00Z".into()), updated_at: Some("2025-01-02T00:00:00Z".into()), + ..Default::default() }; let rows = rows_from_items(vec![a, b]); assert_eq!(rows.len(), 2); @@ -1520,17 +1492,10 @@ mod tests { fn row_uses_tail_timestamp_for_updated_at() { let item = ThreadItem { path: PathBuf::from("/tmp/a.jsonl"), - thread_id: None, first_user_message: Some("Hello".to_string()), - cwd: None, - git_branch: None, - git_sha: None, - git_origin_url: None, - source: None, - model_provider: None, - cli_version: None, created_at: Some("2025-01-01T00:00:00Z".into()), updated_at: Some("2025-01-01T01:00:00Z".into()), + ..Default::default() }; let row = head_to_row(&item);