mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Add experimental attached thread forks (#56351)
## What changed - Add `attachToSource` to `thread/fork` to persist a fork attached to its source without emitting `thread/started`. Attached forks require local storage and cannot use `ephemeral` or `deferGoalContinuation`. - Hide attached threads from ordinary listings and add `attachedToThreadId` to `thread/list` for explicit discovery, including empty threads. Keep this relationship separate from spawned-agent parent and ancestor filters. - Persist attachment metadata in rollouts and SQLite so it survives restarts and database recovery. Reject source deletion until its attached threads are deleted. ## Testing Add coverage for legacy and paginated history, restart and missing or corrupt database recovery, filtered listings and pagination, resume, conflicting options, and deletion safeguards across store instances. GitOrigin-RevId: 589f3174fc36f6054e75d65818360695645a5913
This commit is contained in:
@@ -2697,6 +2697,7 @@ impl ThreadRequestProcessor {
|
||||
search_term,
|
||||
parent_thread_id,
|
||||
ancestor_thread_id,
|
||||
attached_to_thread_id,
|
||||
} = params;
|
||||
let excluded_thread_ids = excluded_thread_ids.unwrap_or_default();
|
||||
if excluded_thread_ids.len() > THREAD_LIST_MAX_EXCLUDED_IDS {
|
||||
@@ -2740,21 +2741,42 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
}
|
||||
let cwd_filters = normalize_thread_list_cwd_filters(cwd)?;
|
||||
let relation_filter = match (parent_thread_id, ancestor_thread_id) {
|
||||
(Some(_), Some(_)) => {
|
||||
let relation_filter = match (parent_thread_id, ancestor_thread_id, attached_to_thread_id) {
|
||||
(Some(_), Some(_), None) => {
|
||||
return Err(invalid_request(
|
||||
"parentThreadId and ancestorThreadId are mutually exclusive",
|
||||
));
|
||||
}
|
||||
(Some(parent_thread_id), None) => Some(StoreThreadRelationFilter::DirectChildrenOf(
|
||||
ThreadId::from_string(&parent_thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid parent thread id: {err}")))?,
|
||||
)),
|
||||
(None, Some(ancestor_thread_id)) => Some(StoreThreadRelationFilter::DescendantsOf(
|
||||
ThreadId::from_string(&ancestor_thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid ancestor thread id: {err}")))?,
|
||||
)),
|
||||
(None, None) => None,
|
||||
(Some(parent_thread_id), None, None) => {
|
||||
Some(StoreThreadRelationFilter::DirectChildrenOf(
|
||||
ThreadId::from_string(&parent_thread_id).map_err(|err| {
|
||||
invalid_request(format!("invalid parent thread id: {err}"))
|
||||
})?,
|
||||
))
|
||||
}
|
||||
(None, Some(ancestor_thread_id), None) => {
|
||||
Some(StoreThreadRelationFilter::DescendantsOf(
|
||||
ThreadId::from_string(&ancestor_thread_id).map_err(|err| {
|
||||
invalid_request(format!("invalid ancestor thread id: {err}"))
|
||||
})?,
|
||||
))
|
||||
}
|
||||
(None, None, Some(parent)) => {
|
||||
if self.state_db.is_none() || !self.thread_store.as_any().is::<LocalThreadStore>() {
|
||||
return Err(unsupported_thread_store_operation("attached threads"));
|
||||
}
|
||||
Some(StoreThreadRelationFilter::AttachedThreadsOf(
|
||||
ThreadId::from_string(&parent).map_err(|err| {
|
||||
invalid_request(format!("invalid attached thread parent id: {err}"))
|
||||
})?,
|
||||
))
|
||||
}
|
||||
(_, _, Some(_)) => {
|
||||
return Err(invalid_request(
|
||||
"attachedToThreadId cannot be combined with parentThreadId or ancestorThreadId",
|
||||
));
|
||||
}
|
||||
(None, None, None) => None,
|
||||
};
|
||||
|
||||
let requested_page_size = limit
|
||||
@@ -5269,11 +5291,22 @@ impl ThreadRequestProcessor {
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
ephemeral,
|
||||
attach_to_source,
|
||||
thread_source,
|
||||
exclude_turns,
|
||||
defer_goal_continuation,
|
||||
} = params;
|
||||
let include_turns = !exclude_turns;
|
||||
if attach_to_source {
|
||||
if ephemeral || defer_goal_continuation {
|
||||
return Err(invalid_request(
|
||||
"attachToSource cannot be combined with ephemeral or deferGoalContinuation",
|
||||
));
|
||||
}
|
||||
if self.state_db.is_none() || !self.thread_store.as_any().is::<LocalThreadStore>() {
|
||||
return Err(unsupported_thread_store_operation("attached threads"));
|
||||
}
|
||||
}
|
||||
if sandbox.is_some() && permissions.is_some() {
|
||||
return Err(invalid_request(
|
||||
"`permissions` cannot be combined with `sandbox`",
|
||||
@@ -5645,6 +5678,7 @@ impl ThreadRequestProcessor {
|
||||
};
|
||||
|
||||
let fork_options = StartThreadOptions {
|
||||
attached_thread: attach_to_source,
|
||||
thread_source,
|
||||
parent_trace,
|
||||
reserved_thread_id,
|
||||
@@ -5877,9 +5911,11 @@ impl ThreadRequestProcessor {
|
||||
.await;
|
||||
}
|
||||
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::ThreadStarted(notif))
|
||||
.await;
|
||||
if !attach_to_source {
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::ThreadStarted(notif))
|
||||
.await;
|
||||
}
|
||||
if inherited_goal {
|
||||
self.thread_goal_processor
|
||||
.emit_thread_goal_snapshot(thread_id)
|
||||
|
||||
Reference in New Issue
Block a user