[2 of 2] Finish moving goal runtime to extension (#26548)

## Stack

1. [#26547](https://github.com/openai/codex/pull/26547) - [1 of 2] Align
goal extension with core behavior
2. [#26548](https://github.com/openai/codex/pull/26548) - [2 of 2] Move
goal runtime to extension

## Why

This PR completes the switch of the goal behavior to the
extension-backed runtime and removes the old core goal implementation.

## What Changed

- Installs the goal extension for app-server `ThreadManager` sessions.
- Routes app-server thread goal `get`, `set`, and `clear` through
`GoalService`.
- Uses thread-idle lifecycle emission after goal resume and snapshot
ordering so the extension can decide whether to continue the goal.
- Forwards extension goal updates through a FIFO async app-server
notification path so backpressure does not drop them or reorder updates.
- Keeps review turns from enabling goal runtime behavior.
- Plans extension tools before dynamic tools so built-in goal tool names
keep their old precedence when goals are enabled.
- Removes the old core goal runtime, core goal tool handlers, and core
goal tool specs.
- Updates tests that were coupled to the core-owned goal runtime while
leaving the legacy `<goal_context>` compatibility path in core for old
threads.
- Removes the stale cargo-shear ignore now that `codex-goal-extension`
is used by the workspace.
- Keeps realtime event matching exhaustive after removing the old
goal-specific realtime text path.


## Validation

- Ran manual `/goal` runs in TUI. Validated time accounting matched
wall-clock time and goal lifecycle state transitions.
This commit is contained in:
Eric Traut
2026-06-05 14:17:30 -07:00
committed by GitHub
Unverified
parent 679cc08445
commit 479a14cf59
34 changed files with 280 additions and 3908 deletions
+38 -1
View File
@@ -17,6 +17,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::sync::Weak;
use tokio::sync::Mutex;
use tokio::sync::mpsc;
@@ -44,8 +45,9 @@ pub(crate) struct PendingThreadResumeRequest {
pub(crate) enum ThreadListenerCommand {
// SendThreadResumeResponse is used to resume an already running thread by sending the thread's history to the client and atomically subscribing for new updates.
SendThreadResumeResponse(Box<PendingThreadResumeRequest>),
// EmitThreadGoalUpdated is used to order app-server goal updates with running-thread resume responses.
// EmitThreadGoalUpdated is used to order goal updates with running-thread resume responses and goal clears.
EmitThreadGoalUpdated {
turn_id: Option<String>,
goal: ThreadGoal,
},
// EmitThreadGoalCleared is used to order app-server goal clears with running-thread resume responses.
@@ -284,6 +286,10 @@ pub(crate) struct ConnectionCapabilities {
#[derive(Clone, Default)]
pub(crate) struct ThreadStateManager {
state: Arc<Mutex<ThreadStateManagerInner>>,
// Extension event sinks are synchronous, so they need an await-free way to
// enqueue work on the active per-thread listener.
listener_commands:
Arc<StdMutex<HashMap<ThreadId, mpsc::UnboundedSender<ThreadListenerCommand>>>>,
}
impl ThreadStateManager {
@@ -337,6 +343,35 @@ impl ThreadStateManager {
state.threads.entry(thread_id).or_default().state.clone()
}
pub(crate) fn current_listener_command_tx(
&self,
thread_id: ThreadId,
) -> Option<mpsc::UnboundedSender<ThreadListenerCommand>> {
self.listener_commands
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&thread_id)
.cloned()
}
pub(crate) fn register_listener_command_tx(
&self,
thread_id: ThreadId,
tx: mpsc::UnboundedSender<ThreadListenerCommand>,
) {
self.listener_commands
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(thread_id, tx);
}
pub(crate) fn unregister_listener_command_tx(&self, thread_id: ThreadId) {
self.listener_commands
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&thread_id);
}
pub(crate) async fn remove_thread_state(&self, thread_id: ThreadId) {
let thread_state = {
let mut state = self.state.lock().await;
@@ -350,6 +385,7 @@ impl ThreadStateManager {
});
thread_state
};
self.unregister_listener_command_tx(thread_id);
if let Some(thread_state) = thread_state {
let mut thread_state = thread_state.lock().await;
@@ -375,6 +411,7 @@ impl ThreadStateManager {
};
for (thread_id, thread_state) in thread_states {
self.unregister_listener_command_tx(thread_id);
let mut thread_state = thread_state.lock().await;
tracing::debug!(
thread_id = %thread_id,