fix(tui): retain historical context windows for resolved Auto turns [skip ci]

CW-7640 and #7133. Keep the plan and the completed turn on the same resolved wire model. Store the matched provider identity, model, token window and provenance. A session projection carries the Auto selector, so only admitted and completed routes establish the actual model. Show a restored receipt separately from the prospective budget. Older, unknown and zero historical windows remain readable.

The baseline discarded the last window on restart. On the frozen Ubuntu ARM64 CLI five fresh-process journeys passed for provider-wide and exact-model overrides and for absent, unknown and zero historical metadata. The saved 135791-token window stayed historical. The next real turn used 75319 and resume issued no model request. The existing round-trip assertion checked all 53 released provider representations on the preceding wire-identical serde implementation. Workspace rustfmt and version gates passed. The separately reported Windows 40% compaction trigger remains open.
This commit is contained in:
CodeWhale Bot committed 2026-11-01 01:26:48 -08:00
1 parent 42532e3395
commit de99f0530a
9 files changed
+119 -40

No files matched your search

+3 -3
View File
@@ -5,7 +5,7 @@ use codewhale_config::route::{
LimitField, LogicalModelRef, OverrideSource, ReadyRouteCandidate, RouteError, RouteLimits,
RouteRequest, RouteResolver, SourcedLimitOverride, WireModelId,
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::client::CodewhaleClient;
use crate::codex_model_cache::model_roster_for;
@@ -22,7 +22,7 @@ use codewhale_runtime::config::{
/// receipt separate from the numeric route limits so every consumer can state
/// whether the number is operator-configured, freshly provider-reported, a
/// Kimi Code safety floor, catalog data, or a conservative fallback.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextWindowSource {
Configured,
@@ -111,7 +111,7 @@ impl ContextWindowSource {
}
/// Context window carried alongside an exact runtime route.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextWindowResolution {
pub tokens: u32,
pub source: ContextWindowSource,
+19
View File
@@ -9,6 +9,7 @@
use crate::approval_log::{ApprovalReceipt, ApprovalReceiptStore, ApprovalReplay};
use crate::model_routing::AutoRouteReceipt;
use crate::project_context::find_git_root;
use crate::route_runtime::ContextWindowResolution;
use crate::tools::goal::{GoalPauseReason, GoalSnapshot};
use crate::tools::plan::PlanSnapshot;
use crate::tools::todo::TodoListSnapshot;
@@ -1032,6 +1033,7 @@ pub struct SavedAutoRouteReceipt {
/// routes where a concrete tier cannot be proven. Optional so older
/// sessions remain loadable.
pub effective_reasoning_effort: Option<ReasoningEffortTier>,
pub context_window: Option<ContextWindowResolution>,
}
#[derive(Serialize, Deserialize)]
@@ -1045,6 +1047,21 @@ struct SavedAutoRouteReceiptWire {
/// sessions remain loadable.
#[serde(default, skip_serializing_if = "Option::is_none")]
effective_reasoning_effort: Option<ReasoningEffortTier>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "read_saved_context_window"
)]
context_window: Option<ContextWindowResolution>,
}
fn read_saved_context_window<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<ContextWindowResolution>, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
Ok(serde_json::from_value::<ContextWindowResolution>(value)
.ok()
.filter(|window| window.tokens > 0))
}
impl Serialize for SavedAutoRouteReceipt {
@@ -1063,6 +1080,7 @@ impl Serialize for SavedAutoRouteReceipt {
model: self.model.clone(),
receipt: self.receipt.clone(),
effective_reasoning_effort: self.effective_reasoning_effort,
context_window: self.context_window,
}
.serialize(serializer)
}
@@ -1084,6 +1102,7 @@ impl<'de> Deserialize<'de> for SavedAutoRouteReceipt {
model: wire.model,
receipt: wire.receipt,
effective_reasoning_effort: wire.effective_reasoning_effort,
context_window: wire.context_window,
})
}
}
+3 -3
View File
@@ -187,7 +187,7 @@ pub async fn plan_turn_route(
// Without an Auto selection there is no per-request signal, so the
// route is the configured model — the same declared default the local
// fallback uses. Request wording is never inspected (#6290 rework).
let effective_model = if request.auto_model {
let selected_model = if request.auto_model {
auto_selection
.as_ref()
.map(|selection| selection.model.clone())
@@ -199,7 +199,7 @@ pub async fn plan_turn_route(
let turn_route = resolve_runtime_route_for_identity(
request.route_config,
selected_identity,
Some(&effective_model),
Some(&selected_model),
);
let turn_route = match turn_route {
@@ -302,10 +302,10 @@ pub async fn plan_turn_route(
};
Ok(PlannedTurnRoute {
effective_model: turn_route.model.clone(),
route: turn_route,
compaction: turn_compaction,
effective_provider,
effective_model,
effective_provider_identity,
effective_provider_label,
selected_reasoning_effort,
@@ -4522,6 +4522,7 @@ mod released_provider_serde {
effective_reasoning_effort: Some(ReasoningEffortTier::High),
};
let saved = codewhale_engine::session_manager::SavedAutoRouteReceipt {
context_window: None,
provider,
provider_identity: id.into(),
model: "model-A".into(),
+33 -14
View File
@@ -159,6 +159,16 @@ pub(crate) struct ContextTokenCache {
pub(crate) message_tokens: Vec<usize>,
}
#[derive(Debug, Clone)]
pub(crate) struct AutoRouteBudget {
pub provider: ProviderKind,
pub provider_identity: String,
pub model: String,
pub limits: Option<RouteLimits>,
pub window: codewhale_engine::route_runtime::ContextWindowResolution,
pub restored: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CompletedAssistantOutputReceipt {
history_index: usize,
@@ -1727,12 +1737,7 @@ pub struct App {
pub auto_model: bool,
/// Last concrete model chosen while `auto_model` is active.
pub last_effective_model: Option<String>,
pub(crate) last_effective_budget: Option<(
ProviderKind,
String,
Option<RouteLimits>,
codewhale_engine::route_runtime::ContextWindowResolution,
)>,
pub(crate) last_effective_budget: Option<AutoRouteBudget>,
/// Provider that actually served the latest auto-routed turn.
pub last_effective_provider: Option<ProviderKind>,
/// Exact non-secret identity for the provider that served the latest Auto
@@ -7301,6 +7306,7 @@ impl App {
model: model.clone(),
receipt: receipt.clone(),
effective_reasoning_effort: self.last_effective_reasoning_effort.map(Into::into),
context_window: self.matching_auto_budget().map(|budget| budget.window),
})
}
@@ -7392,10 +7398,9 @@ impl App {
pub fn effective_model_for_budget(&self) -> &str {
if self.auto_model {
return self
.last_effective_model
.as_deref()
.filter(|model| *model != "auto")
.unwrap_or(DEFAULT_TEXT_MODEL);
.matching_auto_budget()
.filter(|budget| !budget.restored)
.map_or(DEFAULT_TEXT_MODEL, |budget| budget.model.as_str());
}
&self.model
}
@@ -7408,6 +7413,16 @@ impl App {
)
}
pub(crate) fn matching_auto_budget(&self) -> Option<&AutoRouteBudget> {
self.last_effective_budget.as_ref().filter(|budget| {
self.auto_model
&& self.last_effective_provider == Some(budget.provider)
&& self.last_effective_provider_identity.as_deref()
== Some(&budget.provider_identity)
&& self.last_effective_model.as_deref() == Some(&budget.model)
})
}
pub(crate) fn effective_budget_route(
&self,
) -> (
@@ -7416,11 +7431,15 @@ impl App {
Option<RouteLimits>,
Option<codewhale_engine::route_runtime::ContextWindowResolution>,
) {
if self.auto_model
&& let Some((provider, model, limits, window)) = self.last_effective_budget.as_ref()
&& self.last_effective_model.as_deref() == Some(model.as_str())
if let Some(budget) = self.matching_auto_budget()
&& !budget.restored
{
return (*provider, model, *limits, Some(*window));
return (
budget.provider,
&budget.model,
budget.limits,
Some(budget.window),
);
}
(
self.api_provider,
+35 -7
View File
@@ -132,6 +132,21 @@ struct PromptTextLayer<'a> {
body: &'a str,
}
fn restored_window_label(app: &App, locale: Locale) -> Option<String> {
let budget = app
.matching_auto_budget()
.filter(|budget| budget.restored)?;
let route = tr(locale, MessageId::ModelPickerAutoLastRoute)
.replace("{provider}", &budget.provider_identity)
.replace("{model}", &budget.model);
Some(format!(
"{route} · {} {} ({})",
budget.window.tokens,
tr(locale, MessageId::CtxInspTokens),
budget.window.source.display_label()
))
}
#[must_use]
pub fn build_context_inspector_text(app: &App, locale: Locale) -> String {
let mut out = String::new();
@@ -146,6 +161,9 @@ pub fn build_context_inspector_text(app: &App, locale: Locale) -> String {
tr(locale, MessageId::CtxInspModel),
app.model
);
if let Some(previous) = restored_window_label(app, locale) {
let _ = writeln!(out, "{previous}");
}
let _ = writeln!(
out,
"{}: {}",
@@ -760,6 +778,7 @@ pub(crate) struct ContextInspectorView {
percent: f64,
model: String,
workspace: String,
restored_window: Option<String>,
auto_compact: bool,
threshold: f64,
rows: Vec<ContextBucket>,
@@ -777,6 +796,7 @@ impl ContextInspectorView {
percent: 0.0,
model: String::new(),
workspace: String::new(),
restored_window: None,
auto_compact: app.auto_compact,
threshold: 0.0,
rows: Vec::new(),
@@ -800,6 +820,7 @@ impl ContextInspectorView {
self.max = max;
self.percent = percent;
self.model = app.model_display_label();
self.restored_window = restored_window_label(app, app.ui_locale);
self.workspace = codewhale_util::utils::display_path(&app.workspace);
self.auto_compact = app.auto_compact;
self.threshold = if app.auto_compact && max > 0 {
@@ -972,7 +993,10 @@ impl ModalView for ContextInspectorView {
crate::tui::workspace_context::truncate_left(&self.workspace, width),
Style::default().fg(palette::TEXT_DIM),
)),
Line::from(""),
Line::from(Span::styled(
self.restored_window.as_deref().unwrap_or_default(),
Style::default().fg(palette::TEXT_MUTED),
)),
];
if content.height >= 11 && content.width >= 24 {
@@ -1300,15 +1324,19 @@ mod tests {
app.model = "auto".to_string();
app.auto_model = true;
app.last_effective_model = Some("deepseek-v4-pro".to_string());
app.last_effective_budget = Some((
codewhale_config::ProviderKind::Deepseek,
"deepseek-v4-pro".to_string(),
None,
codewhale_engine::route_runtime::ContextWindowResolution {
app.last_effective_provider = Some(codewhale_config::ProviderKind::Deepseek);
app.last_effective_provider_identity = Some("deepseek".into());
app.last_effective_budget = Some(crate::tui::app::AutoRouteBudget {
provider: codewhale_config::ProviderKind::Deepseek,
provider_identity: "deepseek".into(),
model: "deepseek-v4-pro".into(),
limits: None,
window: codewhale_engine::route_runtime::ContextWindowResolution {
tokens: 1_000_000,
source: codewhale_engine::route_runtime::ContextWindowSource::Catalog,
},
));
restored: false,
});
let text = build_context_inspector_text(&app, Locale::En);
assert!(text.contains("Model: auto"), "{text}");
+11
View File
@@ -4609,6 +4609,17 @@ pub(crate) fn apply_loaded_session_with_goal(
app.last_effective_provider_identity = Some(saved.provider_identity.clone());
app.last_effective_model = Some(saved.model.clone());
app.last_auto_route_receipt = Some(saved.receipt.clone());
app.last_effective_budget =
saved
.context_window
.map(|window| crate::tui::app::AutoRouteBudget {
provider: saved.provider,
provider_identity: saved.provider_identity.clone(),
model: saved.model.clone(),
limits: None,
window,
restored: true,
});
app.last_effective_reasoning_effort = saved.effective_reasoning_effort.map(Into::into);
}
resolve_loaded_session_route(app, config);
+13 -10
View File
@@ -1117,12 +1117,19 @@ pub(crate) fn build_dispatch_success_closure(
if prepare.auto_model {
app.last_effective_model = Some(outcome.effective_model.clone());
app.last_effective_budget = Some((
outcome.effective_provider,
outcome.effective_model.clone(),
codewhale_engine::route_budget::known_route_limits(outcome.route_limits),
outcome.context_window,
));
app.last_effective_provider = Some(outcome.effective_provider);
app.last_effective_provider_identity =
Some(outcome.effective_provider_identity.clone());
app.last_effective_budget = Some(crate::tui::app::AutoRouteBudget {
provider: outcome.effective_provider,
provider_identity: outcome.effective_provider_identity.clone(),
model: outcome.effective_model.clone(),
limits: codewhale_engine::route_budget::known_route_limits(
outcome.route_limits,
),
window: outcome.context_window,
restored: false,
});
} else {
app.set_active_route_limits(outcome.route_limits);
app.active_context_window = Some(outcome.context_window);
@@ -1146,10 +1153,6 @@ pub(crate) fn build_dispatch_success_closure(
app.last_effective_reasoning_effort = Some(outcome.effective_reasoning_effort);
if prepare.auto_model {
app.last_effective_model = Some(outcome.effective_model.clone());
app.last_effective_provider = Some(outcome.effective_provider);
app.last_effective_provider_identity =
Some(outcome.effective_provider_identity.clone());
if let Some(selection) = outcome.auto_selection.as_ref() {
app.last_auto_route_receipt = selection.receipt.clone();
let status = app
+1 -3
View File
@@ -177,9 +177,7 @@ pub(super) fn apply_engine_session_projection(
crate::tui::ui::dispatch::settle_accepted_steers(app);
app.system_prompt = system_prompt;
app.instruction_assembly = instruction_assembly;
if app.auto_model {
app.last_effective_model = Some(model);
} else {
if !app.auto_model {
let accepted_route =
app.pending_turn_route
.as_ref()