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")]
CompactionWarning { pressure: f32, tier: u8 },
#[serde(rename = "context_pressure")]
ContextPressure { pressure: f32, tokens: usize },
ContextPressure {
pressure: f32,
tokens_used: usize,
context_limit: usize,
},
#[serde(rename = "inference_strain")]
InferenceStrain {
attempt: u32,
@ -482,7 +486,15 @@ impl From<crate::backend::BackendEvent> for StreamEvent {
pressure,
},
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 {
attempt,
status,
@ -578,9 +590,15 @@ impl From<StreamEvent> for crate::backend::BackendEvent {
StreamEvent::CompactionWarning { pressure, tier } => {
BE::CompactionWarning { pressure, tier }
}
StreamEvent::ContextPressure { pressure, tokens } => {
BE::ContextPressure(pressure, tokens)
}
StreamEvent::ContextPressure {
pressure,
tokens_used,
context_limit,
} => BE::ContextPressure {
pressure,
tokens_used,
context_limit,
},
StreamEvent::InferenceStrain {
attempt,
status,
@ -797,4 +815,43 @@ mod tests {
assert!(!typed.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).
/// Fires every round so the TUI ctx counter reflects live state
/// 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.
/// Correlates to health over time.
InferenceStrain {

View file

@ -28,13 +28,13 @@ fn bifrost_pressure(
counter: &TokenCounter,
messages: &[BifrostMessage],
context_limit: usize,
) -> f32 {
) -> (usize, f32) {
let tokens: usize = messages
.iter()
.map(|m| counter.count(&m.content.as_text()))
.sum();
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,
@ -364,7 +364,7 @@ pub(crate) async fn run_turn(
last_pulse = Instant::now();
}
let pressure = bifrost_pressure(&counter, &messages, context_limit);
let (tokens_used, pressure) = bifrost_pressure(&counter, &messages, context_limit);
tracing::info!(
turn_round = tool_round,
agent = %agent_id,
@ -375,7 +375,11 @@ pub(crate) async fn run_turn(
);
let max_tokens = pressure_to_max_tokens(pressure, output_limit);
let _ = tx
.send(Ok(BackendEvent::ContextPressure(pressure, context_limit)))
.send(Ok(BackendEvent::ContextPressure {
pressure,
tokens_used,
context_limit,
}))
.await;
if max_rounds == 0 {

View file

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

View file

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