mirror of
https://github.com/openai/codex.git
synced 2026-04-02 21:41:37 +03:00
Compare commits
2 Commits
dev/friel/
...
jif/feedba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d5b23bd2 | ||
|
|
7f3597529c |
@@ -188,7 +188,7 @@ Example with notification opt-out:
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination.
|
||||
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); for v2 sub-agent threads, logs and rollout attachments are collected from the current loaded agent subtree sharing the same canonical agent-path prefix; returns the tracking thread id.
|
||||
- `config/read` — fetch the effective config on disk after resolving config layering.
|
||||
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home).
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home).
|
||||
|
||||
@@ -7215,20 +7215,35 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
let snapshot = self.feedback.snapshot(conversation_id);
|
||||
let thread_id = snapshot.thread_id.clone();
|
||||
let feedback_thread_ids = match conversation_id {
|
||||
Some(conversation_id) => self.resolve_feedback_thread_ids(conversation_id).await,
|
||||
None => Vec::new(),
|
||||
};
|
||||
let sqlite_feedback_logs = if include_logs {
|
||||
if let Some(log_db) = self.log_db.as_ref() {
|
||||
log_db.flush().await;
|
||||
}
|
||||
let state_db_ctx = get_state_db(&self.config).await;
|
||||
match (state_db_ctx.as_ref(), conversation_id) {
|
||||
(Some(state_db_ctx), Some(conversation_id)) => {
|
||||
let thread_id_text = conversation_id.to_string();
|
||||
match state_db_ctx.query_feedback_logs(&thread_id_text).await {
|
||||
match state_db_ctx.as_ref() {
|
||||
Some(state_db_ctx) if !feedback_thread_ids.is_empty() => {
|
||||
let feedback_thread_id_text = feedback_thread_ids
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
let feedback_thread_id_refs = feedback_thread_id_text
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
match state_db_ctx
|
||||
.query_feedback_logs_for_threads(&feedback_thread_id_refs)
|
||||
.await
|
||||
{
|
||||
Ok(logs) if logs.is_empty() => None,
|
||||
Ok(logs) => Some(logs),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"failed to query feedback logs from sqlite for thread_id={thread_id_text}: {err}"
|
||||
thread_ids = ?feedback_thread_id_text,
|
||||
"failed to query feedback logs from sqlite for feedback upload: {err}"
|
||||
);
|
||||
None
|
||||
}
|
||||
@@ -7240,15 +7255,12 @@ impl CodexMessageProcessor {
|
||||
None
|
||||
};
|
||||
|
||||
let validated_rollout_path = if include_logs {
|
||||
match conversation_id {
|
||||
Some(conv_id) => self.resolve_rollout_path(conv_id).await,
|
||||
None => None,
|
||||
}
|
||||
let rollout_paths = if include_logs {
|
||||
self.resolve_feedback_rollout_paths(&feedback_thread_ids).await
|
||||
} else {
|
||||
None
|
||||
Vec::new()
|
||||
};
|
||||
let mut attachment_paths = validated_rollout_path.into_iter().collect::<Vec<_>>();
|
||||
let mut attachment_paths = rollout_paths;
|
||||
if let Some(extra_log_files) = extra_log_files {
|
||||
attachment_paths.extend(extra_log_files);
|
||||
}
|
||||
@@ -7375,6 +7387,38 @@ impl CodexMessageProcessor {
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_feedback_thread_ids(&self, conversation_id: ThreadId) -> Vec<ThreadId> {
|
||||
let Ok(thread) = self.thread_manager.get_thread(conversation_id).await else {
|
||||
return vec![conversation_id];
|
||||
};
|
||||
let session_source = thread.config_snapshot().await.session_source;
|
||||
let Some(agent_path) = session_source.get_agent_path() else {
|
||||
return vec![conversation_id];
|
||||
};
|
||||
if agent_path.is_root() {
|
||||
return vec![conversation_id];
|
||||
}
|
||||
|
||||
match self.thread_manager.list_subtree_thread_ids(conversation_id).await {
|
||||
Ok(thread_ids) if !thread_ids.is_empty() => thread_ids,
|
||||
Ok(_) | Err(_) => vec![conversation_id],
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_feedback_rollout_paths(&self, thread_ids: &[ThreadId]) -> Vec<PathBuf> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut rollout_paths = Vec::new();
|
||||
for thread_id in thread_ids {
|
||||
let Some(path) = self.resolve_rollout_path(*thread_id).await else {
|
||||
continue;
|
||||
};
|
||||
if seen.insert(path.clone()) {
|
||||
rollout_paths.push(path);
|
||||
}
|
||||
}
|
||||
rollout_paths
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -37,6 +37,7 @@ use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::cmp::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::watch;
|
||||
@@ -719,18 +720,7 @@ impl AgentControl {
|
||||
.transpose()?;
|
||||
|
||||
let mut live_agents = self.state.live_agents();
|
||||
live_agents.sort_by(|left, right| {
|
||||
left.agent_path
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.cmp(right.agent_path.as_deref().unwrap_or_default())
|
||||
.then_with(|| {
|
||||
left.agent_id
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_default()
|
||||
.cmp(&right.agent_id.map(|id| id.to_string()).unwrap_or_default())
|
||||
})
|
||||
});
|
||||
live_agents.sort_by(compare_agent_metadata);
|
||||
|
||||
let root_path = AgentPath::root();
|
||||
let mut agents = Vec::with_capacity(live_agents.len().saturating_add(1));
|
||||
@@ -780,6 +770,23 @@ impl AgentControl {
|
||||
Ok(agents)
|
||||
}
|
||||
|
||||
pub(crate) fn list_agent_subtree_thread_ids_for_thread(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
) -> Vec<ThreadId> {
|
||||
let Some(agent_path) = self
|
||||
.get_agent_metadata(thread_id)
|
||||
.and_then(|metadata| metadata.agent_path)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if agent_path.is_root() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.list_agent_thread_ids_for_prefix(&agent_path)
|
||||
}
|
||||
|
||||
/// Starts a detached watcher for sub-agents spawned from another thread.
|
||||
///
|
||||
/// This is only enabled for `SubAgentSource::ThreadSpawn`, where a parent thread exists and
|
||||
@@ -1072,6 +1079,31 @@ fn agent_matches_prefix(agent_path: Option<&AgentPath>, prefix: &AgentPath) -> b
|
||||
})
|
||||
}
|
||||
|
||||
fn compare_agent_metadata(left: &AgentMetadata, right: &AgentMetadata) -> Ordering {
|
||||
left.agent_path
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.cmp(right.agent_path.as_deref().unwrap_or_default())
|
||||
.then_with(|| {
|
||||
left.agent_id
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_default()
|
||||
.cmp(&right.agent_id.map(|id| id.to_string()).unwrap_or_default())
|
||||
})
|
||||
}
|
||||
|
||||
impl AgentControl {
|
||||
fn list_agent_thread_ids_for_prefix(&self, agent_path: &AgentPath) -> Vec<ThreadId> {
|
||||
let mut live_agents = self.state.live_agents();
|
||||
live_agents.sort_by(compare_agent_metadata);
|
||||
live_agents
|
||||
.into_iter()
|
||||
.filter(|metadata| agent_matches_prefix(metadata.agent_path.as_ref(), agent_path))
|
||||
.filter_map(|metadata| metadata.agent_id)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
async fn last_task_message_for_thread(thread: &crate::CodexThread) -> Option<String> {
|
||||
let pending_input = thread.codex.session.pending_input_snapshot().await;
|
||||
if let Some(message) = pending_input
|
||||
|
||||
@@ -403,6 +403,13 @@ impl ThreadManager {
|
||||
self.state.get_thread(thread_id).await
|
||||
}
|
||||
|
||||
pub async fn list_subtree_thread_ids(&self, thread_id: ThreadId) -> CodexResult<Vec<ThreadId>> {
|
||||
self.state.get_thread(thread_id).await?;
|
||||
Ok(self
|
||||
.agent_control()
|
||||
.list_agent_subtree_thread_ids_for_thread(thread_id))
|
||||
}
|
||||
|
||||
pub async fn start_thread(&self, config: Config) -> CodexResult<NewThread> {
|
||||
// Box delegated thread-spawn futures so these convenience wrappers do
|
||||
// not inline the full spawn path into every caller's async state.
|
||||
|
||||
@@ -9,7 +9,10 @@ use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
use core_test_support::PathExt;
|
||||
@@ -304,6 +307,102 @@ async fn new_uses_configured_openai_provider_for_model_refresh() {
|
||||
assert_eq!(models_mock.requests().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_subtree_thread_ids_returns_current_agent_and_descendants_only() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config();
|
||||
config.codex_home = temp_dir.path().join("codex-home");
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
let _ = config.features.enable(codex_features::Feature::MultiAgentV2);
|
||||
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
|
||||
let manager = ThreadManager::new(
|
||||
&config,
|
||||
auth_manager,
|
||||
SessionSource::Exec,
|
||||
CollaborationModesConfig::default(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::new(
|
||||
/*exec_server_url*/ None,
|
||||
)),
|
||||
);
|
||||
|
||||
let root = manager
|
||||
.start_thread(config.clone())
|
||||
.await
|
||||
.expect("root thread should start");
|
||||
let control = manager.agent_control();
|
||||
|
||||
let worker_path = AgentPath::from_string("/root/researcher/worker".to_string())
|
||||
.expect("worker path");
|
||||
let helper_path = worker_path.join("helper").expect("helper path");
|
||||
let sibling_path = AgentPath::from_string("/root/researcher/other_worker".to_string())
|
||||
.expect("sibling path");
|
||||
|
||||
let worker = control
|
||||
.spawn_agent_with_metadata(
|
||||
config.clone(),
|
||||
vec![UserInput::Text {
|
||||
text: "worker".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: root.thread_id,
|
||||
depth: 1,
|
||||
agent_path: Some(worker_path),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
crate::agent::control::SpawnAgentOptions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("worker should spawn");
|
||||
let helper = control
|
||||
.spawn_agent_with_metadata(
|
||||
config.clone(),
|
||||
vec![UserInput::Text {
|
||||
text: "helper".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: worker.thread_id,
|
||||
depth: 2,
|
||||
agent_path: Some(helper_path),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
crate::agent::control::SpawnAgentOptions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("helper should spawn");
|
||||
let _sibling = control
|
||||
.spawn_agent_with_metadata(
|
||||
config,
|
||||
vec![UserInput::Text {
|
||||
text: "sibling".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: root.thread_id,
|
||||
depth: 1,
|
||||
agent_path: Some(sibling_path),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
crate::agent::control::SpawnAgentOptions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("sibling should spawn");
|
||||
|
||||
let thread_ids = manager
|
||||
.list_subtree_thread_ids(worker.thread_id)
|
||||
.await
|
||||
.expect("subtree thread ids should load");
|
||||
|
||||
assert_eq!(thread_ids, vec![worker.thread_id, helper.thread_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupted_fork_snapshot_appends_interrupt_boundary() {
|
||||
let committed_history =
|
||||
|
||||
@@ -559,6 +559,10 @@ pub(crate) fn feedback_upload_consent_params(
|
||||
}
|
||||
}
|
||||
}
|
||||
header_lines.push(Line::from("").into());
|
||||
header_lines.push(
|
||||
Line::from("V2 sub-agent reports may include subtree rollouts.".dim()).into(),
|
||||
);
|
||||
|
||||
super::SelectionViewParams {
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
assertion_line: 9134
|
||||
expression: popup
|
||||
---
|
||||
Upload logs?
|
||||
|
||||
The following files will be sent:
|
||||
• codex-logs.log
|
||||
• codex-connectivity-diagnostics.txt
|
||||
|
||||
Connectivity diagnostics
|
||||
- OPENAI_BASE_URL is set and may affect connectivity.
|
||||
- OPENAI_BASE_URL = hello
|
||||
|
||||
V2 sub-agent reports may also include descendant rollouts in the same subtre
|
||||
|
||||
› 1. Yes Share the current Codex session logs with the team for
|
||||
troubleshooting.
|
||||
2. No
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -559,6 +559,10 @@ pub(crate) fn feedback_upload_consent_params(
|
||||
}
|
||||
}
|
||||
}
|
||||
header_lines.push(Line::from("").into());
|
||||
header_lines.push(
|
||||
Line::from("V2 sub-agent reports may include subtree rollouts.".dim()).into(),
|
||||
);
|
||||
|
||||
super::SelectionViewParams {
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: tui_app_server/src/chatwidget/tests.rs
|
||||
assertion_line: 9850
|
||||
expression: popup
|
||||
---
|
||||
Upload logs?
|
||||
|
||||
The following files will be sent:
|
||||
• codex-logs.log
|
||||
• codex-connectivity-diagnostics.txt
|
||||
|
||||
Connectivity diagnostics
|
||||
- OPENAI_BASE_URL is set and may affect connectivity.
|
||||
- OPENAI_BASE_URL = hello
|
||||
|
||||
V2 sub-agent reports may also include descendant rollouts in the same subtre
|
||||
|
||||
› 1. Yes Share the current Codex session logs with the team for
|
||||
troubleshooting.
|
||||
2. No
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
Reference in New Issue
Block a user