Preserve thread context when retrying or editing turns (#33211)

## What changed

- Add experimental `beforeTurnId` support to `thread/fork` for copying history strictly before a turn, including an in-progress turn.
- Use before-turn forks for TUI backtracking and safety retries so the replacement prompt retains thread context instead of recreating the branch from the preceding turn or a new thread.
- Add experimental `deferGoalContinuation` support so safety-retry forks can inherit an active goal while waiting for the replacement turn before automatic continuation resumes.
- Hide duplicated nested-review prompts when replaying history or selecting a prompt to edit.

## Testing

- Cover before-turn truncation, fork validation, active-goal inheritance, TUI safety retries, backtracking, and nested-review replay.

GitOrigin-RevId: 4915d85ae1929a0b8c40f3ed7dca29c7a5bdcdf3
This commit is contained in:
Felipe Coury authored and copyberry committed 2026-07-15 02:35:49 +00:00
1 parent e4711f2a3b
commit d88db19144
25 files changed
+1343 -96

No files matched your search

+34
View File
@@ -93,6 +93,40 @@ impl GoalService {
Self::default()
}
/// Restores persisted goal state into the registered runtime for `thread_id`.
pub async fn restore_thread_runtime_after_resume(
&self,
thread_id: ThreadId,
) -> Result<(), GoalServiceError> {
let runtime = self.runtime_for_thread(thread_id).ok_or_else(|| {
GoalServiceError::Internal(format!(
"goal runtime is unavailable for thread {thread_id}"
))
})?;
runtime
.restore_after_resume()
.await
.map_err(GoalServiceError::Internal)
}
/// Flushes any in-flight goal accounting before a fork copies the source goal snapshot.
pub async fn flush_thread_goal_progress_for_fork(
&self,
thread_id: ThreadId,
) -> Result<(), GoalServiceError> {
let Some(runtime) = self.runtime_for_thread(thread_id) else {
return Ok(());
};
let _goal_state_permit = runtime
.goal_state_permit()
.await
.map_err(GoalServiceError::Internal)?;
runtime
.prepare_external_goal_mutation()
.await
.map_err(GoalServiceError::Internal)
}
pub async fn get_thread_goal(
&self,
state_db: &codex_state::StateRuntime,
+9
View File
@@ -207,6 +207,15 @@ where
return;
}
if let Err(err) = self
.state_dbs
.thread_goals()
.clear_thread_goal_continuation_deferral(runtime.thread_id())
.await
{
tracing::warn!("failed to clear deferred goal continuation: {err}");
}
let accounting = runtime.accounting_state();
accounting.start_turn(
input.turn_id,
+11
View File
@@ -365,6 +365,17 @@ impl GoalRuntimeHandle {
// change the goal after we read it but before the continuation launches.
let _goal_state_permit = self.goal_state_permit().await?;
if self
.inner
.state_dbs
.thread_goals()
.has_thread_goal_continuation_deferral(self.thread_id())
.await
.map_err(|err| err.to_string())?
{
return Ok(());
}
let Some(thread_manager) = self.inner.thread_manager.upgrade() else {
tracing::debug!("skipping goal continuation because thread manager is unavailable");
return Ok(());