feat: account active goal progress in the goal extension (#23696)

## Why

The goal extension can create and surface goals, but the live
turn-accounting path still stopped short of persisting active-goal
progress. That leaves token and wall-clock usage, plus
`ThreadGoalUpdated` events, out of sync with the extension boundary once
work actually advances or a goal transitions out of active state.

## What changed

- Teach `GoalAccountingState` to track the current turn, active goal,
token deltas, and wall-clock progress snapshots against the persisted
goal id.
- Flush active-goal accounting from tool-finish, turn-stop, and
turn-abort lifecycle hooks, and emit `ThreadGoalUpdated` events when
persisted progress changes.
- Route `create_goal` and `update_goal` through the same accounting
state so new goals start from the right baseline, final progress is
flushed before status changes, and `update_goal` can mark a goal
`blocked` as well as `complete`.
- Keep budget-limited goals accruing through the end of the turn while
clearing local active-goal state once a turn or explicit update is
finished.
- Expand backend and lifecycle coverage around store ids, baseline
reset, tool-finish accounting, budget-limited carry-through, and
blocked-goal updates.

## Testing

- Added focused backend coverage in
`codex-rs/ext/goal/tests/goal_extension_backend.rs` for baseline reset,
tool-finish accounting, budget-limited turns, and blocked-goal updates.
- Extended `codex-rs/core/src/session/tests.rs` to assert that lifecycle
inputs expose the expected session, thread, and turn store ids.
This commit is contained in:
jif-oai
2026-05-20 18:36:37 +02:00
committed by GitHub
parent f198ca115b
commit d4f842f3b3
6 changed files with 978 additions and 111 deletions
+34 -37
View File
@@ -1918,6 +1918,9 @@ async fn turn_start_lifecycle_exposes_turn_metadata_and_token_baseline() {
#[derive(Debug, PartialEq, Eq)]
struct RecordedTurnStart {
session_level_id: String,
thread_level_id: String,
turn_level_id: String,
turn_id: String,
collaboration_mode: CollaborationMode,
token_usage_at_turn_start: TokenUsage,
@@ -1936,6 +1939,9 @@ async fn turn_start_lifecycle_exposes_turn_metadata_and_token_baseline() {
.lock()
.expect("turn start records lock")
.push(RecordedTurnStart {
session_level_id: input.session_store.level_id().to_string(),
thread_level_id: input.thread_store.level_id().to_string(),
turn_level_id: input.turn_store.level_id().to_string(),
turn_id: input.turn_id.to_string(),
collaboration_mode: input.collaboration_mode.clone(),
token_usage_at_turn_start: input.token_usage_at_turn_start.clone(),
@@ -1965,52 +1971,43 @@ async fn turn_start_lifecycle_exposes_turn_metadata_and_token_baseline() {
.insert(ThreadTurnStartMarker);
let token_usage_at_turn_start = TokenUsage {
input_tokens: 120,
cached_input_tokens: 15,
output_tokens: 40,
reasoning_output_tokens: 9,
total_tokens: 169,
input_tokens: 100,
cached_input_tokens: 40,
output_tokens: 25,
reasoning_output_tokens: 5,
total_tokens: 130,
};
session
.state
.lock()
.await
.set_token_info(Some(TokenUsageInfo {
total_token_usage: token_usage_at_turn_start.clone(),
last_token_usage: TokenUsage::default(),
model_context_window: turn_context.model_context_window(),
}));
set_total_token_usage(&session, token_usage_at_turn_start.clone()).await;
let turn_context = Arc::new(turn_context);
let session = Arc::new(session);
session
.spawn_task(
Arc::clone(&turn_context),
Vec::new(),
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: true,
},
)
.await;
let expected = RecordedTurnStart {
session_level_id: session.session_id().to_string(),
thread_level_id: session.conversation_id.to_string(),
turn_level_id: turn_context.sub_id.clone(),
turn_id: turn_context.sub_id.clone(),
collaboration_mode: turn_context.collaboration_mode.clone(),
token_usage_at_turn_start,
saw_session_store: true,
saw_thread_store: true,
};
session.abort_all_tasks(TurnAbortReason::Interrupted).await;
let sess = Arc::new(session);
sess.spawn_task(
Arc::new(turn_context),
Vec::new(),
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: true,
},
)
.await;
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
let actual = records
.lock()
.expect("turn start records lock")
.drain(..)
.collect::<Vec<_>>();
assert_eq!(
vec![RecordedTurnStart {
turn_id: turn_context.sub_id.clone(),
collaboration_mode: turn_context.collaboration_mode.clone(),
token_usage_at_turn_start,
saw_session_store: true,
saw_thread_store: true,
}],
actual
);
assert_eq!(vec![expected], actual);
}
#[tokio::test]