Fix goal-first live threads missing from thread/list (#28808)

Fixes #28263.

## Why

When a thread starts with `/goal`, the goal extension can update SQLite
goal state before the thread has any user-turn rollout items.
`thread/list` and `thread/search` rely on persisted listing metadata, so
a goal-first live thread could be absent from app-server listings after
restart even though the goal itself existed.

This regressed when goal handling moved out of core: the core path wrote
the goal update through the live thread rollout path, while the
extension-backed app-server path only updated goal state and emitted the
live notification.

## What

- Add `GoalSetOutcome::thread_goal_updated_item()` so the goal extension
owns the canonical `ThreadGoalUpdated` rollout item shape.
- Expose a narrow `CodexThread::append_rollout_items()` helper that
appends through the live thread and keeps derived SQLite metadata in
sync.
- When app-server sets a goal on an active live thread, persist the goal
update through that live-thread path.
- Add an app-server regression test that starts a live thread with
`thread/goal/set` and verifies it appears in state-DB-only
`thread/list`.

## Verification

- `env -u CODEX_SQLITE_HOME just test -p codex-app-server
goal_first_live_thread_appears_in_state_db_thread_list`
This commit is contained in:
Eric Traut authored and GitHub committed 2026-06-18 10:50:15 -07:00
1 parent 9684ec25be
commit e8dd1b45cb
4 files changed
+129

No files matched your search

+11
View File
@@ -6,8 +6,11 @@ use std::sync::PoisonError;
use std::sync::Weak;
use codex_protocol::ThreadId;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::ThreadGoal;
use codex_protocol::protocol::ThreadGoalStatus;
use codex_protocol::protocol::ThreadGoalUpdatedEvent;
use codex_protocol::protocol::validate_thread_goal_objective;
use crate::runtime::GoalRuntimeHandle;
@@ -61,6 +64,14 @@ pub struct GoalSetOutcome {
}
impl GoalSetOutcome {
pub fn thread_goal_updated_item(&self) -> RolloutItem {
RolloutItem::EventMsg(EventMsg::ThreadGoalUpdated(ThreadGoalUpdatedEvent {
thread_id: self.goal.thread_id,
turn_id: None,
goal: self.goal.clone(),
}))
}
pub async fn apply_runtime_effects(&self, goal_service: &GoalService) {
if let Some(runtime) = goal_service.runtime_for_thread(self.goal.thread_id)
&& let Err(err) = runtime