mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
store and expose parent_thread_id on Threads (#25113)
## Why This PR https://github.com/openai/codex/pull/24161#discussion_r3325692763 revealed a subagent data modeling issue, where we overloaded `forked_from_id` to also mean `parent_thread_id`. That's incorrect since guardian and review subagents can be a subagent and NOT fork the main thread's history. The solution here is to explicitly store a new `parent_thread_id` on `SessionMeta`, alongside `forked_from_id` which already exists. While we're at it, also expose it in the app-server protocol on the `Thread` object. A thread->subagent relationship and a fork of thread history are orthogonal concepts. ## What Changed - Added top-level `parent_thread_id` persistence on `SessionMeta` and runtime/session plumbing through `SessionConfiguredEvent`, `CodexSpawnArgs`, `SessionConfiguration`, `ThreadConfigSnapshot`, `TurnContext`, and `ModelClient`. - Made turn metadata, request headers, analytics, and subagent-start events read the separate runtime/top-level parent field instead of deriving general parent lineage from `SessionSource` or `forked_from_thread_id`. - Passed parent lineage separately at delegated subagent, review, guardian, agent-job, and multi-agent spawn construction sites; copied-history fork lineage remains derived only from `InitialHistory`. - Persisted and exposed parent lineage through rollout/thread-store projections and app-server v2 `Thread.parentThreadId`. - Updated app-server README text and regenerated app-server schema fixtures for the additive `parentThreadId` response field.
This commit is contained in:
committed by
GitHub
Unverified
parent
3b7334d099
commit
cf0911076f
@@ -160,11 +160,13 @@ fn sample_thread_with_metadata(
|
||||
ephemeral: bool,
|
||||
source: AppServerSessionSource,
|
||||
thread_source: Option<AppServerThreadSource>,
|
||||
parent_thread_id: Option<String>,
|
||||
) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
session_id: format!("session-{thread_id}"),
|
||||
forked_from_id: None,
|
||||
parent_thread_id,
|
||||
preview: "first prompt".to_string(),
|
||||
ephemeral,
|
||||
model_provider: "openai".to_string(),
|
||||
@@ -195,6 +197,7 @@ fn sample_thread_start_response(
|
||||
ephemeral,
|
||||
AppServerSessionSource::Exec,
|
||||
Some(AppServerThreadSource::User),
|
||||
/*parent_thread_id*/ None,
|
||||
),
|
||||
model: model.to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
@@ -240,6 +243,7 @@ fn sample_thread_resume_response(
|
||||
model,
|
||||
AppServerSessionSource::Exec,
|
||||
Some(AppServerThreadSource::User),
|
||||
/*parent_thread_id*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -249,9 +253,16 @@ fn sample_thread_resume_response_with_source(
|
||||
model: &str,
|
||||
source: AppServerSessionSource,
|
||||
thread_source: Option<AppServerThreadSource>,
|
||||
parent_thread_id: Option<String>,
|
||||
) -> ClientResponsePayload {
|
||||
ClientResponsePayload::ThreadResume(ThreadResumeResponse {
|
||||
thread: sample_thread_with_metadata(thread_id, ephemeral, source, thread_source),
|
||||
thread: sample_thread_with_metadata(
|
||||
thread_id,
|
||||
ephemeral,
|
||||
source,
|
||||
thread_source,
|
||||
parent_thread_id,
|
||||
),
|
||||
model: model.to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
@@ -1755,6 +1766,7 @@ async fn compaction_event_ingests_custom_fact() {
|
||||
agent_role: None,
|
||||
}),
|
||||
Some(AppServerThreadSource::Subagent),
|
||||
Some(parent_thread_id.to_string()),
|
||||
)),
|
||||
},
|
||||
&mut events,
|
||||
@@ -2456,7 +2468,7 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
|
||||
SubAgentThreadStartedInput {
|
||||
session_id: "session-root".to_string(),
|
||||
thread_id: "thread-spawn".to_string(),
|
||||
parent_thread_id: None,
|
||||
parent_thread_id: Some(parent_thread_id.to_string()),
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: "1.0.0".to_string(),
|
||||
@@ -2534,11 +2546,14 @@ fn subagent_thread_started_other_serializes_expected_shape() {
|
||||
|
||||
#[test]
|
||||
fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
|
||||
let parent_thread_id =
|
||||
codex_protocol::ThreadId::from_string("33333333-3333-4333-8333-333333333333")
|
||||
.expect("valid thread id");
|
||||
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
|
||||
SubAgentThreadStartedInput {
|
||||
session_id: "session-root".to_string(),
|
||||
thread_id: "thread-guardian".to_string(),
|
||||
parent_thread_id: Some("parent-thread-guardian".to_string()),
|
||||
parent_thread_id: Some(parent_thread_id.to_string()),
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: "1.0.0".to_string(),
|
||||
@@ -2553,7 +2568,7 @@ fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
|
||||
assert_eq!(payload["event_params"]["subagent_source"], "guardian");
|
||||
assert_eq!(
|
||||
payload["event_params"]["parent_thread_id"],
|
||||
"parent-thread-guardian"
|
||||
"33333333-3333-4333-8333-333333333333"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2642,7 +2657,7 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
|
||||
SubAgentThreadStartedInput {
|
||||
session_id: "session-root".to_string(),
|
||||
thread_id: "thread-review".to_string(),
|
||||
parent_thread_id: None,
|
||||
parent_thread_id: Some(parent_thread_id.to_string()),
|
||||
product_client_id: "parent-client".to_string(),
|
||||
client_name: "parent-client".to_string(),
|
||||
client_version: "1.0.0".to_string(),
|
||||
|
||||
@@ -124,6 +124,7 @@ fn sample_thread(thread_id: &str) -> Thread {
|
||||
id: thread_id.to_string(),
|
||||
session_id: format!("session-{thread_id}"),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "first prompt".to_string(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
@@ -1048,9 +1048,7 @@ pub(crate) fn subagent_thread_started_event_request(
|
||||
thread_source: Some(ThreadSource::Subagent),
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
||||
parent_thread_id: input
|
||||
.parent_thread_id
|
||||
.or_else(|| subagent_parent_thread_id(&input.subagent_source)),
|
||||
parent_thread_id: input.parent_thread_id,
|
||||
created_at: input.created_at,
|
||||
};
|
||||
ThreadInitializedEvent {
|
||||
@@ -1060,19 +1058,7 @@ pub(crate) fn subagent_thread_started_event_request(
|
||||
}
|
||||
|
||||
pub(crate) fn subagent_source_name(subagent_source: &SubAgentSource) -> String {
|
||||
match subagent_source {
|
||||
SubAgentSource::Review => "review".to_string(),
|
||||
SubAgentSource::Compact => "compact".to_string(),
|
||||
SubAgentSource::ThreadSpawn { .. } => "thread_spawn".to_string(),
|
||||
SubAgentSource::MemoryConsolidation => "memory_consolidation".to_string(),
|
||||
SubAgentSource::Other(other) => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option<String> {
|
||||
subagent_source
|
||||
.parent_thread_id()
|
||||
.map(|parent_thread_id| parent_thread_id.to_string())
|
||||
subagent_source.kind().to_string()
|
||||
}
|
||||
|
||||
fn analytics_hook_status(status: HookRunStatus) -> HookRunStatus {
|
||||
|
||||
@@ -55,7 +55,6 @@ use crate::events::codex_hook_run_metadata;
|
||||
use crate::events::codex_plugin_metadata;
|
||||
use crate::events::codex_plugin_used_metadata;
|
||||
use crate::events::plugin_state_event_type;
|
||||
use crate::events::subagent_parent_thread_id;
|
||||
use crate::events::subagent_source_name;
|
||||
use crate::events::subagent_thread_started_event_request;
|
||||
use crate::facts::AnalyticsFact;
|
||||
@@ -267,20 +266,18 @@ impl ThreadMetadataState {
|
||||
session_id: String,
|
||||
session_source: &SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
parent_thread_id: Option<String>,
|
||||
initialization_mode: ThreadInitializationMode,
|
||||
) -> Self {
|
||||
let (subagent_source, parent_thread_id) = match session_source {
|
||||
SessionSource::SubAgent(subagent_source) => (
|
||||
Some(subagent_source_name(subagent_source)),
|
||||
subagent_parent_thread_id(subagent_source),
|
||||
),
|
||||
let subagent_source = match session_source {
|
||||
SessionSource::SubAgent(subagent_source) => Some(subagent_source_name(subagent_source)),
|
||||
SessionSource::Cli
|
||||
| SessionSource::VSCode
|
||||
| SessionSource::Exec
|
||||
| SessionSource::Mcp
|
||||
| SessionSource::Custom(_)
|
||||
| SessionSource::Internal(_)
|
||||
| SessionSource::Unknown => (None, None),
|
||||
| SessionSource::Unknown => None,
|
||||
};
|
||||
Self {
|
||||
session_id,
|
||||
@@ -516,10 +513,7 @@ impl AnalyticsReducer {
|
||||
input: SubAgentThreadStartedInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let parent_thread_id = input
|
||||
.parent_thread_id
|
||||
.clone()
|
||||
.or_else(|| subagent_parent_thread_id(&input.subagent_source));
|
||||
let parent_thread_id = input.parent_thread_id.clone();
|
||||
let parent_connection_id = parent_thread_id
|
||||
.as_ref()
|
||||
.and_then(|parent_thread_id| self.threads.get(parent_thread_id))
|
||||
@@ -1238,6 +1232,7 @@ impl AnalyticsReducer {
|
||||
let session_source: SessionSource = thread.source.into();
|
||||
let session_id = thread.session_id;
|
||||
let thread_id = thread.id;
|
||||
let parent_thread_id = thread.parent_thread_id;
|
||||
let Some(connection_state) = self.connections.get(&connection_id) else {
|
||||
return;
|
||||
};
|
||||
@@ -1245,6 +1240,7 @@ impl AnalyticsReducer {
|
||||
session_id.clone(),
|
||||
&session_source,
|
||||
thread.thread_source.map(Into::into),
|
||||
parent_thread_id,
|
||||
initialization_mode,
|
||||
);
|
||||
self.threads.insert(
|
||||
|
||||
@@ -3366,6 +3366,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
+7
@@ -15513,6 +15513,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
+7
@@ -13337,6 +13337,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -1036,6 +1036,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -851,6 +851,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
+7
@@ -851,6 +851,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -851,6 +851,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -1036,6 +1036,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -851,6 +851,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -1036,6 +1036,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -851,6 +851,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -851,6 +851,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parentThreadId": {
|
||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "[UNSTABLE] Path to the thread on disk.",
|
||||
"type": [
|
||||
|
||||
@@ -17,6 +17,10 @@ sessionId: string,
|
||||
* Source thread id when this thread was created by forking another thread.
|
||||
*/
|
||||
forkedFromId: string | null,
|
||||
/**
|
||||
* The ID of the parent thread. This will only be set if this thread is a subagent.
|
||||
*/
|
||||
parentThreadId: string | null,
|
||||
/**
|
||||
* Usually the first user message in the thread, if available.
|
||||
*/
|
||||
|
||||
@@ -2328,6 +2328,7 @@ mod tests {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "first prompt".to_string(),
|
||||
ephemeral: true,
|
||||
model_provider: "openai".to_string(),
|
||||
@@ -2370,6 +2371,7 @@ mod tests {
|
||||
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||
"sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7",
|
||||
"forkedFromId": null,
|
||||
"parentThreadId": null,
|
||||
"preview": "first prompt",
|
||||
"ephemeral": true,
|
||||
"modelProvider": "openai",
|
||||
|
||||
@@ -142,6 +142,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
||||
id: "thr_123".to_string(),
|
||||
session_id: "thr_123".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
@@ -3581,6 +3582,7 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
|
||||
let fork: ThreadForkResponse = serde_json::from_value(response).expect("thread/fork response");
|
||||
|
||||
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(start.thread.parent_thread_id, None);
|
||||
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
|
||||
assert_eq!(start.active_permission_profile, None);
|
||||
|
||||
@@ -108,6 +108,8 @@ pub struct Thread {
|
||||
pub session_id: String,
|
||||
/// Source thread id when this thread was created by forking another thread.
|
||||
pub forked_from_id: Option<String>,
|
||||
/// The ID of the parent thread. This will only be set if this thread is a subagent.
|
||||
pub parent_thread_id: Option<String>,
|
||||
/// Usually the first user message in the thread, if available.
|
||||
pub preview: String,
|
||||
/// Whether the thread is ephemeral and should not be materialized on disk.
|
||||
|
||||
@@ -134,7 +134,7 @@ Example with notification opt-out:
|
||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate control/spawn parent is known.
|
||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
|
||||
@@ -424,7 +424,7 @@ Later, after the idle unload timeout:
|
||||
|
||||
### Example: Read a thread
|
||||
|
||||
Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want thread history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
||||
Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want thread history loaded into `thread.turns`. The returned thread includes `parentThreadId`, `agentNickname`, and `agentRole` for subagent threads when available.
|
||||
|
||||
```json
|
||||
{ "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } }
|
||||
|
||||
@@ -2170,6 +2170,7 @@ mod tests {
|
||||
thread_id,
|
||||
rollout_path: None,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "fallback preview".to_string(),
|
||||
name: Some("Rollback thread".to_string()),
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
@@ -4012,6 +4012,7 @@ pub(crate) fn thread_from_stored_thread(
|
||||
id: thread_id.clone(),
|
||||
session_id: thread_id,
|
||||
forked_from_id: thread.forked_from_id.map(|id| id.to_string()),
|
||||
parent_thread_id: thread.parent_thread_id.map(|id| id.to_string()),
|
||||
preview: thread.preview,
|
||||
ephemeral: false,
|
||||
model_provider: if thread.model_provider.is_empty() {
|
||||
@@ -4220,6 +4221,7 @@ fn build_thread_from_snapshot(
|
||||
id: thread_id.to_string(),
|
||||
session_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: config_snapshot.parent_thread_id.map(|id| id.to_string()),
|
||||
preview: String::new(),
|
||||
ephemeral: config_snapshot.ephemeral,
|
||||
model_provider: config_snapshot.model_provider_id.clone(),
|
||||
|
||||
@@ -395,6 +395,7 @@ mod thread_processor_behavior_tests {
|
||||
thread_id,
|
||||
rollout_path: Some(PathBuf::from("/tmp/thread.jsonl")),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "preview".to_string(),
|
||||
name: None,
|
||||
model_provider: "openai".to_string(),
|
||||
@@ -681,6 +682,7 @@ mod thread_processor_behavior_tests {
|
||||
},
|
||||
},
|
||||
session_source: SessionSource::Cli,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -172,6 +172,7 @@ mod tests {
|
||||
id: "thread-1".to_string(),
|
||||
session_id: "session-1".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "preview".to_string(),
|
||||
ephemeral: false,
|
||||
model_provider: "mock_provider".to_string(),
|
||||
|
||||
@@ -305,6 +305,7 @@ pub(crate) fn summary_to_thread(
|
||||
id: thread_id.clone(),
|
||||
session_id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview,
|
||||
ephemeral: false,
|
||||
model_provider,
|
||||
|
||||
@@ -891,6 +891,7 @@ mod tests {
|
||||
id: thread_id.to_string(),
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "mock-provider".to_string(),
|
||||
|
||||
@@ -38,6 +38,7 @@ pub use responses::create_final_assistant_message_sse_response;
|
||||
pub use responses::create_request_permissions_sse_response;
|
||||
pub use responses::create_request_user_input_sse_response;
|
||||
pub use responses::create_shell_command_sse_response;
|
||||
pub use rollout::create_fake_parented_rollout_with_source;
|
||||
pub use rollout::create_fake_rollout;
|
||||
pub use rollout::create_fake_rollout_with_source;
|
||||
pub use rollout::create_fake_rollout_with_text_elements;
|
||||
|
||||
@@ -118,6 +118,53 @@ pub fn create_fake_rollout_with_source(
|
||||
model_provider: Option<&str>,
|
||||
git_info: Option<GitInfo>,
|
||||
source: SessionSource,
|
||||
) -> Result<String> {
|
||||
create_fake_rollout_with_source_and_parent_thread_id(
|
||||
codex_home,
|
||||
filename_ts,
|
||||
meta_rfc3339,
|
||||
preview,
|
||||
model_provider,
|
||||
git_info,
|
||||
source,
|
||||
/*parent_thread_id*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a minimal rollout file with an explicit session source and control parent.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_fake_parented_rollout_with_source(
|
||||
codex_home: &Path,
|
||||
filename_ts: &str,
|
||||
meta_rfc3339: &str,
|
||||
preview: &str,
|
||||
model_provider: Option<&str>,
|
||||
git_info: Option<GitInfo>,
|
||||
source: SessionSource,
|
||||
parent_thread_id: ThreadId,
|
||||
) -> Result<String> {
|
||||
create_fake_rollout_with_source_and_parent_thread_id(
|
||||
codex_home,
|
||||
filename_ts,
|
||||
meta_rfc3339,
|
||||
preview,
|
||||
model_provider,
|
||||
git_info,
|
||||
source,
|
||||
Some(parent_thread_id),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_fake_rollout_with_source_and_parent_thread_id(
|
||||
codex_home: &Path,
|
||||
filename_ts: &str,
|
||||
meta_rfc3339: &str,
|
||||
preview: &str,
|
||||
model_provider: Option<&str>,
|
||||
git_info: Option<GitInfo>,
|
||||
source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
) -> Result<String> {
|
||||
let uuid = Uuid::new_v4();
|
||||
let uuid_str = uuid.to_string();
|
||||
@@ -133,6 +180,7 @@ pub fn create_fake_rollout_with_source(
|
||||
let meta = SessionMeta {
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id,
|
||||
timestamp: meta_rfc3339.to_string(),
|
||||
cwd: PathBuf::from("/"),
|
||||
originator: "codex".to_string(),
|
||||
@@ -217,6 +265,7 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
let meta = SessionMeta {
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: meta_rfc3339.to_string(),
|
||||
cwd: PathBuf::from("/"),
|
||||
originator: "codex".to_string(),
|
||||
|
||||
@@ -124,6 +124,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Cli,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_fake_parented_rollout_with_source;
|
||||
use app_test_support::create_fake_rollout;
|
||||
use app_test_support::create_fake_rollout_with_source;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -9,6 +9,7 @@ use codex_app_server_protocol::ReviewDelivery;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
use codex_app_server_protocol::ReviewStartResponse;
|
||||
use codex_app_server_protocol::ReviewTarget;
|
||||
use codex_app_server_protocol::SessionSource as ApiSessionSource;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
@@ -198,7 +199,7 @@ async fn turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() ->
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn review_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()> {
|
||||
async fn review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let review_payload = serde_json::json!({
|
||||
@@ -276,8 +277,9 @@ async fn review_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -
|
||||
request.header("x-openai-subagent").as_deref(),
|
||||
Some("review")
|
||||
);
|
||||
assert!(metadata.get("forked_from_thread_id").is_none());
|
||||
assert_eq!(
|
||||
metadata["forked_from_thread_id"].as_str(),
|
||||
metadata["parent_thread_id"].as_str(),
|
||||
Some(review_thread_id.as_str())
|
||||
);
|
||||
let review_request_thread_id = metadata["thread_id"]
|
||||
@@ -297,7 +299,7 @@ async fn review_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_sends_subagent_lineage_after_cold_thread_resume_v2() -> Result<()> {
|
||||
async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -320,20 +322,15 @@ async fn turn_start_sends_subagent_lineage_after_cold_thread_resume_v2() -> Resu
|
||||
|
||||
let parent_thread_id = CoreThreadId::new();
|
||||
let parent_thread_id_str = parent_thread_id.to_string();
|
||||
let subagent_thread_id = create_fake_rollout_with_source(
|
||||
let subagent_thread_id = create_fake_parented_rollout_with_source(
|
||||
codex_home.path(),
|
||||
"2025-01-05T12-00-00",
|
||||
"2025-01-05T12:00:00Z",
|
||||
"Saved subagent message",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
SessionSource::SubAgent(SubAgentSource::Other("guardian".to_string())),
|
||||
parent_thread_id,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
@@ -352,6 +349,11 @@ async fn turn_start_sends_subagent_lineage_after_cold_thread_resume_v2() -> Resu
|
||||
.await??;
|
||||
let ThreadResumeResponse { thread, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
assert_eq!(thread.id, subagent_thread_id);
|
||||
assert_eq!(thread.parent_thread_id, Some(parent_thread_id_str.clone()));
|
||||
assert_eq!(
|
||||
thread.source,
|
||||
ApiSessionSource::SubAgent(SubAgentSource::Other("guardian".to_string()))
|
||||
);
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
@@ -386,7 +388,7 @@ async fn turn_start_sends_subagent_lineage_after_cold_thread_resume_v2() -> Resu
|
||||
metadata["parent_thread_id"].as_str(),
|
||||
Some(parent_thread_id_str.as_str())
|
||||
);
|
||||
assert_eq!(metadata["subagent_kind"].as_str(), Some("thread_spawn"));
|
||||
assert_eq!(metadata["subagent_kind"].as_str(), Some("guardian"));
|
||||
assert_eq!(metadata["thread_id"].as_str(), Some(thread.id.as_str()));
|
||||
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
|
||||
assert!(metadata.get("forked_from_thread_id").is_none());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_fake_parented_rollout_with_source;
|
||||
use app_test_support::create_fake_rollout;
|
||||
use app_test_support::create_fake_rollout_with_source;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
@@ -1049,7 +1050,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
|
||||
|
||||
let parent_thread_id = ThreadId::from_string(&Uuid::new_v4().to_string())?;
|
||||
|
||||
let review_id = create_fake_rollout_with_source(
|
||||
let review_id = create_fake_parented_rollout_with_source(
|
||||
codex_home.path(),
|
||||
"2025-02-02T09-00-00",
|
||||
"2025-02-02T09:00:00Z",
|
||||
@@ -1057,6 +1058,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
CoreSessionSource::SubAgent(SubAgentSource::Review),
|
||||
parent_thread_id,
|
||||
)?;
|
||||
let compact_id = create_fake_rollout_with_source(
|
||||
codex_home.path(),
|
||||
@@ -1109,6 +1111,10 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
|
||||
.map(|thread| thread.id.as_str())
|
||||
.collect();
|
||||
assert_eq!(review_ids, vec![review_id.as_str()]);
|
||||
assert_eq!(
|
||||
review.data[0].parent_thread_id,
|
||||
Some(parent_thread_id.to_string())
|
||||
);
|
||||
|
||||
let compact = list_threads(
|
||||
&mut mcp,
|
||||
|
||||
@@ -1358,6 +1358,7 @@ async fn seed_pathless_store_thread(
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: ProtocolSessionSource::Cli,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
|
||||
@@ -1791,6 +1791,7 @@ stream_max_retries = 0
|
||||
let session_meta = SessionMeta {
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: "2025-01-05T12:00:00Z".to_string(),
|
||||
cwd: repo_path.clone(),
|
||||
originator: "codex".to_string(),
|
||||
|
||||
@@ -211,6 +211,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Cli,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
|
||||
@@ -32,5 +32,5 @@ fn register_session_root(session: &Arc<Session>, turn: &Arc<TurnContext>) {
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
.register_session_root(session.conversation_id, &turn.session_source);
|
||||
.register_session_root(session.conversation_id, turn.parent_thread_id);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ pub(crate) enum SpawnAgentForkMode {
|
||||
pub(crate) struct SpawnAgentOptions {
|
||||
pub(crate) fork_parent_spawn_call_id: Option<String>,
|
||||
pub(crate) fork_mode: Option<SpawnAgentForkMode>,
|
||||
pub(crate) parent_thread_id: Option<ThreadId>,
|
||||
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
}
|
||||
|
||||
@@ -262,12 +263,12 @@ impl AgentControl {
|
||||
.await?
|
||||
}
|
||||
(Some(session_source), None) => {
|
||||
let forked_from_thread_id = thread_spawn_parent_thread_id(&session_source);
|
||||
Box::pin(state.spawn_new_thread_with_source(
|
||||
config.clone(),
|
||||
self.clone(),
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
options.parent_thread_id,
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*thread_source*/ Some(ThreadSource::Subagent),
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
@@ -309,6 +310,7 @@ impl AgentControl {
|
||||
}
|
||||
};
|
||||
let thread_config = new_thread.thread.codex.thread_config_snapshot().await;
|
||||
let parent_thread_id = thread_config.parent_thread_id;
|
||||
emit_subagent_session_started(
|
||||
&new_thread
|
||||
.thread
|
||||
@@ -319,7 +321,7 @@ impl AgentControl {
|
||||
client_metadata,
|
||||
new_thread.thread.codex.session.session_id(),
|
||||
new_thread.thread_id,
|
||||
/*parent_thread_id*/ None,
|
||||
parent_thread_id,
|
||||
thread_config,
|
||||
subagent_source.clone(),
|
||||
);
|
||||
@@ -490,6 +492,7 @@ impl AgentControl {
|
||||
self.clone(),
|
||||
session_source,
|
||||
/*thread_source*/ Some(ThreadSource::Subagent),
|
||||
/*parent_thread_id*/ Some(parent_thread_id),
|
||||
/*forked_from_thread_id*/ Some(parent_thread_id),
|
||||
/*persist_extended_history*/ false,
|
||||
inherited_shell_snapshot,
|
||||
@@ -638,6 +641,7 @@ impl AgentControl {
|
||||
.history
|
||||
.ok_or_else(|| CodexErr::ThreadNotFound(thread_id))?
|
||||
.items;
|
||||
let parent_thread_id = stored_thread.parent_thread_id;
|
||||
|
||||
let resumed_thread = state
|
||||
.resume_thread_with_history_with_source(ResumeThreadWithHistoryOptions {
|
||||
@@ -649,6 +653,7 @@ impl AgentControl {
|
||||
}),
|
||||
agent_control: self.clone(),
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
})
|
||||
@@ -841,9 +846,9 @@ impl AgentControl {
|
||||
pub(crate) fn register_session_root(
|
||||
&self,
|
||||
current_thread_id: ThreadId,
|
||||
current_session_source: &SessionSource,
|
||||
current_parent_thread_id: Option<ThreadId>,
|
||||
) {
|
||||
if thread_spawn_parent_thread_id(current_session_source).is_none() {
|
||||
if current_parent_thread_id.is_none() {
|
||||
self.state.register_root_thread(current_thread_id);
|
||||
}
|
||||
}
|
||||
@@ -1218,7 +1223,8 @@ impl AgentControl {
|
||||
child_thread_id: ThreadId,
|
||||
session_source: Option<&SessionSource>,
|
||||
) {
|
||||
let Some(parent_thread_id) = session_source.and_then(thread_spawn_parent_thread_id) else {
|
||||
let Some(parent_thread_id) = session_source.and_then(SessionSource::parent_thread_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(state_db_ctx) = thread.state_db() else {
|
||||
@@ -1263,10 +1269,6 @@ impl AgentControl {
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_spawn_parent_thread_id(session_source: &SessionSource) -> Option<ThreadId> {
|
||||
session_source.parent_thread_id()
|
||||
}
|
||||
|
||||
fn agent_matches_prefix(agent_path: Option<&AgentPath>, prefix: &AgentPath) -> bool {
|
||||
if prefix.is_root() {
|
||||
return true;
|
||||
|
||||
@@ -74,6 +74,15 @@ fn assistant_message(text: &str, phase: Option<MessagePhase>) -> ResponseItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_session_root_skips_threads_with_explicit_parent() {
|
||||
let control = AgentControl::default();
|
||||
|
||||
control.register_session_root(ThreadId::new(), Some(ThreadId::new()));
|
||||
|
||||
assert_eq!(control.state.agent_id_for_path(&AgentPath::root()), None);
|
||||
}
|
||||
|
||||
fn spawn_agent_call(call_id: &str) -> ResponseItem {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
|
||||
@@ -173,6 +173,7 @@ struct ModelClientState {
|
||||
provider: SharedModelProvider,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
session_source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
model_verbosity: Option<VerbosityConfig>,
|
||||
enable_request_compression: bool,
|
||||
include_timing_metrics: bool,
|
||||
@@ -321,6 +322,7 @@ impl ModelClient {
|
||||
installation_id: String,
|
||||
provider_info: ModelProviderInfo,
|
||||
session_source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
model_verbosity: Option<VerbosityConfig>,
|
||||
enable_request_compression: bool,
|
||||
include_timing_metrics: bool,
|
||||
@@ -344,6 +346,7 @@ impl ModelClient {
|
||||
provider: model_provider,
|
||||
auth_env_telemetry,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
model_verbosity,
|
||||
enable_request_compression,
|
||||
include_timing_metrics,
|
||||
@@ -637,7 +640,7 @@ impl ModelClient {
|
||||
|
||||
fn build_responses_identity_headers(&self) -> ApiHeaderMap {
|
||||
let mut extra_headers = self.build_subagent_headers();
|
||||
if let Some(parent_thread_id) = parent_thread_id_header_value(&self.state.session_source)
|
||||
if let Some(parent_thread_id) = parent_thread_id_header_value(self.state.parent_thread_id)
|
||||
&& let Ok(val) = HeaderValue::from_str(&parent_thread_id)
|
||||
{
|
||||
extra_headers.insert(X_CODEX_PARENT_THREAD_ID_HEADER, val);
|
||||
@@ -664,7 +667,7 @@ impl ModelClient {
|
||||
if let Some(subagent) = subagent_header_value(&self.state.session_source) {
|
||||
client_metadata.insert(X_OPENAI_SUBAGENT_HEADER.to_string(), subagent);
|
||||
}
|
||||
if let Some(parent_thread_id) = parent_thread_id_header_value(&self.state.session_source) {
|
||||
if let Some(parent_thread_id) = parent_thread_id_header_value(self.state.parent_thread_id) {
|
||||
client_metadata.insert(
|
||||
X_CODEX_PARENT_THREAD_ID_HEADER.to_string(),
|
||||
parent_thread_id,
|
||||
@@ -1733,10 +1736,8 @@ fn subagent_header_value(session_source: &SessionSource) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parent_thread_id_header_value(session_source: &SessionSource) -> Option<String> {
|
||||
session_source
|
||||
.parent_thread_id()
|
||||
.map(|parent_thread_id| parent_thread_id.to_string())
|
||||
fn parent_thread_id_header_value(parent_thread_id: Option<ThreadId>) -> Option<String> {
|
||||
parent_thread_id.map(|parent_thread_id| parent_thread_id.to_string())
|
||||
}
|
||||
|
||||
const RESPONSE_STREAM_CHANNEL_CAPACITY: usize = 1600;
|
||||
|
||||
@@ -61,6 +61,13 @@ use tracing_subscriber::registry::LookupSpan;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
fn test_model_client(session_source: SessionSource) -> ModelClient {
|
||||
test_model_client_with_parent(session_source, /*parent_thread_id*/ None)
|
||||
}
|
||||
|
||||
fn test_model_client_with_parent(
|
||||
session_source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
) -> ModelClient {
|
||||
let provider = create_oss_provider_with_base_url("https://example.com/v1", WireApi::Responses);
|
||||
let thread_id = ThreadId::new();
|
||||
ModelClient::new(
|
||||
@@ -70,6 +77,7 @@ fn test_model_client(session_source: SessionSource) -> ModelClient {
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
provider,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
/*model_verbosity*/ None,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
@@ -272,13 +280,16 @@ fn build_subagent_headers_sets_internal_memory_consolidation_label() {
|
||||
#[test]
|
||||
fn build_ws_client_metadata_includes_window_lineage_and_turn_metadata() {
|
||||
let parent_thread_id = ThreadId::new();
|
||||
let client = test_model_client(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}));
|
||||
let client = test_model_client_with_parent(
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 2,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
Some(parent_thread_id),
|
||||
);
|
||||
|
||||
client.advance_window_generation();
|
||||
|
||||
@@ -520,6 +531,7 @@ fn model_client_with_counting_attestation(
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
provider,
|
||||
SessionSource::Exec,
|
||||
/*parent_thread_id*/ None,
|
||||
/*model_verbosity*/ None,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
|
||||
@@ -74,6 +74,8 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
) -> Result<Codex, CodexErr> {
|
||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (tx_ops, rx_ops) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let conversation_history = initial_history.unwrap_or(InitialHistory::New);
|
||||
let forked_from_thread_id = conversation_history.forked_from_id();
|
||||
let CodexSpawnOk { codex, .. } = Box::pin(Codex::spawn(CodexSpawnArgs {
|
||||
config,
|
||||
installation_id: parent_session.installation_id.clone(),
|
||||
@@ -84,9 +86,10 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
plugins_manager: Arc::clone(&parent_session.services.plugins_manager),
|
||||
mcp_manager: Arc::clone(&parent_session.services.mcp_manager),
|
||||
extensions: Arc::clone(&parent_session.services.extensions),
|
||||
conversation_history: initial_history.unwrap_or(InitialHistory::New),
|
||||
conversation_history,
|
||||
session_source: SessionSource::SubAgent(subagent_source.clone()),
|
||||
forked_from_thread_id: Some(parent_session.conversation_id),
|
||||
forked_from_thread_id,
|
||||
parent_thread_id: Some(parent_session.conversation_id),
|
||||
thread_source: Some(ThreadSource::Subagent),
|
||||
agent_control: parent_session.services.agent_control.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::session::SessionSettingsUpdate;
|
||||
use crate::session::SteerInputError;
|
||||
use codex_features::Feature;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
@@ -68,6 +69,7 @@ pub struct ThreadConfigSnapshot {
|
||||
pub personality: Option<Personality>,
|
||||
pub collaboration_mode: CollaborationMode,
|
||||
pub session_source: SessionSource,
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
pub thread_source: Option<ThreadSource>,
|
||||
}
|
||||
|
||||
|
||||
@@ -156,8 +156,8 @@ pub(crate) fn is_guardian_reviewer_source(
|
||||
) -> bool {
|
||||
matches!(
|
||||
session_source,
|
||||
codex_protocol::protocol::SessionSource::SubAgent(SubAgentSource::Other(name))
|
||||
if name == GUARDIAN_REVIEWER_NAME
|
||||
codex_protocol::protocol::SessionSource::SubAgent(SubAgentSource::Other(label))
|
||||
if label == GUARDIAN_REVIEWER_NAME
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: TEST_TIMESTAMP.to_string(),
|
||||
cwd: std::path::PathBuf::from("."),
|
||||
originator: "test_originator".to_string(),
|
||||
|
||||
@@ -32,6 +32,7 @@ fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThre
|
||||
thread_id: ThreadId::new(),
|
||||
rollout_path: Some(PathBuf::from("/tmp/rollout.jsonl")),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: first_user_message.to_string(),
|
||||
name: (!title.is_empty()).then(|| title.to_string()),
|
||||
model_provider: "test-provider".to_string(),
|
||||
|
||||
@@ -402,6 +402,7 @@ pub(crate) struct CodexSpawnArgs {
|
||||
pub(crate) conversation_history: InitialHistory,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) forked_from_thread_id: Option<ThreadId>,
|
||||
pub(crate) parent_thread_id: Option<ThreadId>,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) agent_control: AgentControl,
|
||||
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
|
||||
@@ -467,6 +468,7 @@ impl Codex {
|
||||
conversation_history,
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
parent_thread_id,
|
||||
thread_source,
|
||||
agent_control,
|
||||
dynamic_tools,
|
||||
@@ -595,6 +597,7 @@ impl Codex {
|
||||
app_server_client_version: None,
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
parent_thread_id,
|
||||
thread_source,
|
||||
dynamic_tools,
|
||||
persist_extended_history,
|
||||
|
||||
@@ -90,6 +90,7 @@ pub(super) async fn spawn_review_thread(
|
||||
sess.session_id().to_string(),
|
||||
sess.thread_id().to_string(),
|
||||
forked_from_thread_id,
|
||||
parent_turn_context.parent_thread_id,
|
||||
&session_source,
|
||||
parent_turn_context.thread_source,
|
||||
review_turn_id.clone(),
|
||||
@@ -113,6 +114,7 @@ pub(super) async fn spawn_review_thread(
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
session_source,
|
||||
parent_thread_id: parent_turn_context.parent_thread_id,
|
||||
thread_source: parent_turn_context.thread_source,
|
||||
environments: parent_turn_context.environments.clone(),
|
||||
available_models,
|
||||
|
||||
@@ -97,6 +97,8 @@ pub(crate) struct SessionConfiguration {
|
||||
pub(super) session_source: SessionSource,
|
||||
/// Immediate history source copied into this thread, when this thread was forked.
|
||||
pub(super) forked_from_thread_id: Option<ThreadId>,
|
||||
/// Immediate control/spawn parent for this thread, when it has one.
|
||||
pub(super) parent_thread_id: Option<ThreadId>,
|
||||
/// Optional analytics source classification for this thread.
|
||||
pub(super) thread_source: Option<ThreadSource>,
|
||||
pub(super) dynamic_tools: Vec<DynamicToolSpec>,
|
||||
@@ -187,6 +189,7 @@ impl SessionConfiguration {
|
||||
personality: self.personality,
|
||||
collaboration_mode: self.collaboration_mode.clone(),
|
||||
session_source: self.session_source.clone(),
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
thread_source: self.thread_source,
|
||||
}
|
||||
}
|
||||
@@ -511,6 +514,10 @@ impl Session {
|
||||
.forked_from_thread_id
|
||||
.or_else(|| initial_history.forked_from_id());
|
||||
session_configuration.forked_from_thread_id = forked_from_id;
|
||||
let parent_thread_id = session_configuration
|
||||
.parent_thread_id
|
||||
.or_else(|| initial_history.get_resumed_parent_thread_id());
|
||||
session_configuration.parent_thread_id = parent_thread_id;
|
||||
|
||||
let event_persistence_mode = if session_configuration.persist_extended_history {
|
||||
ThreadEventPersistenceMode::Extended
|
||||
@@ -550,6 +557,7 @@ impl Session {
|
||||
CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
source: session_source,
|
||||
thread_source: session_configuration.thread_source,
|
||||
base_instructions: BaseInstructions {
|
||||
@@ -1031,6 +1039,7 @@ impl Session {
|
||||
installation_id.clone(),
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.parent_thread_id,
|
||||
config.model_verbosity,
|
||||
config.features.enabled(Feature::EnableRequestCompression),
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
@@ -1040,7 +1049,7 @@ impl Session {
|
||||
.with_prompt_cache_key_override(
|
||||
crate::guardian::prompt_cache_key_override_for_review_session(
|
||||
&session_configuration.session_source,
|
||||
session_configuration.forked_from_thread_id,
|
||||
session_configuration.parent_thread_id,
|
||||
),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(),
|
||||
@@ -1083,6 +1092,7 @@ impl Session {
|
||||
session_id,
|
||||
thread_id,
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
thread_source: session_configuration.thread_source,
|
||||
thread_name: session_configuration.thread_name.clone(),
|
||||
model: session_configuration.collaboration_mode.model().to_string(),
|
||||
|
||||
@@ -433,6 +433,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
ModelProviderInfo::create_openai_provider(/* base_url */ /*base_url*/ None),
|
||||
codex_protocol::protocol::SessionSource::Exec,
|
||||
/*parent_thread_id*/ None,
|
||||
/*model_verbosity*/ None,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
@@ -3096,6 +3097,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -3201,6 +3203,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -3449,6 +3452,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
|
||||
CreateThreadParams {
|
||||
thread_id: session.conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Exec,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
@@ -3729,6 +3733,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -4473,6 +4478,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -4583,6 +4589,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -4677,6 +4684,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.parent_thread_id,
|
||||
config.model_verbosity,
|
||||
config.features.enabled(Feature::EnableRequestCompression),
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
@@ -4818,6 +4826,7 @@ async fn make_session_with_config_and_rx(
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -4922,6 +4931,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
app_server_client_version: None,
|
||||
session_source: session_source.clone(),
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -5944,6 +5954,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
|
||||
CreateThreadParams {
|
||||
thread_id: session.conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Exec,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
@@ -6426,6 +6437,7 @@ where
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
dynamic_tools,
|
||||
persist_extended_history: false,
|
||||
@@ -6520,6 +6532,7 @@ where
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
session_configuration.provider.clone(),
|
||||
session_configuration.session_source.clone(),
|
||||
session_configuration.parent_thread_id,
|
||||
config.model_verbosity,
|
||||
config.features.enabled(Feature::EnableRequestCompression),
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
|
||||
@@ -693,6 +693,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
GUARDIAN_REVIEWER_NAME.to_string(),
|
||||
)),
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
agent_control: AgentControl::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::environment_selection::ResolvedTurnEnvironments;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::openai_models::ToolMode;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
@@ -62,6 +63,7 @@ pub struct TurnContext {
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
pub(crate) reasoning_summary: ReasoningSummaryConfig,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) parent_thread_id: Option<ThreadId>,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) environments: ResolvedTurnEnvironments,
|
||||
/// The session's absolute working directory. All relative paths provided
|
||||
@@ -232,6 +234,7 @@ impl TurnContext {
|
||||
reasoning_effort,
|
||||
reasoning_summary: self.reasoning_summary,
|
||||
session_source: self.session_source.clone(),
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
thread_source: self.thread_source,
|
||||
environments: self.environments.clone(),
|
||||
#[allow(deprecated)]
|
||||
@@ -506,6 +509,7 @@ impl Session {
|
||||
session_id.to_string(),
|
||||
thread_id.to_string(),
|
||||
session_configuration.forked_from_thread_id,
|
||||
session_configuration.parent_thread_id,
|
||||
&session_configuration.session_source,
|
||||
session_configuration.thread_source,
|
||||
sub_id.clone(),
|
||||
@@ -529,6 +533,7 @@ impl Session {
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
session_source,
|
||||
parent_thread_id: session_configuration.parent_thread_id,
|
||||
thread_source: session_configuration.thread_source,
|
||||
environments,
|
||||
#[allow(deprecated)]
|
||||
|
||||
@@ -187,6 +187,7 @@ pub(crate) struct ResumeThreadWithHistoryOptions {
|
||||
pub(crate) initial_history: InitialHistory,
|
||||
pub(crate) agent_control: AgentControl,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) parent_thread_id: Option<ThreadId>,
|
||||
pub(crate) inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
pub(crate) inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
}
|
||||
@@ -601,6 +602,7 @@ impl ThreadManager {
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
session_source,
|
||||
/*parent_thread_id*/ None,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
options.dynamic_tools,
|
||||
@@ -685,6 +687,7 @@ impl ThreadManager {
|
||||
auth_manager,
|
||||
self.agent_control(),
|
||||
session_source,
|
||||
/*parent_thread_id*/ None,
|
||||
/*forked_from_thread_id*/ None,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
@@ -713,6 +716,7 @@ impl ThreadManager {
|
||||
InitialHistory::New,
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
/*parent_thread_id*/ None,
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*thread_source*/ None,
|
||||
Vec::new(),
|
||||
@@ -746,6 +750,7 @@ impl ThreadManager {
|
||||
auth_manager,
|
||||
self.agent_control(),
|
||||
session_source,
|
||||
/*parent_thread_id*/ None,
|
||||
/*forked_from_thread_id*/ None,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
@@ -916,6 +921,7 @@ impl ThreadManager {
|
||||
history,
|
||||
Arc::clone(&self.state.auth_manager),
|
||||
self.agent_control(),
|
||||
/*parent_thread_id*/ None,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
@@ -1039,6 +1045,7 @@ impl ThreadManagerState {
|
||||
config,
|
||||
agent_control,
|
||||
self.session_source.clone(),
|
||||
/*parent_thread_id*/ None,
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*thread_source*/ None,
|
||||
/*persist_extended_history*/ false,
|
||||
@@ -1056,6 +1063,7 @@ impl ThreadManagerState {
|
||||
config: Config,
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
thread_source: Option<ThreadSource>,
|
||||
persist_extended_history: bool,
|
||||
@@ -1073,6 +1081,7 @@ impl ThreadManagerState {
|
||||
Arc::clone(&self.auth_manager),
|
||||
agent_control,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
@@ -1096,6 +1105,7 @@ impl ThreadManagerState {
|
||||
initial_history,
|
||||
agent_control,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
} = options;
|
||||
@@ -1108,6 +1118,7 @@ impl ThreadManagerState {
|
||||
Arc::clone(&self.auth_manager),
|
||||
agent_control,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
/*forked_from_thread_id*/ None,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
@@ -1130,6 +1141,7 @@ impl ThreadManagerState {
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
persist_extended_history: bool,
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
@@ -1145,6 +1157,7 @@ impl ThreadManagerState {
|
||||
Arc::clone(&self.auth_manager),
|
||||
agent_control,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
Vec::new(),
|
||||
@@ -1167,6 +1180,7 @@ impl ThreadManagerState {
|
||||
initial_history: InitialHistory,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
agent_control: AgentControl,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
thread_source: Option<ThreadSource>,
|
||||
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||
@@ -1182,6 +1196,7 @@ impl ThreadManagerState {
|
||||
auth_manager,
|
||||
agent_control,
|
||||
self.session_source.clone(),
|
||||
parent_thread_id,
|
||||
forked_from_thread_id,
|
||||
thread_source,
|
||||
dynamic_tools,
|
||||
@@ -1204,6 +1219,7 @@ impl ThreadManagerState {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
thread_source: Option<ThreadSource>,
|
||||
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||
@@ -1258,6 +1274,7 @@ impl ThreadManagerState {
|
||||
conversation_history: initial_history,
|
||||
session_source,
|
||||
forked_from_thread_id,
|
||||
parent_thread_id,
|
||||
thread_source,
|
||||
agent_control,
|
||||
dynamic_tools,
|
||||
|
||||
@@ -211,6 +211,7 @@ async fn run_agent_job_loop(
|
||||
"agent_job:{job_id}"
|
||||
)))),
|
||||
SpawnAgentOptions {
|
||||
parent_thread_id: Some(session.conversation_id),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -124,6 +124,7 @@ async fn handle_spawn_agent(
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()),
|
||||
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
|
||||
parent_thread_id: Some(session.conversation_id),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
},
|
||||
))
|
||||
|
||||
@@ -30,7 +30,7 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
.register_session_root(session.conversation_id, &turn.session_source);
|
||||
.register_session_root(session.conversation_id, turn.parent_thread_id);
|
||||
let agents = session
|
||||
.services
|
||||
.agent_control
|
||||
|
||||
@@ -144,6 +144,7 @@ async fn handle_spawn_agent(
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()),
|
||||
fork_mode,
|
||||
parent_thread_id: Some(session.conversation_id),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -261,6 +261,7 @@ impl TurnMetadataState {
|
||||
session_id: String,
|
||||
thread_id: String,
|
||||
forked_from_thread_id: Option<ThreadId>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
session_source: &SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
turn_id: String,
|
||||
@@ -278,18 +279,15 @@ impl TurnMetadataState {
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
let (parent_thread_id, subagent_kind) = match session_source {
|
||||
SessionSource::SubAgent(subagent_source) => (
|
||||
subagent_source.parent_thread_id().or(forked_from_thread_id),
|
||||
Some(subagent_source.kind().to_string()),
|
||||
),
|
||||
let subagent_kind = match session_source {
|
||||
SessionSource::SubAgent(subagent_source) => Some(subagent_source.kind().to_string()),
|
||||
SessionSource::Cli
|
||||
| SessionSource::VSCode
|
||||
| SessionSource::Exec
|
||||
| SessionSource::Mcp
|
||||
| SessionSource::Custom(_)
|
||||
| SessionSource::Internal(_)
|
||||
| SessionSource::Unknown => (None, None),
|
||||
| SessionSource::Unknown => None,
|
||||
};
|
||||
let base_metadata = TurnMetadataBag {
|
||||
request_kind: None,
|
||||
|
||||
@@ -122,6 +122,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
@@ -163,6 +164,7 @@ fn turn_metadata_state_uses_explicit_subagent_thread_source() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
@@ -191,6 +193,7 @@ fn turn_metadata_state_includes_root_fork_lineage() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
Some(source_thread_id),
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
@@ -223,6 +226,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
Some(parent_thread_id),
|
||||
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
@@ -261,6 +265,7 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
Some(parent_thread_id),
|
||||
Some(parent_thread_id),
|
||||
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
@@ -291,38 +296,46 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_includes_known_parent_for_other_subagent() {
|
||||
fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_without_fork() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().abs();
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
let parent_thread_id =
|
||||
ThreadId::from_string("44444444-4444-4444-8444-444444444444").expect("thread id");
|
||||
let sources = [
|
||||
(SubAgentSource::Review, "review"),
|
||||
(SubAgentSource::Other("guardian".to_string()), "guardian"),
|
||||
(
|
||||
SubAgentSource::Other("agent_job:job-1".to_string()),
|
||||
"agent_job:job-1",
|
||||
),
|
||||
];
|
||||
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
Some(parent_thread_id),
|
||||
&SessionSource::SubAgent(SubAgentSource::Other("guardian".to_string())),
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&permission_profile,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
for (subagent_source, subagent_kind) in sources {
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
Some(parent_thread_id),
|
||||
&SessionSource::SubAgent(subagent_source),
|
||||
Some(ThreadSource::Subagent),
|
||||
"turn-a".to_string(),
|
||||
cwd.clone(),
|
||||
&permission_profile,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*enforce_managed_network*/ false,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
let header = state.current_header_value().expect("header");
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert_eq!(
|
||||
json["forked_from_thread_id"].as_str(),
|
||||
Some("44444444-4444-4444-8444-444444444444")
|
||||
);
|
||||
assert_eq!(
|
||||
json["parent_thread_id"].as_str(),
|
||||
Some("44444444-4444-4444-8444-444444444444")
|
||||
);
|
||||
assert_eq!(json["subagent_kind"].as_str(), Some("guardian"));
|
||||
assert!(json.get("forked_from_thread_id").is_none());
|
||||
assert_eq!(
|
||||
json["parent_thread_id"].as_str(),
|
||||
Some("44444444-4444-4444-8444-444444444444")
|
||||
);
|
||||
assert_eq!(json["subagent_kind"].as_str(), Some(subagent_kind));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -335,6 +348,7 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
@@ -364,6 +378,7 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
"turn-a".to_string(),
|
||||
@@ -412,6 +427,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
"turn-a".to_string(),
|
||||
@@ -464,6 +480,7 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
@@ -511,6 +528,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
Some(source_thread_id),
|
||||
Some(parent_thread_id),
|
||||
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
@@ -612,6 +630,7 @@ fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
/*forked_from_thread_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
&SessionSource::Exec,
|
||||
Some(ThreadSource::User),
|
||||
"turn-a".to_string(),
|
||||
@@ -671,6 +690,7 @@ async fn turn_metadata_state_preserves_lineage_after_git_enrichment() {
|
||||
"session-a".to_string(),
|
||||
"thread-a".to_string(),
|
||||
Some(parent_thread_id),
|
||||
Some(parent_thread_id),
|
||||
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
|
||||
@@ -105,6 +105,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
|
||||
/*installation_id*/ TEST_INSTALLATION_ID.to_string(),
|
||||
provider.clone(),
|
||||
session_source,
|
||||
/*parent_thread_id*/ None,
|
||||
config.model_verbosity,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
@@ -233,6 +234,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
|
||||
/*installation_id*/ TEST_INSTALLATION_ID.to_string(),
|
||||
provider.clone(),
|
||||
session_source,
|
||||
/*parent_thread_id*/ None,
|
||||
config.model_verbosity,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
@@ -350,6 +352,7 @@ async fn responses_respects_model_info_overrides_from_config() {
|
||||
/*installation_id*/ TEST_INSTALLATION_ID.to_string(),
|
||||
provider.clone(),
|
||||
session_source,
|
||||
/*parent_thread_id*/ None,
|
||||
config.model_verbosity,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
|
||||
@@ -488,6 +488,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
|
||||
item: RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: ThreadId::default(),
|
||||
parent_thread_id: None,
|
||||
timestamp: "2024-01-01T00:00:00Z".to_string(),
|
||||
cwd: ".".into(),
|
||||
originator: "test_originator".to_string(),
|
||||
@@ -618,6 +619,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
|
||||
item: RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: ThreadId::default(),
|
||||
parent_thread_id: None,
|
||||
timestamp: "2024-01-01T00:00:00Z".to_string(),
|
||||
cwd: ".".into(),
|
||||
originator: "test_originator".to_string(),
|
||||
@@ -902,6 +904,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
provider,
|
||||
SessionSource::Exec,
|
||||
/*parent_thread_id*/ None,
|
||||
config.model_verbosity,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
@@ -2358,6 +2361,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
provider.clone(),
|
||||
SessionSource::Exec,
|
||||
/*parent_thread_id*/ None,
|
||||
config.model_verbosity,
|
||||
/*enable_request_compression*/ false,
|
||||
/*include_timing_metrics*/ false,
|
||||
|
||||
@@ -2155,6 +2155,7 @@ async fn websocket_harness_with_provider_options(
|
||||
/*installation_id*/ TEST_INSTALLATION_ID.to_string(),
|
||||
provider.clone(),
|
||||
SessionSource::Exec,
|
||||
/*parent_thread_id*/ None,
|
||||
config.model_verbosity,
|
||||
/*enable_request_compression*/ false,
|
||||
runtime_metrics_enabled,
|
||||
|
||||
@@ -61,6 +61,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: TEST_TIMESTAMP.to_string(),
|
||||
cwd: std::path::PathBuf::from("."),
|
||||
originator: "test_originator".to_string(),
|
||||
@@ -109,6 +110,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: TEST_TIMESTAMP.to_string(),
|
||||
cwd: std::path::PathBuf::from("."),
|
||||
originator: "test_originator".to_string(),
|
||||
|
||||
@@ -130,8 +130,9 @@ async fn responses_api_parent_and_subagent_requests_include_identity_headers() -
|
||||
.header("x-codex-turn-metadata")
|
||||
.ok_or_else(|| anyhow!("child request missing x-codex-turn-metadata"))?,
|
||||
)?;
|
||||
assert!(child_turn_metadata.get("forked_from_thread_id").is_none());
|
||||
assert_eq!(
|
||||
child_turn_metadata["forked_from_thread_id"].as_str(),
|
||||
child_turn_metadata["parent_thread_id"].as_str(),
|
||||
Some(parent_thread_id)
|
||||
);
|
||||
|
||||
|
||||
@@ -136,8 +136,9 @@ async fn review_op_emits_lifecycle_and_review_output() {
|
||||
.expect("review request turn metadata"),
|
||||
)
|
||||
.expect("review request turn metadata json");
|
||||
assert!(turn_metadata.get("forked_from_thread_id").is_none());
|
||||
assert_eq!(
|
||||
turn_metadata["forked_from_thread_id"].as_str(),
|
||||
turn_metadata["parent_thread_id"].as_str(),
|
||||
Some(parent_thread_id.as_str())
|
||||
);
|
||||
|
||||
|
||||
@@ -184,6 +184,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()>
|
||||
RolloutRecorderParams::new(
|
||||
thread_id,
|
||||
/*forked_from_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
BaseInstructions::default(),
|
||||
|
||||
@@ -212,6 +212,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: "2026-01-27T12:00:00Z".to_string(),
|
||||
cwd: codex_home.to_path_buf(),
|
||||
originator: "test".to_string(),
|
||||
|
||||
@@ -211,6 +211,7 @@ async fn config_summary_entries_include_runtime_workspace_roots() {
|
||||
session_id: SessionId::new(),
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
thread_name: None,
|
||||
model: "gpt-5.4".to_string(),
|
||||
|
||||
@@ -1070,6 +1070,7 @@ fn session_configured_from_thread_start_response(
|
||||
session_configured_from_thread_response(
|
||||
&response.thread.session_id,
|
||||
&response.thread.id,
|
||||
response.thread.parent_thread_id.as_deref(),
|
||||
response.thread.thread_source.map(Into::into),
|
||||
response.thread.name.clone(),
|
||||
response.thread.path.clone(),
|
||||
@@ -1092,6 +1093,7 @@ fn session_configured_from_thread_resume_response(
|
||||
session_configured_from_thread_response(
|
||||
&response.thread.session_id,
|
||||
&response.thread.id,
|
||||
response.thread.parent_thread_id.as_deref(),
|
||||
response.thread.thread_source.map(Into::into),
|
||||
response.thread.name.clone(),
|
||||
response.thread.path.clone(),
|
||||
@@ -1123,6 +1125,7 @@ fn review_target_to_api(target: ReviewTarget) -> ApiReviewTarget {
|
||||
fn session_configured_from_thread_response(
|
||||
session_id: &str,
|
||||
thread_id: &str,
|
||||
parent_thread_id: Option<&str>,
|
||||
thread_source: Option<codex_protocol::protocol::ThreadSource>,
|
||||
thread_name: Option<String>,
|
||||
rollout_path: Option<PathBuf>,
|
||||
@@ -1140,11 +1143,16 @@ fn session_configured_from_thread_response(
|
||||
.map_err(|err| format!("session id `{session_id}` is invalid: {err}"))?;
|
||||
let thread_id = ThreadId::from_string(thread_id)
|
||||
.map_err(|err| format!("thread id `{thread_id}` is invalid: {err}"))?;
|
||||
let parent_thread_id = parent_thread_id
|
||||
.map(ThreadId::from_string)
|
||||
.transpose()
|
||||
.map_err(|err| format!("parent thread id is invalid: {err}"))?;
|
||||
|
||||
Ok(SessionConfiguredEvent {
|
||||
session_id,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id,
|
||||
thread_source,
|
||||
thread_name,
|
||||
model,
|
||||
|
||||
@@ -307,6 +307,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
id: "thread-1".to_string(),
|
||||
session_id: "thread-1".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
@@ -585,12 +586,33 @@ async fn session_configured_from_thread_response_preserves_thread_source() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configured_from_thread_response_preserves_parent_thread_id() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build config");
|
||||
let parent_thread_id = ThreadId::new();
|
||||
let mut response = sample_thread_start_response();
|
||||
response.thread.parent_thread_id = Some(parent_thread_id.to_string());
|
||||
|
||||
let event = session_configured_from_thread_start_response(&response, &config)
|
||||
.expect("build bootstrap session configured event");
|
||||
|
||||
assert_eq!(event.parent_thread_id, Some(parent_thread_id));
|
||||
}
|
||||
|
||||
fn sample_thread_start_response() -> ThreadStartResponse {
|
||||
ThreadStartResponse {
|
||||
thread: codex_app_server_protocol::Thread {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
@@ -111,6 +111,7 @@ fn session_configured_produces_thread_started_event() {
|
||||
session_id: SessionId::from(thread_id),
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
thread_name: None,
|
||||
model: "codex-mini-latest".to_string(),
|
||||
|
||||
@@ -299,6 +299,7 @@ mod tests {
|
||||
session_id: codex_protocol::SessionId::new(),
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
thread_name: None,
|
||||
model: "gpt-4o".to_string(),
|
||||
@@ -344,6 +345,7 @@ mod tests {
|
||||
session_id: codex_protocol::SessionId::new(),
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
thread_name: None,
|
||||
model: "gpt-4o".to_string(),
|
||||
@@ -411,6 +413,7 @@ mod tests {
|
||||
session_id: codex_protocol::SessionId::new(),
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
thread_name: None,
|
||||
model: "gpt-4o".to_string(),
|
||||
|
||||
@@ -171,7 +171,8 @@ impl MemoryStartupContext {
|
||||
context: &StageOneRequestContext,
|
||||
) -> anyhow::Result<(String, Option<TokenUsage>)> {
|
||||
let installation_id = resolve_installation_id(&config.codex_home).await?;
|
||||
let session_source = self.thread.config_snapshot().await.session_source;
|
||||
let config_snapshot = self.thread.config_snapshot().await;
|
||||
let session_source = config_snapshot.session_source;
|
||||
let model_client = ModelClient::new(
|
||||
Some(Arc::clone(&self.auth_manager)),
|
||||
SessionId::from(self.thread_id), // We use thread_id to detach this query from the foreground user session.
|
||||
@@ -179,6 +180,7 @@ impl MemoryStartupContext {
|
||||
installation_id,
|
||||
config.model_provider.clone(),
|
||||
session_source,
|
||||
config_snapshot.parent_thread_id,
|
||||
config.model_verbosity,
|
||||
config.features.enabled(Feature::EnableRequestCompression),
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
|
||||
@@ -2451,6 +2451,11 @@ impl InitialHistory {
|
||||
.and_then(|meta| meta.thread_source)
|
||||
}
|
||||
|
||||
pub fn get_resumed_parent_thread_id(&self) -> Option<ThreadId> {
|
||||
self.get_resumed_session_meta()
|
||||
.and_then(|meta| meta.parent_thread_id)
|
||||
}
|
||||
|
||||
fn get_resumed_session_meta(&self) -> Option<&SessionMeta> {
|
||||
match self {
|
||||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
|
||||
@@ -2716,6 +2721,8 @@ pub struct SessionMeta {
|
||||
pub id: ThreadId,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub forked_from_id: Option<ThreadId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
pub timestamp: String,
|
||||
pub cwd: PathBuf,
|
||||
pub originator: String,
|
||||
@@ -2750,6 +2757,7 @@ impl Default for SessionMeta {
|
||||
SessionMeta {
|
||||
id: ThreadId::default(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: String::new(),
|
||||
cwd: PathBuf::new(),
|
||||
originator: String::new(),
|
||||
@@ -3421,6 +3429,8 @@ pub struct SessionConfiguredEvent {
|
||||
pub thread_id: ThreadId,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub forked_from_id: Option<ThreadId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
/// Optional analytics source classification for this thread.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thread_source: Option<ThreadSource>,
|
||||
@@ -3490,6 +3500,7 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
|
||||
#[serde(default)]
|
||||
thread_id: Option<ThreadId>,
|
||||
forked_from_id: Option<ThreadId>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
#[serde(default)]
|
||||
thread_source: Option<ThreadSource>,
|
||||
#[serde(default)]
|
||||
@@ -3530,6 +3541,7 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
|
||||
session_id: wire.session_id,
|
||||
thread_id: wire.thread_id.unwrap_or_else(|| wire.session_id.into()),
|
||||
forked_from_id: wire.forked_from_id,
|
||||
parent_thread_id: wire.parent_thread_id,
|
||||
thread_source: wire.thread_source,
|
||||
thread_name: wire.thread_name,
|
||||
model: wire.model,
|
||||
@@ -5329,6 +5341,7 @@ mod tests {
|
||||
session_id,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
thread_name: None,
|
||||
model: "codex-mini-latest".to_string(),
|
||||
|
||||
@@ -62,6 +62,8 @@ pub struct ThreadItem {
|
||||
pub git_origin_url: Option<String>,
|
||||
/// Session source from session metadata.
|
||||
pub source: Option<SessionSource>,
|
||||
/// Immediate control/spawn parent thread id from session metadata.
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
/// Random unique nickname from session metadata for AgentControl-spawned sub-agents.
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Role (agent_role) from session metadata for AgentControl-spawned sub-agents.
|
||||
@@ -95,6 +97,7 @@ struct HeadTailSummary {
|
||||
git_sha: Option<String>,
|
||||
git_origin_url: Option<String>,
|
||||
source: Option<SessionSource>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
model_provider: Option<String>,
|
||||
@@ -778,6 +781,7 @@ async fn build_thread_item(
|
||||
git_sha,
|
||||
git_origin_url,
|
||||
source,
|
||||
parent_thread_id,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
@@ -799,6 +803,7 @@ async fn build_thread_item(
|
||||
git_sha,
|
||||
git_origin_url,
|
||||
source,
|
||||
parent_thread_id,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
@@ -1101,6 +1106,7 @@ async fn read_head_summary(path: &Path, head_limit: usize) -> io::Result<HeadTai
|
||||
RolloutItem::SessionMeta(session_meta_line) => {
|
||||
if !summary.saw_session_meta {
|
||||
summary.source = Some(session_meta_line.meta.source.clone());
|
||||
summary.parent_thread_id = session_meta_line.meta.parent_thread_id;
|
||||
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();
|
||||
|
||||
@@ -35,6 +35,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
|
||||
let session_meta = SessionMeta {
|
||||
id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: "2026-01-27T12:34:56Z".to_string(),
|
||||
cwd: dir.path().to_path_buf(),
|
||||
originator: "cli".to_string(),
|
||||
@@ -87,6 +88,7 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() {
|
||||
let session_meta = SessionMeta {
|
||||
id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: "2026-01-27T12:34:56Z".to_string(),
|
||||
cwd: dir.path().to_path_buf(),
|
||||
originator: "cli".to_string(),
|
||||
@@ -347,6 +349,7 @@ fn write_rollout_in_sessions_with_cwd(
|
||||
let session_meta = SessionMeta {
|
||||
id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: event_ts.to_string(),
|
||||
cwd,
|
||||
originator: "cli".to_string(),
|
||||
|
||||
@@ -81,6 +81,7 @@ pub enum RolloutRecorderParams {
|
||||
Create {
|
||||
conversation_id: ThreadId,
|
||||
forked_from_id: Option<ThreadId>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
base_instructions: BaseInstructions,
|
||||
@@ -156,6 +157,7 @@ impl RolloutRecorderParams {
|
||||
pub fn new(
|
||||
conversation_id: ThreadId,
|
||||
forked_from_id: Option<ThreadId>,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
base_instructions: BaseInstructions,
|
||||
@@ -164,6 +166,7 @@ impl RolloutRecorderParams {
|
||||
Self::Create {
|
||||
conversation_id,
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
source,
|
||||
thread_source,
|
||||
base_instructions,
|
||||
@@ -652,6 +655,7 @@ impl RolloutRecorder {
|
||||
RolloutRecorderParams::Create {
|
||||
conversation_id,
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
source,
|
||||
thread_source,
|
||||
base_instructions,
|
||||
@@ -673,6 +677,7 @@ impl RolloutRecorder {
|
||||
let session_meta = SessionMeta {
|
||||
id: session_id,
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
timestamp,
|
||||
cwd: config.cwd().to_path_buf(),
|
||||
originator: originator().value,
|
||||
@@ -1020,6 +1025,7 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
|
||||
git_sha,
|
||||
git_origin_url,
|
||||
source,
|
||||
parent_thread_id,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
@@ -1049,6 +1055,9 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
|
||||
if item.source.is_none() {
|
||||
item.source = source;
|
||||
}
|
||||
if item.parent_thread_id.is_none() {
|
||||
item.parent_thread_id = parent_thread_id;
|
||||
}
|
||||
if item.agent_nickname.is_none() {
|
||||
item.agent_nickname = agent_nickname;
|
||||
}
|
||||
@@ -1690,6 +1699,7 @@ fn thread_item_from_state_metadata(item: codex_state::ThreadMetadata) -> ThreadI
|
||||
.or_else(|_| serde_json::from_value(Value::String(item.source)))
|
||||
.unwrap_or(SessionSource::Unknown),
|
||||
),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: item.agent_nickname,
|
||||
agent_role: item.agent_role,
|
||||
model_provider: Some(item.model_provider),
|
||||
|
||||
@@ -85,6 +85,7 @@ async fn state_db_init_backfills_before_returning() -> anyhow::Result<()> {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: "2026-01-27T12:34:56Z".to_string(),
|
||||
cwd: home.path().to_path_buf(),
|
||||
originator: "test".to_string(),
|
||||
@@ -369,6 +370,7 @@ async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result<
|
||||
RolloutRecorderParams::new(
|
||||
thread_id,
|
||||
/*forked_from_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
BaseInstructions::default(),
|
||||
@@ -449,6 +451,7 @@ async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::i
|
||||
RolloutRecorderParams::new(
|
||||
thread_id,
|
||||
/*forked_from_id*/ None,
|
||||
/*parent_thread_id*/ None,
|
||||
SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
BaseInstructions::default(),
|
||||
@@ -974,6 +977,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
||||
git_sha: Some("filesystem-sha".to_string()),
|
||||
git_origin_url: Some("https://example.com/filesystem.git".to_string()),
|
||||
source: None,
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
@@ -991,6 +995,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
||||
git_sha: Some("state-sha".to_string()),
|
||||
git_origin_url: Some("https://example.com/state.git".to_string()),
|
||||
source: Some(SessionSource::Exec),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: Some("state-agent".to_string()),
|
||||
agent_role: Some("state-role".to_string()),
|
||||
model_provider: Some("state-provider".to_string()),
|
||||
|
||||
@@ -27,6 +27,7 @@ fn write_rollout_with_metadata(path: &Path, thread_id: ThreadId) -> std::io::Res
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp,
|
||||
cwd: ".".into(),
|
||||
originator: "test_originator".into(),
|
||||
|
||||
@@ -606,6 +606,7 @@ async fn test_list_conversations_latest_first() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -623,6 +624,7 @@ async fn test_list_conversations_latest_first() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -640,6 +642,7 @@ async fn test_list_conversations_latest_first() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -750,6 +753,7 @@ async fn test_pagination_cursor() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -767,6 +771,7 @@ async fn test_pagination_cursor() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -820,6 +825,7 @@ async fn test_pagination_cursor() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -837,6 +843,7 @@ async fn test_pagination_cursor() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -882,6 +889,7 @@ async fn test_pagination_cursor() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -1052,6 +1060,7 @@ async fn test_get_thread_contents() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -1251,6 +1260,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
|
||||
meta: SessionMeta {
|
||||
id: conversation_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: ts.to_string(),
|
||||
cwd: ".".into(),
|
||||
originator: "test_originator".into(),
|
||||
@@ -1403,6 +1413,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
@@ -1420,6 +1431,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
parent_thread_id: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
|
||||
@@ -322,6 +322,7 @@ mod tests {
|
||||
forked_from_id: Some(
|
||||
ThreadId::from_string(&Uuid::now_v7().to_string()).expect("thread id"),
|
||||
),
|
||||
parent_thread_id: None,
|
||||
timestamp: "2026-02-26T00:00:00.000Z".to_string(),
|
||||
cwd: PathBuf::from("/child/worktree"),
|
||||
originator: "codex_cli_rs".to_string(),
|
||||
@@ -479,6 +480,7 @@ mod tests {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: "2026-02-26T00:00:00.000Z".to_string(),
|
||||
cwd: PathBuf::from("/workspace"),
|
||||
originator: "codex_cli_rs".to_string(),
|
||||
|
||||
@@ -1318,6 +1318,7 @@ mod tests {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: metadata.created_at.to_rfc3339(),
|
||||
cwd: PathBuf::new(),
|
||||
originator: String::new(),
|
||||
@@ -1377,6 +1378,7 @@ mod tests {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
timestamp: created_at,
|
||||
cwd: PathBuf::new(),
|
||||
originator: String::new(),
|
||||
|
||||
@@ -328,6 +328,7 @@ fn stored_thread_from_state(
|
||||
.and_then(|metadata| metadata.rollout_path.clone())
|
||||
.or(rollout_path),
|
||||
forked_from_id: created.forked_from_id,
|
||||
parent_thread_id: created.parent_thread_id,
|
||||
preview: metadata
|
||||
.and_then(|metadata| metadata.preview.clone())
|
||||
.unwrap_or_default(),
|
||||
|
||||
@@ -30,6 +30,7 @@ pub(super) async fn create_thread(
|
||||
RolloutRecorderParams::new(
|
||||
params.thread_id,
|
||||
params.forked_from_id,
|
||||
params.parent_thread_id,
|
||||
params.source,
|
||||
params.thread_source,
|
||||
params.base_instructions,
|
||||
|
||||
@@ -122,6 +122,7 @@ pub(super) fn stored_thread_from_rollout_item(
|
||||
thread_id,
|
||||
rollout_path: Some(item.path),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: item.parent_thread_id,
|
||||
preview,
|
||||
name: None,
|
||||
model_provider: item
|
||||
|
||||
@@ -1023,6 +1023,7 @@ mod tests {
|
||||
CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Exec,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
|
||||
@@ -235,6 +235,7 @@ async fn read_thread_from_rollout_path(
|
||||
})?;
|
||||
if let Ok(meta_line) = read_session_meta_line(path.as_path()).await {
|
||||
thread.forked_from_id = meta_line.meta.forked_from_id;
|
||||
thread.parent_thread_id = meta_line.meta.parent_thread_id;
|
||||
if let Some(model_provider) = meta_line
|
||||
.meta
|
||||
.model_provider
|
||||
@@ -287,6 +288,7 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
.ok()
|
||||
.map(|meta_line| meta_line.meta);
|
||||
let forked_from_id = session_meta.as_ref().and_then(|meta| meta.forked_from_id);
|
||||
let parent_thread_id = session_meta.as_ref().and_then(|meta| meta.parent_thread_id);
|
||||
let preview = metadata
|
||||
.preview
|
||||
.clone()
|
||||
@@ -298,6 +300,7 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
thread_id: metadata.id,
|
||||
rollout_path: Some(metadata.rollout_path),
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
preview,
|
||||
name,
|
||||
model_provider: if metadata.model_provider.is_empty() {
|
||||
@@ -361,6 +364,7 @@ fn stored_thread_from_meta_line(
|
||||
thread_id: meta_line.meta.id,
|
||||
rollout_path: Some(path),
|
||||
forked_from_id: meta_line.meta.forked_from_id,
|
||||
parent_thread_id: meta_line.meta.parent_thread_id,
|
||||
preview: String::new(),
|
||||
name: None,
|
||||
model_provider: meta_line
|
||||
|
||||
@@ -72,6 +72,8 @@ pub struct CreateThreadParams {
|
||||
pub thread_id: ThreadId,
|
||||
/// Source thread id when this thread is created as a fork.
|
||||
pub forked_from_id: Option<ThreadId>,
|
||||
/// The ID of the parent thread. This will only be set if this thread is a subagent.
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
/// Runtime source for the thread.
|
||||
pub source: SessionSource,
|
||||
/// Optional analytics source classification for this thread.
|
||||
@@ -362,6 +364,8 @@ pub struct StoredThread {
|
||||
pub rollout_path: Option<PathBuf>,
|
||||
/// Source thread id when this thread was forked from another thread.
|
||||
pub forked_from_id: Option<ThreadId>,
|
||||
/// The ID of the parent thread. This will only be set if this thread is a subagent.
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
/// Best available user-facing preview, usually the first user message.
|
||||
pub preview: String,
|
||||
/// Optional user-facing thread name/title.
|
||||
|
||||
@@ -120,6 +120,7 @@ mod tests {
|
||||
id: thread_id.to_string(),
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
@@ -2767,6 +2767,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
||||
id: agent_thread_id.to_string(),
|
||||
session_id: agent_thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "agent thread".to_string(),
|
||||
ephemeral: false,
|
||||
model_provider: "agent-provider".to_string(),
|
||||
@@ -2856,6 +2857,7 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_
|
||||
id: agent_thread_id.to_string(),
|
||||
session_id: agent_thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "agent thread".to_string(),
|
||||
ephemeral: false,
|
||||
model_provider: "agent-provider".to_string(),
|
||||
@@ -2914,6 +2916,7 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() {
|
||||
id: read_thread_id.to_string(),
|
||||
session_id: read_thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "read thread".to_string(),
|
||||
ephemeral: false,
|
||||
model_provider: "read-provider".to_string(),
|
||||
@@ -5011,6 +5014,7 @@ async fn thread_rollback_response_discards_queued_active_thread_events() {
|
||||
id: thread_id.to_string(),
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
@@ -411,6 +411,7 @@ mod tests {
|
||||
id: read_thread_id.to_string(),
|
||||
session_id: read_thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: "read thread".to_string(),
|
||||
ephemeral: false,
|
||||
model_provider: "read-provider".to_string(),
|
||||
|
||||
@@ -2279,6 +2279,7 @@ mod tests {
|
||||
id: thread_id.to_string(),
|
||||
session_id: ThreadId::new().to_string(),
|
||||
forked_from_id: Some(forked_from_id.to_string()),
|
||||
parent_thread_id: None,
|
||||
preview: "hello".to_string(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
@@ -5726,6 +5726,7 @@ session_picker_view = "dense"
|
||||
id: thread_id.to_string(),
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::from("remote thread"),
|
||||
ephemeral: false,
|
||||
model_provider: String::from("openai"),
|
||||
@@ -5760,6 +5761,7 @@ session_picker_view = "dense"
|
||||
id: thread_id.to_string(),
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::from("preview"),
|
||||
ephemeral: false,
|
||||
model_provider: String::from("openai"),
|
||||
@@ -5828,6 +5830,7 @@ session_picker_view = "dense"
|
||||
id: thread_id.to_string(),
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::from("preview"),
|
||||
ephemeral: false,
|
||||
model_provider: String::from("openai"),
|
||||
@@ -5885,6 +5888,7 @@ session_picker_view = "dense"
|
||||
id: thread_id.to_string(),
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::from("preview"),
|
||||
ephemeral: false,
|
||||
model_provider: String::from("openai"),
|
||||
|
||||
Reference in New Issue
Block a user