Add turn-scoped context contributions (#28911)

## Summary
- keep context injection on a single ContextContributor trait
- split context injection into thread-scoped and turn-scoped
contribution methods
- wire turn-scoped fragments into initial context assembly so extensions
can contribute context from turn-local state
This commit is contained in:
jif authored and GitHub committed 2026-06-18 19:40:28 +02:00
1 parent 790213ded0
commit 9684ec25be
14 files changed
+307 -33

No files matched your search

+1 -1
View File
@@ -107,7 +107,7 @@ impl codex_extension_api::ThreadLifecycleContributor<Config> for GuardianMemoryC
} }
impl codex_extension_api::ContextContributor for GuardianMemoryContextProbe { impl codex_extension_api::ContextContributor for GuardianMemoryContextProbe {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
_session_store: &'a codex_extension_api::ExtensionData, _session_store: &'a codex_extension_api::ExtensionData,
thread_store: &'a codex_extension_api::ExtensionData, thread_store: &'a codex_extension_api::ExtensionData,
+113 -16
View File
@@ -58,7 +58,9 @@ use codex_exec_server::Environment;
use codex_exec_server::EnvironmentManager; use codex_exec_server::EnvironmentManager;
use codex_extension_api::ExtensionDataInit; use codex_extension_api::ExtensionDataInit;
use codex_extension_api::LoadedUserInstructions; use codex_extension_api::LoadedUserInstructions;
use codex_extension_api::PromptFragment;
use codex_extension_api::PromptSlot; use codex_extension_api::PromptSlot;
use codex_extension_api::TurnContextContributionInput;
use codex_features::FEATURES; use codex_features::FEATURES;
use codex_features::Feature; use codex_features::Feature;
use codex_features::unstable_features_warning_event; use codex_features::unstable_features_warning_event;
@@ -911,6 +913,25 @@ async fn thread_title_from_thread_store(
(!title.is_empty() && thread.preview.trim() != title).then(|| title.to_string()) (!title.is_empty() && thread.preview.trim() != title).then(|| title.to_string())
} }
fn push_prompt_fragment(
fragment: PromptFragment,
developer_sections: &mut Vec<String>,
contextual_user_sections: &mut Vec<String>,
separate_developer_sections: &mut Vec<String>,
) {
match fragment.slot() {
PromptSlot::DeveloperPolicy | PromptSlot::DeveloperCapabilities => {
developer_sections.push(fragment.text().to_string());
}
PromptSlot::ContextualUser => {
contextual_user_sections.push(fragment.text().to_string());
}
PromptSlot::SeparateDeveloper => {
separate_developer_sections.push(fragment.text().to_string());
}
}
}
impl Session { impl Session {
pub(crate) async fn app_server_client_metadata(&self) -> AppServerClientMetadata { pub(crate) async fn app_server_client_metadata(&self) -> AppServerClientMetadata {
let state = self.state.lock().await; let state = self.state.lock().await;
@@ -2860,6 +2881,57 @@ impl Session {
} }
} }
async fn build_turn_context_contribution_items(
&self,
turn_context: &TurnContext,
) -> Vec<ResponseItem> {
let mut developer_sections = Vec::new();
let mut contextual_user_sections = Vec::new();
let mut separate_developer_sections = Vec::new();
let context_contributors = self.services.extensions.context_contributors().to_vec();
for contributor in &context_contributors {
for fragment in contributor
.contribute_turn_context(TurnContextContributionInput {
thread_id: self.thread_id(),
turn_id: turn_context.sub_id.as_str(),
session_store: &self.services.session_extension_data,
thread_store: &self.services.thread_extension_data,
turn_store: turn_context.extension_data.as_ref(),
model_context_window: turn_context.model_context_window(),
})
.await
{
push_prompt_fragment(
fragment,
&mut developer_sections,
&mut contextual_user_sections,
&mut separate_developer_sections,
);
}
}
let mut items = Vec::with_capacity(3);
if let Some(developer_message) =
crate::context_manager::updates::build_developer_update_item(developer_sections)
{
items.push(developer_message);
}
for section in separate_developer_sections {
if let Some(developer_message) =
crate::context_manager::updates::build_developer_update_item(vec![section])
{
items.push(developer_message);
}
}
if let Some(contextual_user_message) =
crate::context_manager::updates::build_contextual_user_message(contextual_user_sections)
{
items.push(contextual_user_message);
}
items
}
pub(crate) async fn build_initial_context( pub(crate) async fn build_initial_context(
&self, &self,
turn_context: &TurnContext, turn_context: &TurnContext,
@@ -3026,25 +3098,40 @@ impl Session {
developer_sections.push(plugin_instructions.render()); developer_sections.push(plugin_instructions.render());
} }
let context_contributors = self.services.extensions.context_contributors().to_vec(); let context_contributors = self.services.extensions.context_contributors().to_vec();
for contributor in context_contributors { for contributor in &context_contributors {
for fragment in contributor for fragment in contributor
.contribute( .contribute_thread_context(
&self.services.session_extension_data, &self.services.session_extension_data,
&self.services.thread_extension_data, &self.services.thread_extension_data,
) )
.await .await
{ {
match fragment.slot() { push_prompt_fragment(
PromptSlot::DeveloperPolicy | PromptSlot::DeveloperCapabilities => { fragment,
developer_sections.push(fragment.text().to_string()); &mut developer_sections,
} &mut contextual_user_sections,
PromptSlot::ContextualUser => { &mut separate_developer_sections,
contextual_user_sections.push(fragment.text().to_string()); );
} }
PromptSlot::SeparateDeveloper => { }
separate_developer_sections.push(fragment.text().to_string()); for contributor in &context_contributors {
} for fragment in contributor
} .contribute_turn_context(TurnContextContributionInput {
thread_id: self.thread_id(),
turn_id: turn_context.sub_id.as_str(),
session_store: &self.services.session_extension_data,
thread_store: &self.services.thread_extension_data,
turn_store: turn_context.extension_data.as_ref(),
model_context_window: turn_context.model_context_window(),
})
.await
{
push_prompt_fragment(
fragment,
&mut developer_sections,
&mut contextual_user_sections,
&mut separate_developer_sections,
);
} }
} }
if let Some(user_instructions) = turn_context.user_instructions.as_deref() { if let Some(user_instructions) = turn_context.user_instructions.as_deref() {
@@ -3211,15 +3298,25 @@ impl Session {
let state = self.state.lock().await; let state = self.state.lock().await;
state.reference_context_item() state.reference_context_item()
}; };
let turn_context_item = turn_context.to_turn_context_item();
if reference_context_item.as_ref() == Some(&turn_context_item) {
return;
}
let should_inject_full_context = reference_context_item.is_none(); let should_inject_full_context = reference_context_item.is_none();
let context_items = if should_inject_full_context { let mut context_items = if should_inject_full_context {
self.build_initial_context(turn_context).await self.build_initial_context(turn_context).await
} else { } else {
// Steady-state path: append only context diffs to minimize token overhead. // Steady-state path: append only built-in context diffs here; turn-scoped extension
// context is added below.
self.build_settings_update_items(reference_context_item.as_ref(), turn_context) self.build_settings_update_items(reference_context_item.as_ref(), turn_context)
.await .await
}; };
let turn_context_item = turn_context.to_turn_context_item(); if !should_inject_full_context {
context_items.extend(
self.build_turn_context_contribution_items(turn_context)
.await,
);
}
if !context_items.is_empty() { if !context_items.is_empty() {
self.record_conversation_items(turn_context, &context_items) self.record_conversation_items(turn_context, &context_items)
.await; .await;
+91 -1
View File
@@ -7540,9 +7540,13 @@ async fn make_multi_agent_v2_usage_hint_test_session(
struct PromptExtensionTestContributor; struct PromptExtensionTestContributor;
struct PromptExtensionTestState; struct PromptExtensionTestState;
struct TurnContextExtensionTestContributor;
struct TurnContextExtensionTestState {
expected_model_context_window: Option<i64>,
}
impl codex_extension_api::ContextContributor for PromptExtensionTestContributor { impl codex_extension_api::ContextContributor for PromptExtensionTestContributor {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
_session_store: &'a codex_extension_api::ExtensionData, _session_store: &'a codex_extension_api::ExtensionData,
thread_store: &'a codex_extension_api::ExtensionData, thread_store: &'a codex_extension_api::ExtensionData,
@@ -7571,6 +7575,31 @@ fn prompt_extension_test_registry()
Arc::new(builder.build()) Arc::new(builder.build())
} }
impl codex_extension_api::ContextContributor for TurnContextExtensionTestContributor {
fn contribute_turn_context<'a>(
&'a self,
input: codex_extension_api::TurnContextContributionInput<'a>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Vec<codex_extension_api::PromptFragment>> + Send + 'a>,
> {
Box::pin(async move {
let Some(state) = input.turn_store.get::<TurnContextExtensionTestState>() else {
return Vec::new();
};
(input.model_context_window == state.expected_model_context_window
&& input.model_context_window.is_some()
&& !input.turn_id.is_empty())
.then(|| {
codex_extension_api::PromptFragment::developer_policy(
"turn context extension enabled",
)
})
.into_iter()
.collect()
})
}
}
#[tokio::test] #[tokio::test]
async fn build_initial_context_includes_prompt_fragments_from_extensions() { async fn build_initial_context_includes_prompt_fragments_from_extensions() {
let (mut session, turn_context) = make_session_and_context().await; let (mut session, turn_context) = make_session_and_context().await;
@@ -7592,6 +7621,67 @@ async fn build_initial_context_includes_prompt_fragments_from_extensions() {
); );
} }
#[tokio::test]
async fn build_initial_context_includes_turn_context_fragments_from_extensions() {
let (mut session, mut turn_context) = make_session_and_context().await;
let mut builder = codex_extension_api::ExtensionRegistryBuilder::new();
builder.prompt_contributor(Arc::new(TurnContextExtensionTestContributor));
session.services.extensions = Arc::new(builder.build());
turn_context.model_info.context_window = Some(100);
turn_context.model_info.effective_context_window_percent = 50;
turn_context
.extension_data
.insert(TurnContextExtensionTestState {
expected_model_context_window: Some(50),
});
let initial_context = session.build_initial_context(&turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
developer_messages
.iter()
.flatten()
.any(|text| *text == "turn context extension enabled"),
"expected turn context extension developer text, got {developer_messages:?}"
);
}
#[tokio::test]
async fn record_context_updates_includes_turn_context_fragments_on_steady_state_turns() {
let (mut session, mut turn_context) = make_session_and_context().await;
let mut builder = codex_extension_api::ExtensionRegistryBuilder::new();
builder.prompt_contributor(Arc::new(TurnContextExtensionTestContributor));
session.services.extensions = Arc::new(builder.build());
turn_context.model_info.context_window = Some(200);
turn_context.model_info.effective_context_window_percent = 25;
turn_context
.extension_data
.insert(TurnContextExtensionTestState {
expected_model_context_window: Some(50),
});
let mut previous_context_item = turn_context.to_turn_context_item();
previous_context_item.turn_id = Some("previous-turn-id".to_string());
{
let mut state = session.state.lock().await;
state.set_reference_context_item(Some(previous_context_item));
}
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.await;
let history = session.clone_history().await;
let developer_messages = developer_message_texts(history.raw_items());
assert!(
developer_messages
.iter()
.flatten()
.any(|text| *text == "turn context extension enabled"),
"expected steady-state turn context extension developer text, got {developer_messages:?}"
);
}
#[tokio::test] #[tokio::test]
async fn build_initial_context_omits_prompt_fragments_without_extension_state() { async fn build_initial_context_omits_prompt_fragments_without_extension_state() {
let (mut session, turn_context) = make_session_and_context().await; let (mut session, turn_context) = make_session_and_context().await;
@@ -74,7 +74,11 @@ async fn contribute_prompt(
) -> Vec<codex_extension_api::PromptFragment> { ) -> Vec<codex_extension_api::PromptFragment> {
let mut fragments = Vec::new(); let mut fragments = Vec::new();
for contributor in registry.context_contributors() { for contributor in registry.context_contributors() {
fragments.extend(contributor.contribute(session_store, thread_store).await); fragments.extend(
contributor
.contribute_thread_context(session_store, thread_store)
.await,
);
} }
fragments fragments
} }
@@ -17,7 +17,7 @@ pub fn install(registry: &mut ExtensionRegistryBuilder<()>) {
struct StyleContributor; struct StyleContributor;
impl ContextContributor for StyleContributor { impl ContextContributor for StyleContributor {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
session_store: &'a ExtensionData, session_store: &'a ExtensionData,
thread_store: &'a ExtensionData, thread_store: &'a ExtensionData,
@@ -37,7 +37,7 @@ impl ContextContributor for StyleContributor {
struct UsageContributor; struct UsageContributor;
impl ContextContributor for UsageContributor { impl ContextContributor for UsageContributor {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
session_store: &'a ExtensionData, session_store: &'a ExtensionData,
thread_store: &'a ExtensionData, thread_store: &'a ExtensionData,
+26 -2
View File
@@ -11,6 +11,7 @@ use codex_tools::ToolExecutor;
use crate::ExtensionData; use crate::ExtensionData;
mod context;
mod mcp; mod mcp;
mod prompt; mod prompt;
mod thread_lifecycle; mod thread_lifecycle;
@@ -18,6 +19,7 @@ mod tool_lifecycle;
mod turn_input; mod turn_input;
mod turn_lifecycle; mod turn_lifecycle;
pub use context::TurnContextContributionInput;
pub use mcp::McpServerContribution; pub use mcp::McpServerContribution;
pub use mcp::McpServerContributionContext; pub use mcp::McpServerContributionContext;
pub use prompt::PromptFragment; pub use prompt::PromptFragment;
@@ -62,12 +64,34 @@ pub trait McpServerContributor<C: Sync>: Send + Sync {
} }
/// Extension contribution that adds prompt fragments during prompt assembly. /// Extension contribution that adds prompt fragments during prompt assembly.
///
/// Implementations should use the method matching the scope needed by the
/// fragment: thread/session context for stable inputs, and turn context for
/// fragments that depend on turn-local host state.
pub trait ContextContributor: Send + Sync { pub trait ContextContributor: Send + Sync {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
session_store: &'a ExtensionData, session_store: &'a ExtensionData,
thread_store: &'a ExtensionData, thread_store: &'a ExtensionData,
) -> ExtensionFuture<'a, Vec<PromptFragment>>; ) -> ExtensionFuture<'a, Vec<PromptFragment>> {
Box::pin(async move {
let _self = self;
let _session_store = session_store;
let _thread_store = thread_store;
Vec::new()
})
}
fn contribute_turn_context<'a>(
&'a self,
input: TurnContextContributionInput<'a>,
) -> ExtensionFuture<'a, Vec<PromptFragment>> {
Box::pin(async move {
let _self = self;
let _input = input;
Vec::new()
})
}
} }
/// Contributor for host-owned thread lifecycle gates. /// Contributor for host-owned thread lifecycle gates.
@@ -0,0 +1,20 @@
use codex_protocol::ThreadId;
use crate::ExtensionData;
/// Host context available while extensions contribute turn-scoped context fragments.
#[derive(Clone, Copy)]
pub struct TurnContextContributionInput<'a> {
/// Stable host-owned thread identifier.
pub thread_id: ThreadId,
/// Stable host-owned turn identifier.
pub turn_id: &'a str,
/// Store scoped to the host session runtime.
pub session_store: &'a ExtensionData,
/// Store scoped to this thread runtime.
pub thread_store: &'a ExtensionData,
/// Store scoped to this turn.
pub turn_store: &'a ExtensionData,
/// Effective model context window for this turn, when known.
pub model_context_window: Option<i64>,
}
+1
View File
@@ -54,6 +54,7 @@ pub use contributors::ToolLifecycleContributor;
pub use contributors::ToolLifecycleFuture; pub use contributors::ToolLifecycleFuture;
pub use contributors::ToolStartInput; pub use contributors::ToolStartInput;
pub use contributors::TurnAbortInput; pub use contributors::TurnAbortInput;
pub use contributors::TurnContextContributionInput;
pub use contributors::TurnErrorInput; pub use contributors::TurnErrorInput;
pub use contributors::TurnInputContext; pub use contributors::TurnInputContext;
pub use contributors::TurnInputContributor; pub use contributors::TurnInputContributor;
+41 -3
View File
@@ -12,12 +12,14 @@ use codex_extension_api::ExtensionEventSink;
use codex_extension_api::ExtensionFuture; use codex_extension_api::ExtensionFuture;
use codex_extension_api::ExtensionRegistryBuilder; use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::PromptFragment; use codex_extension_api::PromptFragment;
use codex_extension_api::PromptSlot;
use codex_extension_api::ThreadLifecycleContributor; use codex_extension_api::ThreadLifecycleContributor;
use codex_extension_api::TokenUsageContributor; use codex_extension_api::TokenUsageContributor;
use codex_extension_api::ToolCall; use codex_extension_api::ToolCall;
use codex_extension_api::ToolContributor; use codex_extension_api::ToolContributor;
use codex_extension_api::ToolExecutor; use codex_extension_api::ToolExecutor;
use codex_extension_api::ToolLifecycleContributor; use codex_extension_api::ToolLifecycleContributor;
use codex_extension_api::TurnContextContributionInput;
use codex_extension_api::TurnInputContext; use codex_extension_api::TurnInputContext;
use codex_extension_api::TurnInputContributor; use codex_extension_api::TurnInputContributor;
use codex_extension_api::TurnItemContributor; use codex_extension_api::TurnItemContributor;
@@ -34,7 +36,7 @@ use pretty_assertions::assert_eq;
struct AllContributors; struct AllContributors;
impl ContextContributor for AllContributors { impl ContextContributor for AllContributors {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
_session_store: &'a ExtensionData, _session_store: &'a ExtensionData,
_thread_store: &'a ExtensionData, _thread_store: &'a ExtensionData,
@@ -147,7 +149,7 @@ async fn build_round_trips_every_contributor_category() {
struct NamedContextContributor(&'static str); struct NamedContextContributor(&'static str);
impl ContextContributor for NamedContextContributor { impl ContextContributor for NamedContextContributor {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
_session_store: &'a ExtensionData, _session_store: &'a ExtensionData,
_thread_store: &'a ExtensionData, _thread_store: &'a ExtensionData,
@@ -158,6 +160,20 @@ impl ContextContributor for NamedContextContributor {
} }
} }
struct NamedTurnContextContributor(&'static str);
impl ContextContributor for NamedTurnContextContributor {
fn contribute_turn_context<'a>(
&'a self,
_input: TurnContextContributionInput<'a>,
) -> ExtensionFuture<'a, Vec<PromptFragment>> {
Box::pin(std::future::ready(vec![PromptFragment::new(
PromptSlot::ContextualUser,
self.0,
)]))
}
}
struct RecordingTurnItemContributor { struct RecordingTurnItemContributor {
name: &'static str, name: &'static str,
calls: Arc<Mutex<Vec<&'static str>>>, calls: Arc<Mutex<Vec<&'static str>>>,
@@ -186,6 +202,8 @@ async fn contributors_preserve_registration_order() {
let mut builder = ExtensionRegistryBuilder::<()>::new(); let mut builder = ExtensionRegistryBuilder::<()>::new();
builder.prompt_contributor(Arc::new(NamedContextContributor("first"))); builder.prompt_contributor(Arc::new(NamedContextContributor("first")));
builder.prompt_contributor(Arc::new(NamedContextContributor("second"))); builder.prompt_contributor(Arc::new(NamedContextContributor("second")));
builder.prompt_contributor(Arc::new(NamedTurnContextContributor("turn-first")));
builder.prompt_contributor(Arc::new(NamedTurnContextContributor("turn-second")));
for name in ["first", "second"] { for name in ["first", "second"] {
builder.turn_item_contributor(Arc::new(RecordingTurnItemContributor { builder.turn_item_contributor(Arc::new(RecordingTurnItemContributor {
name, name,
@@ -199,7 +217,25 @@ async fn contributors_preserve_registration_order() {
let mut fragments = Vec::new(); let mut fragments = Vec::new();
for contributor in registry.context_contributors() { for contributor in registry.context_contributors() {
fragments.extend(contributor.contribute(&session_store, &thread_store).await); fragments.extend(
contributor
.contribute_thread_context(&session_store, &thread_store)
.await,
);
}
for contributor in registry.context_contributors() {
fragments.extend(
contributor
.contribute_turn_context(TurnContextContributionInput {
thread_id: codex_protocol::ThreadId::default(),
turn_id: turn_store.level_id(),
session_store: &session_store,
thread_store: &thread_store,
turn_store: &turn_store,
model_context_window: Some(123),
})
.await,
);
} }
let mut item = TurnItem::HookPrompt(HookPromptItem { let mut item = TurnItem::HookPrompt(HookPromptItem {
id: "item".to_string(), id: "item".to_string(),
@@ -217,6 +253,8 @@ async fn contributors_preserve_registration_order() {
vec![ vec![
PromptFragment::developer_policy("first"), PromptFragment::developer_policy("first"),
PromptFragment::developer_policy("second"), PromptFragment::developer_policy("second"),
PromptFragment::new(PromptSlot::ContextualUser, "turn-first"),
PromptFragment::new(PromptSlot::ContextualUser, "turn-second"),
] ]
); );
assert_eq!( assert_eq!(
+1 -1
View File
@@ -48,7 +48,7 @@ impl MemoriesExtensionConfig {
} }
impl ContextContributor for MemoriesExtension { impl ContextContributor for MemoriesExtension {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
_session_store: &'a ExtensionData, _session_store: &'a ExtensionData,
thread_store: &'a ExtensionData, thread_store: &'a ExtensionData,
+1 -1
View File
@@ -181,7 +181,7 @@ async fn prompt_contribution_uses_memory_summary_when_enabled() {
}); });
let fragments = extension let fragments = extension
.contribute(&ExtensionData::new("session"), &thread_store) .contribute_thread_context(&ExtensionData::new("session"), &thread_store)
.await; .await;
assert_eq!(fragments.len(), 1); assert_eq!(fragments.len(), 1);
+1 -1
View File
@@ -103,7 +103,7 @@ impl<C> ContextContributor for SkillsExtension<C>
where where
C: Send + Sync + 'static, C: Send + Sync + 'static,
{ {
fn contribute<'a>( fn contribute_thread_context<'a>(
&'a self, &'a self,
session_store: &'a ExtensionData, session_store: &'a ExtensionData,
thread_store: &'a ExtensionData, thread_store: &'a ExtensionData,
@@ -183,7 +183,7 @@ async fn selected_executor_catalog_is_context_and_selected_entrypoint_is_turn_in
.await; .await;
let prompt_fragments = registry.context_contributors()[0] let prompt_fragments = registry.context_contributors()[0]
.contribute(&session_store, &thread_store) .contribute_thread_context(&session_store, &thread_store)
.await; .await;
assert_eq!(1, prompt_fragments.len()); assert_eq!(1, prompt_fragments.len());
assert!( assert!(
@@ -228,7 +228,7 @@ async fn selected_executor_catalog_is_context_and_selected_entrypoint_is_turn_in
read_request_keys(&read_requests) read_request_keys(&read_requests)
); );
let rebuilt_prompt_fragments = registry.context_contributors()[0] let rebuilt_prompt_fragments = registry.context_contributors()[0]
.contribute(&session_store, &thread_store) .contribute_thread_context(&session_store, &thread_store)
.await; .await;
assert_eq!(1, rebuilt_prompt_fragments.len()); assert_eq!(1, rebuilt_prompt_fragments.len());
assert!(rebuilt_prompt_fragments[0].text().contains("lint-fix")); assert!(rebuilt_prompt_fragments[0].text().contains("lint-fix"));
@@ -294,7 +294,7 @@ async fn orchestrator_catalog_snapshot_caches_failure() -> TestResult {
.await; .await;
let initial_fragments = registry.context_contributors()[0] let initial_fragments = registry.context_contributors()[0]
.contribute(&session_store, &thread_store) .contribute_thread_context(&session_store, &thread_store)
.await; .await;
assert!(initial_fragments.is_empty()); assert!(initial_fragments.is_empty());
let EventMsg::Warning(warning) = event_rx.try_recv()?.msg else { let EventMsg::Warning(warning) = event_rx.try_recv()?.msg else {
+1 -1
View File
@@ -2992,7 +2992,7 @@ pub struct TurnContextNetworkItem {
/// context updates, and again after mid-turn compaction when replacement /// context updates, and again after mid-turn compaction when replacement
/// history re-establishes full context, so resume/fork replay can recover the /// history re-establishes full context, so resume/fork replay can recover the
/// latest durable baseline. /// latest durable baseline.
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
pub struct TurnContextItem { pub struct TurnContextItem {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<String>, pub turn_id: Option<String>,