chore: isolate thread goal storage behind GoalStore (#23295)

## Why

Thread goal persistence is being prepared for a dedicated storage
boundary. Before that split, goal-specific reads, writes, accounting,
and cleanup were exposed directly on `StateRuntime`, so core and
app-server callsites stayed coupled to the full runtime instead of a
goal-specific store.

This PR introduces that boundary without changing the goal wire API or
current persistence behavior. Callers now go through
`StateRuntime::thread_goals()` and the new `GoalStore`, while
`GoalStore` still uses the existing state DB pool underneath.

## What changed

- Added `GoalStore` in `state/src/runtime/goals.rs` and exposed it from
`StateRuntime` via `thread_goals()`.
- Moved thread-goal reads, writes, status updates, pause, delete, and
usage accounting onto `GoalStore`.
- Updated core session goal handling, app-server goal RPCs, resume
snapshots, and goal tests to use the store boundary.
- Kept thread deletion responsible for cascading goal cleanup by
deleting the goal through the store only after a thread row is removed.

## Testing

- Existing goal persistence, resume, and accounting tests were updated
to exercise the new `GoalStore` access path.
This commit is contained in:
jif-oai
2026-05-18 14:47:05 +02:00
committed by GitHub
parent 6a8173588c
commit 7ee7fe239f
10 changed files with 203 additions and 43 deletions

View File

@@ -150,12 +150,14 @@ impl ThreadGoalRequestProcessor {
let (goal, previous_status) = (if let Some(objective) = objective {
let existing_goal = state_db
.thread_goals()
.get_thread_goal(thread_id)
.await
.map_err(|err| invalid_request(err.to_string()))?;
if let Some(goal) = existing_goal.as_ref() {
let previous_status = ExternalGoalPreviousStatus::from(goal);
state_db
.thread_goals()
.update_thread_goal(
thread_id,
codex_state::ThreadGoalUpdate {
@@ -177,6 +179,7 @@ impl ThreadGoalRequestProcessor {
} else {
let previous_status = ExternalGoalPreviousStatus::NewGoal;
state_db
.thread_goals()
.replace_thread_goal(
thread_id,
objective,
@@ -188,6 +191,7 @@ impl ThreadGoalRequestProcessor {
}
} else {
let existing_goal = state_db
.thread_goals()
.get_thread_goal(thread_id)
.await
.map_err(|err| invalid_request(err.to_string()))?;
@@ -198,6 +202,7 @@ impl ThreadGoalRequestProcessor {
};
let previous_status = ExternalGoalPreviousStatus::from(&existing_goal);
state_db
.thread_goals()
.update_thread_goal(
thread_id,
codex_state::ThreadGoalUpdate {
@@ -246,6 +251,7 @@ impl ThreadGoalRequestProcessor {
let thread_id = parse_thread_id_for_request(params.thread_id.as_str())?;
let state_db = self.state_db_for_materialized_thread(thread_id).await?;
let goal = state_db
.thread_goals()
.get_thread_goal(thread_id)
.await
.map_err(|err| internal_error(format!("failed to read thread goal: {err}")))?
@@ -303,6 +309,7 @@ impl ThreadGoalRequestProcessor {
thread_state.listener_command_tx()
};
let cleared = state_db
.thread_goals()
.delete_thread_goal(thread_id)
.await
.map_err(|err| internal_error(format!("failed to clear thread goal: {err}")))?;

View File

@@ -676,7 +676,7 @@ pub(super) async fn send_thread_goal_snapshot_notification(
thread_id: ThreadId,
state_db: &StateDbHandle,
) {
match state_db.get_thread_goal(thread_id).await {
match state_db.thread_goals().get_thread_goal(thread_id).await {
Ok(Some(goal)) => {
outgoing
.send_server_notification(ServerNotification::ThreadGoalUpdated(