Stabilize skill catalogs across executor availability changes (#48353)

## Why

Changes in executor availability can alter cloud skill catalog rendering under a shared budget and repeat an unchanged executor catalog when it reconnects.

## What changed

- Cap cloud skills at three quarters of the metadata budget initially, leaving the remaining budget and unused cloud allowance for filesystem skills. Reduce the cloud cap only when doing so allows all skill entries to fit; description truncation alone does not trigger rebalancing.
- Persist the allocation across disconnects, compaction, and thread resume. Recompute it when the cloud inventory or total budget changes.
- Emit a short availability update when an unchanged selected-environment catalog returns and its full text remains in history. Reinject the full catalog when that text is missing.

## Testing

Add coverage for allocation stability, catalog and budget changes, omission handling, and full catalog reinjection after compaction and resume. Extend the selected capability stack test to verify reconnection avoids repeating the catalog.

GitOrigin-RevId: 839fe98024e00c4082a46e6a40675e15fd2c16c6
This commit is contained in:
vkg-oai
2026-09-26 04:19:46 +00:00
committed by copyberry
parent ca41ed337f
commit e72da2b538
17 changed files with 1173 additions and 233 deletions
@@ -400,6 +400,7 @@ impl WorldState {
}
/// Renders each section against the exact persisted snapshot when available.
#[cfg(test)]
pub(crate) fn render_diff(
&self,
previous: &WorldStateSnapshot,
+2 -1
View File
@@ -107,7 +107,8 @@ pub(crate) struct ContextManager {
/// also clear this when it trims a mixed initial-context developer bundle
/// whose non-diff fragments no longer exist in the surviving history.
reference_context_item: Option<TurnContextItem>,
/// World state most recently appended to model-visible history.
/// World-state comparison checkpoint. After compaction this may contain only
/// extension metadata, with model-visible context still awaiting reinjection.
world_state_baseline: Option<WorldStateSnapshot>,
}
+21 -4
View File
@@ -31,6 +31,7 @@ use crate::context::MultiAgentRoleInstructions;
use crate::context::NetworkRuleSaved;
use crate::context::RecommendedPluginsInstructions;
use crate::context::world_state::WorldState;
use crate::context::world_state::WorldStateSnapshot;
use crate::current_time::TimeProvider;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::exec_policy::BANNED_PREFIX_SUGGESTIONS;
@@ -3689,8 +3690,12 @@ impl Session {
let world_state_item = world_state_snapshot
.merge_patch_from(&previous_snapshot)
.map(WorldStateItem::patch);
// A catalog may have left history during compaction even when its snapshot survives.
let items = crate::context_manager::updates::merge_contextual_fragments(
world_state.render_diff(&previous_snapshot),
world_state.render_history_diff(
Some(&previous_snapshot),
self.state.lock().await.history.raw_items(),
),
);
if !items.is_empty() {
self.record_conversation_items(turn_context, &step_context.settings.model_info, &items)
@@ -4098,10 +4103,23 @@ impl Session {
// Wait for accepted updates to finish persisting, then keep later updates from
// overtaking the current settings snapshot while its checkpoint is written.
let _settings_guard = thread_settings::acquire_persistence_lock(self).await;
// Compaction starts a new history window, so its WorldState baseline must be full.
// A new history window needs a full checkpoint, even when it contains only
// extension metadata and model-visible context will be rebuilt on the next turn.
let mut world_state_item = None;
let compacted_item = {
let mut state = self.state.lock().await;
let snapshot = world_state_baseline
.map(|world_state| world_state.snapshot())
.or_else(|| {
let previous = state.history.world_state_checkpoint()?;
let mut retained = serde_json::Map::new();
for contributor in self.services.extensions.context_contributors() {
retained.extend(
contributor.retain_world_state_after_compaction(&previous.state),
);
}
(!retained.is_empty()).then(|| WorldStateSnapshot::from(&retained))
});
state.replace_annotated_history(
items,
reference_context_item.clone(),
@@ -4110,8 +4128,7 @@ impl Session {
},
);
state.reasoning_effort_pin = ReasoningEffortPin::Compacted;
if let Some(world_state) = world_state_baseline {
let snapshot = world_state.snapshot();
if let Some(snapshot) = snapshot {
world_state_item = Some(WorldStateItem::full(snapshot.clone().into_object()));
state.history.set_world_state_baseline(snapshot);
}
+2
View File
@@ -290,6 +290,7 @@ impl Session {
.iter()
.map(|root| root.selected_root().clone())
.collect::<Vec<_>>();
let previous_world_state = self.state.lock().await.history.world_state_checkpoint();
for contributor in self.services.extensions.context_contributors() {
for section in contributor
.contribute_world_state(WorldStateContributionInput {
@@ -305,6 +306,7 @@ impl Session {
session_store: &self.services.session_extension_data,
thread_store: &self.services.thread_extension_data,
turn_store: turn_context.extension_data.as_ref(),
previous_world_state: previous_world_state.as_ref().map(|state| &state.state),
})
.await
{