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:
parent
5cc7a90f44
commit
f266136510
5 changed files with 101 additions and 16 deletions
|
|
@ -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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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, ..
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue