Watch
1
0
Fork
You've already forked souveraine
0

context pressure carries usage and ceiling as named fields

BackendEvent::ContextPressure was (f32, usize) whose second element was the
context limit. A positional tuple crossing a module boundary made every
consumer guess: the TUI guessed limit and was right, the HTTP layer named it
tokens and was wrong. So every non-TUI surface rendered the ceiling as the
usage — a constant 250000 that looked like a measurement. Nothing failed and
nothing logged.

Name the fields, and carry tokens_used explicitly rather than leaving it to
be reconstructed as pressure x limit. bifrost_pressure already computed the
token count and discarded it.
This commit is contained in:
Souveraine 2026-08-12 12:11:21 -04:00
commit f266136510
5 changed files with 101 additions and 16 deletions

View file

@ -378,7 +378,11 @@ pub enum StreamEvent {
#[serde(rename = "compaction_warning")] #[serde(rename = "compaction_warning")]
CompactionWarning { pressure: f32, tier: u8 }, CompactionWarning { pressure: f32, tier: u8 },
#[serde(rename = "context_pressure")] #[serde(rename = "context_pressure")]
ContextPressure { pressure: f32, tokens: usize }, ContextPressure {
pressure: f32,
tokens_used: usize,
context_limit: usize,
},
#[serde(rename = "inference_strain")] #[serde(rename = "inference_strain")]
InferenceStrain { InferenceStrain {
attempt: u32, attempt: u32,
@ -482,7 +486,15 @@ impl From<crate::backend::BackendEvent> for StreamEvent {
pressure, pressure,
}, },
BE::CompactionWarning { pressure, tier } => Self::CompactionWarning { pressure, tier }, BE::CompactionWarning { pressure, tier } => Self::CompactionWarning { pressure, tier },
BE::ContextPressure(pressure, tokens) => Self::ContextPressure { pressure, tokens }, BE::ContextPressure {
pressure,
tokens_used,
context_limit,
} => Self::ContextPressure {
pressure,
tokens_used,
context_limit,
},
BE::InferenceStrain { BE::InferenceStrain {
attempt, attempt,
status, status,
@ -578,9 +590,15 @@ impl From<StreamEvent> for crate::backend::BackendEvent {
StreamEvent::CompactionWarning { pressure, tier } => { StreamEvent::CompactionWarning { pressure, tier } => {
BE::CompactionWarning { pressure, tier } BE::CompactionWarning { pressure, tier }
} }
StreamEvent::ContextPressure { pressure, tokens } => { StreamEvent::ContextPressure {
BE::ContextPressure(pressure, tokens) pressure,
} tokens_used,
context_limit,
} => BE::ContextPressure {
pressure,
tokens_used,
context_limit,
},
StreamEvent::InferenceStrain { StreamEvent::InferenceStrain {
attempt, attempt,
status, status,
@ -797,4 +815,43 @@ mod tests {
assert!(!typed.content.has_images()); assert!(!typed.content.has_images());
assert!(carrying.content.has_images()); assert!(carrying.content.has_images());
} }
#[test]
fn context_pressure_keeps_usage_and_ceiling_distinct_on_the_wire() {
// Regression, 2026-08-12. `BackendEvent::ContextPressure` was a
// positional `(f32, usize)` whose second element was the context
// *limit*. This layer named that element `tokens`, so every HTTP
// surface rendered the ceiling as the usage — a constant 250000
// that looked exactly like a measurement. Assert the two numbers
// are carried separately and cannot be confused again.
let ev = StreamEvent::ContextPressure {
pressure: 0.37,
tokens_used: 92_500,
context_limit: 250_000,
};
let wire: serde_json::Value = serde_json::to_value(&ev).unwrap();
assert_eq!(wire["tokens_used"], 92_500);
assert_eq!(wire["context_limit"], 250_000);
// The old, ambiguous name must not reappear.
assert!(
wire.get("tokens").is_none(),
"`tokens` was the ambiguous name that caused the bug; \
a surface reading it must fail loudly, not read a ceiling"
);
// And it must survive the trip back into the backend enum.
let back: crate::backend::BackendEvent = ev.into();
match back {
crate::backend::BackendEvent::ContextPressure {
tokens_used,
context_limit,
..
} => {
assert_eq!(tokens_used, 92_500);
assert_eq!(context_limit, 250_000);
assert_ne!(tokens_used, context_limit);
}
other => panic!("expected ContextPressure, got {other:?}"),
}
}
} }

View file

@ -82,7 +82,19 @@ pub enum BackendEvent {
/// Continuous context pressure update (sub-threshold). /// Continuous context pressure update (sub-threshold).
/// Fires every round so the TUI ctx counter reflects live state /// Fires every round so the TUI ctx counter reflects live state
/// rather than only updating when a warning crosses a threshold. /// rather than only updating when a warning crosses a threshold.
ContextPressure(f32, usize), ///
/// Named fields, deliberately. This was `(f32, usize)` until 2026-08-12,
/// and a positional tuple crossing a module boundary made every consumer
/// guess what the second element meant. The TUI guessed `limit` (right);
/// the HTTP layer guessed `tokens` (wrong), so every non-TUI surface
/// rendered the context *ceiling* as if it were the usage — a constant
/// 250000 that looked like a measurement. Nothing failed and nothing
/// logged. Carry the names.
ContextPressure {
pressure: f32,
tokens_used: usize,
context_limit: usize,
},
/// Inference strain — the voice is hoarse, providers are slow. /// Inference strain — the voice is hoarse, providers are slow.
/// Correlates to health over time. /// Correlates to health over time.
InferenceStrain { InferenceStrain {

View file

@ -28,13 +28,13 @@ fn bifrost_pressure(
counter: &TokenCounter, counter: &TokenCounter,
messages: &[BifrostMessage], messages: &[BifrostMessage],
context_limit: usize, context_limit: usize,
) -> f32 { ) -> (usize, f32) {
let tokens: usize = messages let tokens: usize = messages
.iter() .iter()
.map(|m| counter.count(&m.content.as_text())) .map(|m| counter.count(&m.content.as_text()))
.sum(); .sum();
let limit = context_limit.max(1); let limit = context_limit.max(1);
(tokens as f32 / limit as f32).min(1.0) (tokens, (tokens as f32 / limit as f32).min(1.0))
} }
/// Helper: bump adaptive delay when we hit a 429. No decay — once bumped, /// Helper: bump adaptive delay when we hit a 429. No decay — once bumped,
@ -364,7 +364,7 @@ pub(crate) async fn run_turn(
last_pulse = Instant::now(); last_pulse = Instant::now();
} }
let pressure = bifrost_pressure(&counter, &messages, context_limit); let (tokens_used, pressure) = bifrost_pressure(&counter, &messages, context_limit);
tracing::info!( tracing::info!(
turn_round = tool_round, turn_round = tool_round,
agent = %agent_id, agent = %agent_id,
@ -375,7 +375,11 @@ pub(crate) async fn run_turn(
); );
let max_tokens = pressure_to_max_tokens(pressure, output_limit); let max_tokens = pressure_to_max_tokens(pressure, output_limit);
let _ = tx let _ = tx
.send(Ok(BackendEvent::ContextPressure(pressure, context_limit))) .send(Ok(BackendEvent::ContextPressure {
pressure,
tokens_used,
context_limit,
}))
.await; .await;
if max_rounds == 0 { if max_rounds == 0 {

View file

@ -397,8 +397,12 @@ impl App {
BackendEvent::CompactionWarning { pressure, tier } => { BackendEvent::CompactionWarning { pressure, tier } => {
self.dispatch(TuiEvent::CompactionWarning { pressure, tier }); self.dispatch(TuiEvent::CompactionWarning { pressure, tier });
} }
BackendEvent::ContextPressure(p, limit) => { BackendEvent::ContextPressure {
self.dispatch(TuiEvent::PressureChanged(p, limit)); pressure,
context_limit,
..
} => {
self.dispatch(TuiEvent::PressureChanged(pressure, context_limit));
} }
BackendEvent::InferenceStrain { BackendEvent::InferenceStrain {
attempt, status, .. attempt, status, ..

View file

@ -296,11 +296,19 @@ impl ChatState {
self.pending_consciousness self.pending_consciousness
.push(BackendEvent::CompactionWarning { pressure, tier }); .push(BackendEvent::CompactionWarning { pressure, tier });
} }
BackendEvent::ContextPressure(p, limit) => { BackendEvent::ContextPressure {
self.pressure = p; pressure,
self.context_limit = Some(limit); tokens_used,
context_limit,
} => {
self.pressure = pressure;
self.context_limit = Some(context_limit);
self.pending_consciousness self.pending_consciousness
.push(BackendEvent::ContextPressure(p, limit)); .push(BackendEvent::ContextPressure {
pressure,
tokens_used,
context_limit,
});
} }
BackendEvent::InferenceStrain { BackendEvent::InferenceStrain {
attempt, attempt,