mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Preserve turn lineage across goal continuations (#41562)
## Why Automatic goal continuations should remain attributable to the turn that created the goal. External input, hook context, or goal edits can make that attribution ambiguous and must not leave stale lineage metadata behind. ## What changed - Carry the trusted root and previous parent turn through successive automatic goal continuations. - Invalidate stored lineage when external context reaches an active turn or when a goal is edited or cleared. - Preserve lineage for async hook results only when they belong to the receiving turn. ## Testing - Cover goal continuations across intervening user turns and external objective edits. - Cover active, buffered, and externally injected hook or response context. GitOrigin-RevId: 22ccc07961cd0ed8f4b17912b7efa416b342bf50
This commit is contained in:
1 parent
6478a751fd
commit
4210c08def
9 files changed
+410
-10
No files matched your search
@@ -275,6 +275,10 @@ impl GoalService {
|
||||
.map(|goal| (goal, Some(previous_goal)))?
|
||||
};
|
||||
|
||||
if let Some(runtime) = runtime.as_ref() {
|
||||
runtime.invalidate_turn_lineage().await;
|
||||
}
|
||||
|
||||
if objective.is_some() {
|
||||
fill_empty_thread_preview_if_possible(state_db, thread_id, &goal).await;
|
||||
}
|
||||
@@ -316,6 +320,9 @@ impl GoalService {
|
||||
GoalServiceError::Internal(format!("failed to clear thread goal: {err}"))
|
||||
})?;
|
||||
let cleared = cleared_goal.is_some();
|
||||
if cleared && let Some(runtime) = runtime.as_ref() {
|
||||
runtime.invalidate_turn_lineage().await;
|
||||
}
|
||||
drop(goal_state_permit);
|
||||
drop(runtime);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Weak;
|
||||
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::TurnStartOptions;
|
||||
use codex_extension_api::ConfigContributor;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::ExtensionEventSink;
|
||||
@@ -41,6 +42,7 @@ use crate::metrics::GoalMetrics;
|
||||
use crate::runtime::ActiveGoalStopReason;
|
||||
use crate::runtime::GoalRuntimeConfig;
|
||||
use crate::runtime::GoalRuntimeHandle;
|
||||
use crate::spec::CREATE_GOAL_TOOL_NAME;
|
||||
use crate::spec::UPDATE_GOAL_TOOL_NAME;
|
||||
use crate::steering::budget_limit_steering_item;
|
||||
use crate::tool::GoalToolExecutor;
|
||||
@@ -288,6 +290,7 @@ where
|
||||
)
|
||||
.await
|
||||
{
|
||||
input.thread_store.remove::<TurnStartOptions>();
|
||||
tracing::warn!(
|
||||
"failed to stop active goal after repeated execution failures for {turn_id}: {err}"
|
||||
);
|
||||
@@ -302,12 +305,27 @@ where
|
||||
)
|
||||
.await
|
||||
{
|
||||
input.thread_store.remove::<TurnStartOptions>();
|
||||
tracing::warn!(
|
||||
"failed to account active goal progress at turn stop for {turn_id}: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
runtime.accounting_state().finish_turn(turn_id);
|
||||
let accounting = runtime.accounting_state();
|
||||
if accounting
|
||||
.current_active_goal_id_for_turn(turn_id)
|
||||
.is_some()
|
||||
&& let Some(options) = input.thread_store.get::<TurnStartOptions>()
|
||||
{
|
||||
input.thread_store.insert_if(
|
||||
TurnStartOptions {
|
||||
parent_turn_id: Some(turn_id.to_string()),
|
||||
..options.as_ref().clone()
|
||||
},
|
||||
|current| current.is_some(),
|
||||
);
|
||||
}
|
||||
accounting.finish_turn(turn_id);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -321,6 +339,7 @@ where
|
||||
}
|
||||
|
||||
let turn_id = input.turn_store.level_id();
|
||||
input.thread_store.remove::<TurnStartOptions>();
|
||||
if let Err(err) = runtime
|
||||
.account_active_goal_progress(
|
||||
turn_id,
|
||||
@@ -412,6 +431,23 @@ where
|
||||
input.tool_name,
|
||||
input.outcome,
|
||||
);
|
||||
if input.tool_name.is_default_namespace()
|
||||
&& input.tool_name.name == CREATE_GOAL_TOOL_NAME
|
||||
&& matches!(input.outcome, ToolCallOutcome::Completed { success: true })
|
||||
{
|
||||
input.thread_store.remove::<TurnStartOptions>();
|
||||
if let Ok(_goal_state_permit) = runtime.goal_state_permit().await
|
||||
&& let Some(thread_manager) = self.thread_manager.upgrade()
|
||||
&& let Ok(thread) = thread_manager.get_thread(runtime.thread_id()).await
|
||||
&& let Some(root_turn_id) = thread.active_turn_root(input.turn_id).await
|
||||
{
|
||||
input.thread_store.insert(TurnStartOptions {
|
||||
root_turn_id: Some(root_turn_id),
|
||||
parent_turn_id: Some(input.turn_id.to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
let should_count_for_goal_progress =
|
||||
tool_attempt_counts_for_goal_progress(input.outcome)
|
||||
&& !(input.tool_name.is_default_namespace()
|
||||
|
||||
@@ -135,6 +135,19 @@ impl GoalRuntimeHandle {
|
||||
self.inner.root_accounting_state.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn invalidate_turn_lineage(&self) {
|
||||
let Some(thread_manager) = self.inner.thread_manager.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Ok(thread) = thread_manager.get_thread(self.inner.thread_id).await else {
|
||||
return;
|
||||
};
|
||||
thread.thread_extension_data().remove::<TurnStartOptions>();
|
||||
if let Some(turn_id) = self.inner.accounting_state.current_turn_id() {
|
||||
thread.invalidate_turn_lineage(turn_id.as_str()).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn goal_state_permit(&self) -> Result<SemaphorePermit<'_>, String> {
|
||||
self.inner
|
||||
.goal_state_lock
|
||||
@@ -430,13 +443,18 @@ impl GoalRuntimeHandle {
|
||||
self.inner.accounting_state.clear_active_goal();
|
||||
return Ok(());
|
||||
}
|
||||
let start_options = thread
|
||||
.thread_extension_data()
|
||||
.get::<TurnStartOptions>()
|
||||
.map(|options| options.as_ref().clone())
|
||||
.unwrap_or_default();
|
||||
let item = continuation_steering_item(&protocol_goal_from_state(goal));
|
||||
|
||||
match thread
|
||||
.start_turn_if_idle(
|
||||
TurnInputRequest::new(TurnInput::ResponseItem(item)).on_start(TurnStartOptions {
|
||||
turn_trigger: Some("goal".to_string()),
|
||||
..Default::default()
|
||||
..start_options
|
||||
}),
|
||||
)
|
||||
.await
|
||||
|
||||
Reference in new issue
Block a user