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
2026-06-18 10:50:15 -07:00
committed by GitHub
Unverified
parent 9684ec25be
commit e8dd1b45cb
4 changed files with 129 additions and 0 deletions
+13
View File
@@ -24,6 +24,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
@@ -535,6 +536,18 @@ impl CodexThread {
live_thread.update_metadata(patch, include_archived).await
}
/// Appends rollout items through the live thread so derived metadata stays in sync.
pub async fn append_rollout_items(&self, items: &[RolloutItem]) -> ThreadStoreResult<()> {
let live_thread = self
.codex
.session
.live_thread_for_persistence("append rollout items")
.map_err(|err| ThreadStoreError::Internal {
message: err.to_string(),
})?;
live_thread.append_items(items).await
}
pub fn state_db(&self) -> Option<StateDbHandle> {
self.codex.state_db()
}