Capture saved Daybreak preferences for native helpers (#60749)

## What changed

Add the disabled-by-default `daybreak_program_propagation` feature to capture a parent's saved Daybreak choice when spawning native helpers. Retain the choice and account identity for helper turns and nested spawns, while preserving explicit `cyber_access_program` overrides. A helper's own saved preference takes precedence on later turns.

Read pending metadata before persisted metadata so preferences survive thread materialization, with stored choices taking precedence. Share this resolution logic with `thread/read` and support refreshing the feature through user configuration.

## Testing

Add coverage for saved and pending preferences, unset choices, explicit overrides, failed metadata reads, nested spawns, parent unloading, feature withdrawal, and preference changes between helper turns.

GitOrigin-RevId: b3b349da8cb3d48154cddf058890587385efcda2
This commit is contained in:
jamy-OAI
2026-11-11 07:39:04 +00:00
committed by copyberry
parent c320b5c69e
commit d9a32c1789
23 changed files with 780 additions and 24 deletions
@@ -50,6 +50,7 @@ use codex_protocol::mcp::ClientMcpExtensions;
use codex_protocol::mcp::plain_output_text;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_thread_store::LocalHistoryAccount;
use codex_thread_store::PendingDaybreakPreference;
use codex_thread_store::PersistContext;
use std::ops::ControlFlow;
@@ -3143,17 +3144,14 @@ impl ThreadRequestProcessor {
thread_id: ThreadId,
include_turns: bool,
) -> Result<Thread, ThreadReadViewError> {
// Read staging first: persistence can consume it while the stored thread is read.
let pending_daybreak_enabled = self
.thread_store
.read_pending_thread_metadata(thread_id)
.await
.map_err(|err| {
ThreadReadViewError::Internal(format!(
"failed to read pending thread metadata: {err}"
))
})?
.and_then(|metadata| metadata.daybreak_enabled);
let pending_preference =
PendingDaybreakPreference::read(self.thread_store.as_ref(), thread_id)
.await
.map_err(|err| {
ThreadReadViewError::Internal(format!(
"failed to read pending thread metadata: {err}"
))
})?;
let loaded_thread = self.thread_manager.get_thread(thread_id).await.ok();
let mut thread = if include_turns {
if let Some(loaded_thread) = loaded_thread.as_ref() {
@@ -3207,7 +3205,7 @@ impl ThreadRequestProcessor {
)));
};
thread.daybreak_enabled = thread.daybreak_enabled.or(pending_daybreak_enabled);
thread.daybreak_enabled = pending_preference.resolve(thread.daybreak_enabled);
let has_live_in_progress_turn = if let Some(loaded_thread) = loaded_thread.as_ref() {
matches!(loaded_thread.agent_status().await, AgentStatus::Running)
@@ -3280,14 +3278,7 @@ impl ThreadRequestProcessor {
.await
{
Ok(stored_thread) => Ok(Some(stored_thread)),
Err(ThreadStoreError::InvalidRequest { message })
if message == format!("no rollout found for thread id {thread_id}") =>
{
Ok(None)
}
Err(ThreadStoreError::ThreadNotFound {
thread_id: missing_thread_id,
}) if missing_thread_id == thread_id => Ok(None),
Err(err) if err.is_missing_for_read(thread_id) => Ok(None),
Err(ThreadStoreError::InvalidRequest { message }) => {
Err(ThreadReadViewError::InvalidRequest(message))
}