use serde_json::Value;
use std::future::Future;
use std::pin::Pin;
use std::sync::OnceLock;
use crate::prune::{prune, PruneConfig};
use super::trim::{estimate_tokens, estimate_value_tokens, repair_orphaned_tool_calls};
use crate::tokens::TokenEstimation;
pub type SummarizeFuture = Pin<Box<dyn Future<Output = anyhow::Result<String>> + Send>>;
pub type SummarizeFn = dyn Fn(String) -> SummarizeFuture + Send + Sync;
pub type Summarizer = Box<SummarizeFn>;
pub const SUMMARY_PREFIX: &str = "[CONTEXT COMPACTION — REFERENCE ONLY]";
pub const SUMMARY_END_MARKER: &str = "--- END OF CONTEXT SUMMARY ---";
pub(crate) fn is_compaction_message(m: &Value) -> bool {
m["content"]
.as_str()
.is_some_and(|c| c.starts_with(SUMMARY_PREFIX))
}
pub(crate) fn is_compaction_text(content: &str) -> bool {
content.starts_with(SUMMARY_PREFIX)
}
const TAIL_MIN_MESSAGES: usize = 3;
const SUMMARY_INPUT_MSG_CAP: usize = 2_000;
const THRASH_MIN_SAVINGS: f32 = 0.10;
const GAP_MIN_PROGRESS: f32 = 0.25;
const ABS_MIN_RECLAIM_TOKENS: usize = 200;
#[derive(Debug)]
pub struct CompressState {
last_savings: [f32; 2],
last_effective: [bool; 2],
attempts: usize,
disabled: bool,
notified: bool,
failopen_notified: bool,
}
impl Default for CompressState {
fn default() -> Self {
Self::new()
}
}
impl CompressState {
pub fn new() -> Self {
Self {
last_savings: [1.0, 1.0],
last_effective: [true, true],
attempts: 0,
disabled: false,
notified: false,
failopen_notified: false,
}
}
fn record(&mut self, tokens_before: usize, tokens_after: usize, budget: usize) {
let relative = if tokens_before > 0 {
1.0 - (tokens_after as f32 / tokens_before as f32)
} else {
0.0
};
let gap_before = tokens_before.saturating_sub(budget);
let gap_after = tokens_after.saturating_sub(budget);
let effective = tokens_after <= budget
|| (gap_before > 0
&& (gap_after as f32) <= (gap_before as f32) * (1.0 - GAP_MIN_PROGRESS))
|| tokens_before.saturating_sub(tokens_after) >= ABS_MIN_RECLAIM_TOKENS
|| relative >= THRASH_MIN_SAVINGS;
self.last_savings = [self.last_savings[1], relative];
self.last_effective = [self.last_effective[1], effective];
self.attempts += 1;
if self.attempts >= 2 && !self.last_effective[0] && !self.last_effective[1] {
self.disabled = true;
}
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn reset(&mut self) {
*self = Self::new();
}
#[doc(hidden)]
pub fn latch_disabled_for_tests(&mut self) {
self.disabled = true;
self.notified = true;
}
pub fn counters(&self) -> CompressCounters {
let strikes = if self.attempts == 0 || self.last_effective[1] {
0
} else if self.attempts >= 2 && !self.last_effective[0] {
2
} else {
1
};
CompressCounters {
compressions: self.attempts,
strikes,
disabled: self.disabled,
last_reclaim: (self.attempts > 0).then_some(self.last_savings[1]),
}
}
fn take_notice(&mut self) -> Option<String> {
if self.disabled && !self.notified {
self.notified = true;
Some(
"context compression was ineffective twice in a row — auto-compression \
is disabled for this session; start a new conversation to reset"
.to_string(),
)
} else {
None
}
}
fn take_failopen_notice(&mut self) -> Option<String> {
if !self.failopen_notified {
self.failopen_notified = true;
Some(
"context exceeds the proven-good budget, but no authoritative window \
limit is known for this model — dispatching over budget and letting \
the backend decide; an accepted size raises the learned budget"
.to_string(),
)
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CompressCounters {
pub compressions: usize,
pub strikes: usize,
pub disabled: bool,
pub last_reclaim: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CompressTrigger {
pub budget: usize,
pub max_messages: Option<usize>,
pub hard_budget: bool,
}
pub(crate) fn compression_trigger(
len: usize,
current_tokens: usize,
message_tokens: usize,
count_threshold: usize,
token_threshold: Option<usize>,
send_budget: Option<usize>,
tool_tokens: usize,
) -> Option<CompressTrigger> {
let token_threshold = token_threshold.filter(|&b| b > 0);
let send_budget = send_budget.filter(|&b| b > 0);
let count_fired = len > count_threshold;
let token_fired = token_threshold.is_some_and(|b| current_tokens > b);
let guard_fired = send_budget.is_some_and(|b| current_tokens > b);
if !(count_fired || token_fired || guard_fired) {
return None;
}
let mut budget = usize::MAX;
if token_fired {
budget = budget.min(token_threshold.unwrap_or(usize::MAX));
}
if guard_fired {
budget = budget.min(
send_budget
.unwrap_or(usize::MAX)
.saturating_sub(tool_tokens),
);
}
let hard_budget = budget != usize::MAX;
if !hard_budget {
budget = message_tokens / 2;
}
Some(CompressTrigger {
budget,
max_messages: count_fired.then_some(count_threshold / 2),
hard_budget,
})
}
pub(crate) struct CompressRequest<'a> {
pub messages: &'a [Value],
pub budget: usize,
pub max_messages: Option<usize>,
pub task: &'a str,
pub authoritative: bool,
pub hard_budget: bool,
pub focus: Option<&'a str>,
pub est: crate::tokens::TokenEstimation,
pub summary_input_cap_floor_chars: usize,
pub compaction_store: Option<&'a dyn crate::agentic::spill::SpillStore>,
}
impl<'a> CompressRequest<'a> {
pub(crate) fn user_initiated(
messages: &'a [Value],
task: &'a str,
focus: Option<&'a str>,
est: TokenEstimation,
summary_input_cap_floor_chars: usize,
) -> Self {
Self {
messages,
budget: estimate_tokens(messages, est) / 2,
max_messages: None,
task,
hard_budget: false,
authoritative: true,
focus,
est,
summary_input_cap_floor_chars,
compaction_store: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CompressAction {
Fit,
Pruned,
Summarized,
StaticFallback,
Refused,
DispatchedOverBudget,
}
impl CompressAction {
pub(crate) fn describe(self) -> &'static str {
match self {
Self::Fit => "no change",
Self::Pruned => "structural prune",
Self::Summarized => "prune + summary",
Self::StaticFallback => "prune + static marker",
Self::Refused => "refused",
Self::DispatchedOverBudget => "over budget — dispatched",
}
}
}
pub(crate) struct CompressOutcome {
pub messages: Vec<Value>,
pub action: CompressAction,
pub fired: bool,
pub tokens_before: usize,
pub tokens_after: usize,
pub notice: Option<String>,
}
pub(crate) async fn compress(
req: CompressRequest<'_>,
summarizer: Option<&SummarizeFn>,
state: &mut CompressState,
) -> CompressOutcome {
let tokens_before = estimate_tokens(req.messages, req.est);
let tokens_over_entry = req.hard_budget && tokens_before > req.budget;
let over = |tokens: usize, len: usize| {
tokens > req.budget || req.max_messages.is_some_and(|m| len > m)
};
if !over(tokens_before, req.messages.len()) {
return CompressOutcome {
messages: req.messages.to_vec(),
action: CompressAction::Fit,
fired: false,
tokens_before,
tokens_after: tokens_before,
notice: None,
};
}
let mut force_marker = false;
if state.disabled && req.hard_budget {
if tokens_over_entry {
if req.authoritative {
force_marker = true;
} else {
return CompressOutcome {
messages: req.messages.to_vec(),
action: CompressAction::DispatchedOverBudget,
fired: false,
tokens_before,
tokens_after: tokens_before,
notice: state.take_failopen_notice(),
};
}
} else {
return CompressOutcome {
messages: req.messages.to_vec(),
action: CompressAction::Fit,
fired: false,
tokens_before,
tokens_after: tokens_before,
notice: state.take_notice(),
};
}
}
let pruned = prune(req.messages, &PruneConfig::default());
let prune_changed = pruned.chars_reclaimed > 0;
let pruned = pruned.messages;
let after_prune = estimate_tokens(&pruned, req.est);
if !over(after_prune, pruned.len()) {
if tokens_over_entry {
state.record(tokens_before, after_prune, req.budget);
}
return CompressOutcome {
messages: pruned,
action: CompressAction::Pruned,
fired: prune_changed,
tokens_before,
tokens_after: after_prune,
notice: state.take_notice(),
};
}
let boundary = compute_boundary(&pruned, req.budget, req.max_messages, req.est);
let middle = &pruned[boundary.head..boundary.tail_start];
let (mut assembled, mut action) = if middle.is_empty() {
(pruned.clone(), CompressAction::Pruned)
} else {
let body = if force_marker {
None
} else {
match summarizer {
Some(f) => {
let middle_cap = req
.est
.chars_for_tokens(req.budget)
.max(req.summary_input_cap_floor_chars);
summarize_middle(f, req.task, middle, middle_cap, req.focus).await
}
None => None,
}
};
let action = if body.is_some() {
CompressAction::Summarized
} else {
CompressAction::StaticFallback
};
let mut body = body.unwrap_or_else(|| static_fallback_text(middle.len()));
if let Some(crumb) = reread_breadcrumb(middle) {
body.push_str("\n\n");
body.push_str(&crumb);
}
if let Some(store) = req.compaction_store {
let verbatim: String = middle
.iter()
.map(render_message)
.collect::<Vec<_>>()
.join("\n");
let id = store.store(redact_secrets(&verbatim));
body.push_str(&format!(
"\n\n[the full verbatim text of this compacted span is retrievable with \
memory_fetch(\"compaction:{id}\") — use it to recover an exact detail \
this summary dropped, instead of guessing]"
));
}
let mut out = Vec::with_capacity(boundary.head + 1 + (pruned.len() - boundary.tail_start));
out.extend_from_slice(&pruned[..boundary.head]);
out.push(summary_message(&body));
out.extend_from_slice(&pruned[boundary.tail_start..]);
(out, action)
};
repair_orphaned_tool_calls(&mut assembled);
if estimate_tokens(&assembled, req.est) > req.budget {
let aggressive = prune(
&assembled,
&PruneConfig {
keep_last: trailing_tool_group_len(&assembled).max(2),
..PruneConfig::default()
},
);
if aggressive.chars_reclaimed > 0 {
assembled = aggressive.messages;
if action == CompressAction::Fit {
action = CompressAction::Pruned;
}
}
if req.hard_budget
&& estimate_tokens(&assembled, req.est) > req.budget
&& reclaim_within_trailing_group(&mut assembled, req.budget, req.est)
&& action == CompressAction::Fit
{
action = CompressAction::Pruned;
}
}
if force_marker && estimate_tokens(&assembled, req.est) > req.budget {
return CompressOutcome {
messages: req.messages.to_vec(),
action: CompressAction::Refused,
fired: false,
tokens_before,
tokens_after: tokens_before,
notice: state.take_notice(),
};
}
let tokens_after = estimate_tokens(&assembled, req.est);
let fired =
prune_changed || assembled.len() != req.messages.len() || tokens_after != tokens_before;
if tokens_over_entry && !force_marker {
state.record(tokens_before, tokens_after, req.budget);
}
CompressOutcome {
messages: assembled,
action,
fired,
tokens_before,
tokens_after,
notice: state.take_notice(),
}
}
#[derive(Debug, Clone)]
pub struct ManualCompressOutcome {
pub messages: Vec<Value>,
pub fired: bool,
pub messages_before: usize,
pub messages_after: usize,
pub tokens_before: usize,
pub tokens_after: usize,
pub how: &'static str,
pub notice: Option<String>,
}
pub async fn compress_user_initiated(
messages: &[Value],
focus: Option<&str>,
summarizer: Option<&SummarizeFn>,
state: &mut CompressState,
est: crate::tokens::TokenEstimation,
summary_input_cap_floor_chars: usize,
) -> ManualCompressOutcome {
let task = messages
.iter()
.find(|m| m["role"].as_str() == Some("user") && !is_compaction_message(m))
.and_then(|m| m["content"].as_str())
.unwrap_or_default()
.to_string();
let outcome = compress(
CompressRequest::user_initiated(messages, &task, focus, est, summary_input_cap_floor_chars),
summarizer,
state,
)
.await;
if outcome.fired {
state.record(
outcome.tokens_before,
outcome.tokens_after,
outcome.tokens_before / 2,
);
}
let notice = outcome.notice.or_else(|| state.take_notice());
ManualCompressOutcome {
messages_before: messages.len(),
messages_after: outcome.messages.len(),
fired: outcome.fired,
tokens_before: outcome.tokens_before,
tokens_after: outcome.tokens_after,
how: outcome.action.describe(),
notice,
messages: outcome.messages,
}
}
struct Boundary {
head: usize,
tail_start: usize,
}
fn compute_boundary(
messages: &[Value],
budget: usize,
max_messages: Option<usize>,
est: TokenEstimation,
) -> Boundary {
let head = head_len(messages);
let max_tail = max_messages.map(|m| m.saturating_sub(head + 1).max(1));
let tail_budget = (budget / 4).max(1);
let mut tail_start = messages.len();
let mut acc = 0usize;
let mut kept = 0usize;
while tail_start > head {
if max_tail.is_some_and(|m| kept >= m) {
break;
}
let t = estimate_value_tokens(&messages[tail_start - 1], est);
if kept >= TAIL_MIN_MESSAGES && acc + t > tail_budget {
break;
}
acc += t;
kept += 1;
tail_start -= 1;
}
if let Some(last_user) = messages
.iter()
.rposition(|m| m["role"].as_str() == Some("user") && !is_compaction_message(m))
{
if last_user >= head {
tail_start = tail_start.min(last_user);
}
}
while tail_start > head && messages[tail_start]["role"].as_str() == Some("tool") {
tail_start -= 1;
}
if let Some(max_tail) = max_tail {
let cap_start = messages.len().saturating_sub(max_tail);
if tail_start < cap_start {
tail_start = cap_start;
while tail_start > head && messages[tail_start]["role"].as_str() == Some("tool") {
tail_start -= 1;
}
}
}
Boundary { head, tail_start }
}
fn head_len(messages: &[Value]) -> usize {
let mut head = 0;
while head < messages.len() && messages[head]["role"].as_str() == Some("system") {
head += 1;
}
if head < messages.len()
&& messages[head]["role"].as_str() == Some("user")
&& !is_compaction_message(&messages[head])
{
head += 1;
}
head
}
fn trailing_tool_group_len(messages: &[Value]) -> usize {
messages
.iter()
.rposition(|m| m["tool_calls"].as_array().is_some_and(|t| !t.is_empty()))
.map_or(0, |i| messages.len() - i)
}
fn reclaim_within_trailing_group(
assembled: &mut Vec<Value>,
budget: usize,
est: TokenEstimation,
) -> bool {
let group_len = trailing_tool_group_len(assembled);
if group_len == 0 {
return false;
}
let group_start = assembled.len() - group_len;
let outside = estimate_tokens(&assembled[..group_start], est);
let group_tokens = estimate_tokens(&assembled[group_start..], est);
if group_tokens <= budget.saturating_sub(outside) {
return false;
}
let result_idxs: Vec<usize> = (group_start..assembled.len())
.filter(|&i| assembled[i]["role"].as_str() == Some("tool"))
.collect();
let mut changed = false;
for &i in result_idxs.iter().take(result_idxs.len().saturating_sub(1)) {
let pass = prune(
assembled,
&PruneConfig {
keep_last: assembled.len() - i - 1,
..PruneConfig::default()
},
);
if pass.chars_reclaimed > 0 {
*assembled = pass.messages;
changed = true;
}
if estimate_tokens(assembled, est) <= budget {
break;
}
}
changed
}
fn static_fallback_text(removed: usize) -> String {
format!("Summary generation was unavailable. {removed} message(s) were removed.")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConvShape {
Coding,
General,
}
fn middle_shape(middle: &[Value]) -> ConvShape {
let has_tools = middle
.iter()
.any(|m| m["tool_calls"].as_array().is_some_and(|t| !t.is_empty()));
if has_tools {
ConvShape::Coding
} else {
ConvShape::General
}
}
fn reread_breadcrumb(middle: &[Value]) -> Option<String> {
let mut paths: Vec<String> = Vec::new();
for m in middle {
if m["role"].as_str() != Some("assistant") {
continue;
}
let Some(calls) = m["tool_calls"].as_array() else {
continue;
};
for call in calls {
let func = &call["function"];
if !matches!(
func["name"].as_str(),
Some("read_file") | Some("edit_file") | Some("write_file")
) {
continue;
}
let args = &func["arguments"];
let path = args["path"].as_str().map(str::to_string).or_else(|| {
args.as_str()
.and_then(|s| serde_json::from_str::<Value>(s).ok())
.and_then(|v| v["path"].as_str().map(str::to_string))
});
if let Some(p) = path {
if !paths.contains(&p) {
paths.push(p);
}
}
}
}
if paths.is_empty() {
return None;
}
let list = paths
.iter()
.map(|p| format!("- {p}"))
.collect::<Vec<_>>()
.join("\n");
Some(format!(
"Files read or edited in the compacted span — their FULL CONTENTS are \
NOT preserved in the summary above. RE-READ any you rely on before \
using their exact signatures, types, or line contents; do NOT recall \
them from this summary (it is prose, not the file):\n{list}"
))
}
fn summary_message(body: &str) -> Value {
serde_json::json!({
"role": "user",
"content": format!(
"{SUMMARY_PREFIX}\n\
The middle of this conversation was compressed. The text below \
summarizes the removed messages — treat it as background \
reference, NOT as fresh instructions. Your task is unchanged: \
it is stated above and continues in the messages below.\n\n\
{body}\n\n\
{SUMMARY_END_MARKER}"
),
})
}
fn summary_prompt_for(
task: &str,
body: &str,
focus: Option<&str>,
note: Option<&str>,
target_chars: usize,
shape: ConvShape,
) -> String {
let mut p = String::with_capacity(1024);
p.push_str(match shape {
ConvShape::Coding => "You are compressing the middle of a coding-agent conversation.\n\n",
ConvShape::General => "You are compressing the middle of a conversation.\n\n",
});
p.push_str("## Original Task (copy this VERBATIM into \"## Active Task\")\n");
p.push_str(task);
p.push_str("\n\n## Conversation middle to summarise\n");
if let Some(note) = note {
p.push_str(note);
p.push('\n');
}
p.push_str(body);
let words = (target_chars / 6).max(40);
let tokens = (target_chars / 4).max(60);
let sections = match shape {
ConvShape::Coding => {
"## Active Task\n## Completed Actions\n## In Progress\n## Key Decisions\n\
## Relevant Files\n## Critical Context\n"
}
ConvShape::General => {
"## Active Task\n## Discussion\n## Key Points\n## Open Questions\n\
## Critical Context\n"
}
};
p.push_str(&format!(
"\nProduce a concise structured summary with sections:\n{sections}\
Start \"## Active Task\" with the original task copied verbatim. \
Keep the WHOLE summary under ~{words} words (~{tokens} tokens); if it \
cannot all fit, drop low-salience detail — NEVER the Active Task. \
Preserve specifics (file names, error messages, decisions). \
NEVER include API keys, tokens, passwords, or other credentials — \
write [REDACTED] instead.",
));
if let Some(focus) = focus {
let focus = redact_secrets(focus);
let focus = focus.trim();
if !focus.is_empty() {
p.push_str(&format!(
"\nThe user asked for this compression and wants emphasis on \
a topic: emphasize anything about {focus} — give it the bulk \
of the summary's detail while keeping every section above."
));
}
}
p
}
fn summary_request(
task: &str,
middle: &[Value],
middle_cap_chars: usize,
focus: Option<&str>,
shape: ConvShape,
) -> String {
let rendered: Vec<String> = middle.iter().map(render_message).collect();
let mut start = rendered.len();
let mut total = 0usize;
while start > 0 {
let len = rendered[start - 1].chars().count();
if start < rendered.len() && total + len > middle_cap_chars {
break;
}
total += len;
start -= 1;
}
let note = (start > 0).then(|| {
format!(
"[{start} older message(s) omitted from this summary input to fit \
the summarizer's window]"
)
});
let body: String = rendered[start..].concat();
summary_prompt_for(
task,
&body,
focus,
note.as_deref(),
middle_cap_chars / 3,
shape,
)
}
async fn summarize_middle(
summarizer: &SummarizeFn,
task: &str,
middle: &[Value],
cap_chars: usize,
focus: Option<&str>,
) -> Option<String> {
let shape = middle_shape(middle);
let rendered: Vec<String> = middle.iter().map(render_message).collect();
let total: usize = rendered.iter().map(|r| r.chars().count()).sum();
if total <= cap_chars {
let req = redact_secrets(&summary_request(task, middle, cap_chars, focus, shape));
return run_summary(summarizer, req).await;
}
let chunks = chunk_strings(&rendered, cap_chars);
let n = chunks.len();
let mut partials = Vec::with_capacity(n);
for (i, chunk) in chunks.iter().enumerate() {
let note = format!("[part {}/{} of the conversation middle]", i + 1, n);
let req = redact_secrets(&summary_prompt_for(
task,
chunk,
focus,
Some(¬e),
cap_chars / 3,
shape,
));
if let Some(s) = run_summary(summarizer, req).await {
partials.push(s);
}
}
reduce_partials(summarizer, task, partials, cap_chars, focus, shape).await
}
fn chunk_strings(parts: &[String], cap: usize) -> Vec<String> {
let mut chunks = Vec::new();
let mut cur = String::new();
let mut cur_len = 0usize;
for p in parts {
let len = p.chars().count();
if cur_len > 0 && cur_len + len > cap {
chunks.push(std::mem::take(&mut cur));
cur_len = 0;
}
cur.push_str(p);
cur_len += len;
}
if !cur.is_empty() {
chunks.push(cur);
}
chunks
}
fn reduce_partials<'a>(
summarizer: &'a SummarizeFn,
task: &'a str,
partials: Vec<String>,
cap_chars: usize,
focus: Option<&'a str>,
shape: ConvShape,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Option<String>> + Send + 'a>> {
Box::pin(async move {
match partials.len() {
0 => None,
1 => partials.into_iter().next(),
_ => {
let joined_len: usize = partials.iter().map(|p| p.chars().count() + 2).sum();
if joined_len <= cap_chars {
let body = partials.join("\n\n");
let note = format!(
"[{} partial summaries of ONE conversation — consolidate into one]",
partials.len()
);
let req = redact_secrets(&summary_prompt_for(
task,
&body,
focus,
Some(¬e),
cap_chars / 3,
shape,
));
return run_summary(summarizer, req).await;
}
let groups = chunk_strings(&partials, cap_chars);
if groups.len() >= partials.len() {
return Some(partials.join("\n\n"));
}
let mut next = Vec::with_capacity(groups.len());
for g in &groups {
let req = redact_secrets(&summary_prompt_for(
task,
g,
focus,
Some("[partial summaries — consolidate]"),
cap_chars / 3,
shape,
));
if let Some(s) = run_summary(summarizer, req).await {
next.push(s);
}
}
reduce_partials(summarizer, task, next, cap_chars, focus, shape).await
}
}
})
}
async fn run_summary(summarizer: &SummarizeFn, req: String) -> Option<String> {
match summarizer(req).await {
Ok(s) if !s.trim().is_empty() => Some(s),
Ok(_) => None,
Err(e) => {
tracing::warn!(error = %e, "compression summarizer failed — static marker fallback");
None
}
}
}
fn render_message(m: &Value) -> String {
let role = m["role"].as_str().unwrap_or("unknown");
let mut line = format!("[{role}]");
if let Some(tcs) = m["tool_calls"].as_array() {
for tc in tcs {
let name = tc["function"]["name"].as_str().unwrap_or("tool");
let args = tc["function"]["arguments"].to_string();
line.push_str(" called ");
line.push_str(name);
line.push('(');
line.push_str(&excerpt(&redact_secrets(&args), 200));
line.push(')');
}
}
if let Some(content) = m["content"].as_str() {
if !content.is_empty() {
line.push(' ');
line.push_str(&excerpt(&redact_secrets(content), SUMMARY_INPUT_MSG_CAP));
}
}
line.push('\n');
line
}
fn excerpt(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let head: String = s.chars().take(max_chars).collect();
format!("{head}…")
}
}
const REDACTION_TABLE: &[(&str, &str)] = &[
(
r"(?s)-----BEGIN [A-Z ]*PRIVATE KEY-----.*?(?:-----END [A-Z ]*PRIVATE KEY-----|\z)",
"[REDACTED]",
),
(r"\bsk-[A-Za-z0-9_-]{20,}", "[REDACTED]"),
(r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}", "[REDACTED]"),
(r"\bgithub_pat_[A-Za-z0-9_]{20,}", "[REDACTED]"),
(r"\bAKIA[0-9A-Z]{16}\b", "[REDACTED]"),
(
r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}",
"[REDACTED]",
),
(
r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]{20,}",
"Bearer [REDACTED]",
),
(
r#"(?i)\b(api[_-]?key|secret[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|password|passwd)\b["']?\s*[:=]\s*["']?[^\s"']{8,}["']?"#,
"${1}=[REDACTED]",
),
];
fn redaction_patterns() -> &'static Vec<(regex::Regex, &'static str)> {
static PATTERNS: OnceLock<Vec<(regex::Regex, &'static str)>> = OnceLock::new();
PATTERNS.get_or_init(|| {
REDACTION_TABLE
.iter()
.map(|(pat, rep)| {
(
regex::Regex::new(pat).expect("redaction pattern must compile"),
*rep,
)
})
.collect()
})
}
pub(crate) fn redact_secrets(input: &str) -> String {
let mut out = input.to_string();
for (re, rep) in redaction_patterns() {
if re.is_match(&out) {
out = re.replace_all(&out, *rep).into_owned();
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const EST: TokenEstimation = TokenEstimation { chars_per_token: 4 };
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
fn sys(text: &str) -> Value {
json!({"role": "system", "content": text})
}
fn user(text: &str) -> Value {
json!({"role": "user", "content": text})
}
fn assistant_call(name: &str, args: Value) -> Value {
json!({"role": "assistant", "content": "",
"tool_calls": [{"function": {"name": name, "arguments": args}}]})
}
fn tool_result(content: &str) -> Value {
json!({"role": "tool", "content": content})
}
fn tool_heavy(task: &str, rounds: usize, result_chars: usize) -> Vec<Value> {
let mut msgs = vec![sys("you are newt"), user(task)];
for i in 0..rounds {
msgs.push(assistant_call(
"read_file",
json!({"path": format!("src/file_{i}.rs")}),
));
msgs.push(tool_result(&format!("{i}:{}", "x".repeat(result_chars))));
}
msgs
}
fn recording_summarizer(prompts: Arc<Mutex<Vec<String>>>, reply: &'static str) -> Summarizer {
Box::new(move |prompt: String| {
let prompts = prompts.clone();
Box::pin(async move {
prompts.lock().unwrap().push(prompt);
Ok(reply.to_string())
})
})
}
fn failing_summarizer(calls: Arc<AtomicUsize>) -> Summarizer {
Box::new(move |_prompt: String| {
let calls = calls.clone();
Box::pin(async move {
calls.fetch_add(1, Ordering::SeqCst);
anyhow::bail!("summarizer endpoint 500")
})
})
}
async fn run(
messages: &[Value],
budget: usize,
max_messages: Option<usize>,
summarizer: Option<&SummarizeFn>,
state: &mut CompressState,
) -> CompressOutcome {
compress(
CompressRequest {
messages,
budget,
max_messages,
task: "fix the failing test",
hard_budget: true,
authoritative: true,
focus: None,
est: EST,
summary_input_cap_floor_chars: 8_192,
compaction_store: None,
},
summarizer,
state,
)
.await
}
async fn run_non_authoritative(
messages: &[Value],
budget: usize,
max_messages: Option<usize>,
summarizer: Option<&SummarizeFn>,
state: &mut CompressState,
) -> CompressOutcome {
compress(
CompressRequest {
messages,
budget,
max_messages,
task: "fix the failing test",
hard_budget: true,
authoritative: false,
focus: None,
est: EST,
summary_input_cap_floor_chars: 8_192,
compaction_store: None,
},
summarizer,
state,
)
.await
}
async fn run_count_only(
messages: &[Value],
budget: usize,
max_messages: Option<usize>,
summarizer: Option<&SummarizeFn>,
state: &mut CompressState,
) -> CompressOutcome {
compress(
CompressRequest {
messages,
budget,
max_messages,
task: "fix the failing test",
hard_budget: false,
authoritative: false,
focus: None,
est: EST,
summary_input_cap_floor_chars: 8_192,
compaction_store: None,
},
summarizer,
state,
)
.await
}
#[tokio::test]
async fn within_budget_is_a_noop() {
let msgs = tool_heavy("task", 2, 100);
let mut state = CompressState::new();
let out = run(&msgs, 100_000, None, None, &mut state).await;
assert_eq!(out.action, CompressAction::Fit);
assert!(!out.fired);
assert_eq!(out.messages, msgs);
assert_eq!(state.attempts, 0, "a no-op never counts as a compression");
}
#[tokio::test]
async fn prune_short_circuits_when_sufficient() {
let big = "y".repeat(8_000);
let mut msgs = vec![
sys("you are newt"),
user("task"),
assistant_call("run_command", json!({"command": "cargo test"})),
tool_result(&big),
assistant_call("run_command", json!({"command": "cargo test"})),
tool_result(&big),
];
for i in 0..10 {
msgs.push(user(&format!("filler {i}")));
}
let before = estimate_tokens(&msgs, EST);
let budget = before - 1_000; let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "SUMMARY");
let mut state = CompressState::new();
let out = run(&msgs, budget, None, Some(&*s), &mut state).await;
assert_eq!(out.action, CompressAction::Pruned);
assert!(out.fired);
assert!(out.tokens_after <= budget);
assert_eq!(out.messages.len(), msgs.len(), "prune never drops messages");
assert!(
prompts.lock().unwrap().is_empty(),
"summarizer must not be called when pruning suffices"
);
}
#[tokio::test]
async fn summarizes_middle_with_markers_when_prune_insufficient() {
let msgs = tool_heavy("ACTIVE TASK GAUNTLET-7f3d9c: do the thing", 6, 4_000);
let before = estimate_tokens(&msgs, EST);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "## Active Task\nGAUNTLET summary");
let mut state = CompressState::new();
let out = run(&msgs, before / 3, None, Some(&*s), &mut state).await;
assert_eq!(out.action, CompressAction::Summarized);
assert!(out.fired);
assert!(out.tokens_after < before);
assert_eq!(out.messages[0], msgs[0]);
assert_eq!(out.messages[1], msgs[1]);
let summary = out.messages[2]["content"].as_str().unwrap();
assert!(summary.starts_with(SUMMARY_PREFIX), "{summary}");
assert!(summary.contains("GAUNTLET summary"), "{summary}");
assert!(summary.contains(SUMMARY_END_MARKER), "{summary}");
assert!(
!out.messages.iter().any(|m| m["content"]
.as_str()
.is_some_and(|c| c.contains("earlier tool-call messages omitted"))),
"the old placeholder-discard line must not appear"
);
}
#[tokio::test]
async fn summarized_file_reads_get_a_reread_breadcrumb() {
let sig = "pub fn connect(&self, url: &str, timeout: Duration) -> Result<Session, ConnErr>";
let api_body = format!(
"pub struct ApiClient;\nimpl ApiClient {{\n {sig} {{ todo!() }}\n}}\n{}",
"// detail line\n".repeat(200)
);
let mut msgs = vec![
sys("you are newt, a coding agent"),
user("ACTIVE TASK: implement reconnect() on ApiClient using its connect() method"),
assistant_call("read_file", json!({ "path": "src/api.rs" })),
tool_result(&api_body), ];
for i in 0..8 {
msgs.push(assistant_call(
"read_file",
json!({ "path": format!("src/other_{i}.rs") }),
));
msgs.push(tool_result(&format!(
"// other file {i}\n{}",
"filler line\n".repeat(150)
)));
}
let before = estimate_tokens(&msgs, EST);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(
prompts.clone(),
"## Active Task\nImplement reconnect(). The agent earlier read src/api.rs \
(defines ApiClient) and several other files.",
);
let mut state = CompressState::new();
let out = run(&msgs, before / 2, None, Some(&*s), &mut state).await;
let assembled: String = out
.messages
.iter()
.filter_map(|m| m["content"].as_str())
.collect::<Vec<_>>()
.join("\n");
eprintln!(
"#319: fired={} action={:?}\n{}",
out.fired,
out.action,
&assembled[..assembled.len().min(1200)]
);
assert!(out.fired && out.action == CompressAction::Summarized);
assert!(
assembled.contains("src/api.rs"),
"the dropped file must be named so the model knows to re-read it"
);
assert!(
assembled.contains("RE-READ") && assembled.contains("do NOT recall"),
"the breadcrumb must carry the re-read / don't-recall directive"
);
}
#[tokio::test]
async fn summary_request_carries_task_verbatim_and_template() {
let task = "ACTIVE TASK GAUNTLET-7f3d9c: read ten files then report";
let mut msgs = tool_heavy(task, 6, 4_000);
msgs[1] = user(task);
let before = estimate_tokens(&msgs, EST);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "SUMMARY");
let mut state = CompressState::new();
let out = compress(
CompressRequest {
messages: &msgs,
budget: before / 3,
max_messages: None,
task,
hard_budget: true,
authoritative: true,
focus: None,
est: EST,
summary_input_cap_floor_chars: 8_192,
compaction_store: None,
},
Some(&*s),
&mut state,
)
.await;
assert_eq!(out.action, CompressAction::Summarized);
let prompts = prompts.lock().unwrap();
assert_eq!(prompts.len(), 1);
let p = &prompts[0];
assert!(p.contains(task), "original task must appear verbatim: {p}");
for section in [
"## Active Task",
"## Completed Actions",
"## In Progress",
"## Key Decisions",
"## Relevant Files",
"## Critical Context",
] {
assert!(p.contains(section), "missing template section {section}");
}
assert!(p.contains("copied verbatim"), "verbatim-Active-Task rule");
assert!(p.contains("[REDACTED]"), "redaction preamble present");
}
#[tokio::test]
async fn no_summarizer_uses_static_fallback_marker() {
let msgs = tool_heavy("task", 6, 4_000);
let before = estimate_tokens(&msgs, EST);
let mut state = CompressState::new();
let out = run(&msgs, before / 3, None, None, &mut state).await;
assert_eq!(out.action, CompressAction::StaticFallback);
let summary = out.messages[2]["content"].as_str().unwrap();
assert!(summary.starts_with(SUMMARY_PREFIX), "{summary}");
assert!(summary.contains(SUMMARY_END_MARKER), "{summary}");
let removed = msgs.len() - (out.messages.len() - 1);
assert!(
summary.contains(&format!(
"Summary generation was unavailable. {removed} message(s) were removed."
)),
"{summary}"
);
}
#[tokio::test]
async fn summarizer_failure_falls_back_to_static_marker() {
let msgs = tool_heavy("task", 6, 4_000);
let before = estimate_tokens(&msgs, EST);
let calls = Arc::new(AtomicUsize::new(0));
let s = failing_summarizer(calls.clone());
let mut state = CompressState::new();
let out = run(&msgs, before / 3, None, Some(&*s), &mut state).await;
assert_eq!(calls.load(Ordering::SeqCst), 1, "summarizer was attempted");
assert_eq!(out.action, CompressAction::StaticFallback);
let summary = out.messages[2]["content"].as_str().unwrap();
assert!(summary.contains("Summary generation was unavailable."));
}
#[tokio::test]
async fn empty_summary_falls_back_to_static_marker() {
let msgs = tool_heavy("task", 6, 4_000);
let before = estimate_tokens(&msgs, EST);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), " \n ");
let mut state = CompressState::new();
let out = run(&msgs, before / 3, None, Some(&*s), &mut state).await;
assert_eq!(out.action, CompressAction::StaticFallback);
}
#[tokio::test]
async fn giant_aged_round_is_pruned_aggressively_not_shipped_over_budget() {
let task = "ACTIVE TASK GAUNTLET-7f3d9c: summarize the three files";
let mut msgs = vec![sys("you are newt"), user(task)];
msgs.push(json!({"role": "assistant", "content": "", "tool_calls": [
{"function": {"name": "read_file", "arguments": {"path": "a.txt"}}},
{"function": {"name": "read_file", "arguments": {"path": "b.txt"}}},
{"function": {"name": "read_file", "arguments": {"path": "c.txt"}}},
]}));
for _ in 0..3 {
msgs.push(tool_result(&"z".repeat(50_000))); }
msgs.push(assistant_call("read_file", json!({"path": "d.txt"})));
msgs.push(tool_result("short fresh result"));
let mut state = CompressState::new();
let out = run(&msgs, 3_000, None, None, &mut state).await;
assert!(
out.tokens_after <= 3_000,
"the fit pass must bring ~{} under budget, got {}",
out.tokens_before,
out.tokens_after
);
assert!(out.fired);
assert!(out
.messages
.iter()
.any(|m| m["content"].as_str() == Some(task)));
assert_eq!(out.messages[2]["tool_calls"].as_array().unwrap().len(), 3);
assert_eq!(
out.messages
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.count(),
4
);
assert_eq!(
out.messages.last().unwrap()["content"].as_str(),
Some("short fresh result")
);
}
#[tokio::test]
async fn fresh_trailing_tool_group_survives_the_aggressive_pass() {
let task = "ACTIVE TASK GAUNTLET-7f3d9c: summarize the three files";
let big = "z".repeat(50_000);
let mut msgs = vec![sys("you are newt"), user(task)];
msgs.push(json!({"role": "assistant", "content": "", "tool_calls": [
{"function": {"name": "read_file", "arguments": {"path": "a.txt"}}},
{"function": {"name": "read_file", "arguments": {"path": "b.txt"}}},
{"function": {"name": "read_file", "arguments": {"path": "c.txt"}}},
]}));
for _ in 0..3 {
msgs.push(tool_result(&big));
}
let mut state = CompressState::new();
let out = run_count_only(&msgs, 3_000, None, None, &mut state).await;
let results: Vec<&str> = out
.messages
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.map(|m| m["content"].as_str().unwrap())
.collect();
assert_eq!(results.len(), 3);
for r in results {
assert_eq!(r, big, "fresh trailing tool results must never be pruned");
}
assert!(
out.tokens_after > 3_000,
"this shape is genuinely incompressible without destroying fresh results"
);
}
#[test]
fn trailing_group_derivation_survives_interleaved_messages() {
let mut msgs = vec![sys("you are newt"), user("task")];
msgs.push(json!({"role": "assistant", "content": "", "tool_calls": [
{"function": {"name": "read_file", "arguments": {"path": "a.rs"}}},
{"function": {"name": "read_file", "arguments": {"path": "b.rs"}}},
]}));
msgs.push(tool_result("result a"));
msgs.push(tool_result("result b"));
assert_eq!(trailing_tool_group_len(&msgs), 3);
msgs.push(user(
"[3 consecutive read-only rounds with no file writes.]",
));
assert_eq!(trailing_tool_group_len(&msgs), 4);
msgs.push(summary_message("reference summary"));
assert_eq!(trailing_tool_group_len(&msgs), 5);
msgs.push(json!({"role": "assistant", "content": "thinking…"}));
assert_eq!(trailing_tool_group_len(&msgs), 6);
assert_eq!(trailing_tool_group_len(&[sys("s"), user("t")]), 0);
let roleless = vec![
user("task"),
json!({"content": "", "tool_calls": [
{"function": {"name": "read_file", "arguments": {"path": "a"}}}]}),
tool_result("result a"),
];
assert_eq!(trailing_tool_group_len(&roleless), 2);
}
#[tokio::test]
async fn nudge_after_fresh_group_does_not_defeat_the_protection() {
let task = "ACTIVE TASK GAUNTLET-7f3d9c: read both files then report";
let unseen1 = format!("1:{}", "u".repeat(8_000));
let unseen2 = format!("2:{}", "v".repeat(8_000));
let mut msgs = vec![sys("you are newt"), user(task)];
for i in 0..6 {
msgs.push(assistant_call(
"read_file",
json!({"path": format!("aged_{i}.rs")}),
));
msgs.push(tool_result(&format!("{i}:{}", "a".repeat(8_000))));
}
msgs.push(json!({"role": "assistant", "content": "", "tool_calls": [
{"function": {"name": "read_file", "arguments": {"path": "unseen1.rs"}}},
{"function": {"name": "read_file", "arguments": {"path": "unseen2.rs"}}},
]}));
msgs.push(tool_result(&unseen1));
msgs.push(tool_result(&unseen2));
msgs.push(user(
"[3 consecutive read-only rounds with no file writes. \
Stop exploring. Call edit_file or write_file now.]",
));
let mut state = CompressState::new();
let out = run_count_only(&msgs, 2_000, None, None, &mut state).await;
assert!(out.fired);
let tool_contents: Vec<&str> = out
.messages
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.map(|m| m["content"].as_str().unwrap())
.collect();
assert!(
tool_contents.contains(&unseen1.as_str()),
"#270: UNSEEN1 must survive the nudge-truncated derivation \
(got tool contents {:?})",
tool_contents
.iter()
.map(|c| c.chars().take(40).collect::<String>())
.collect::<Vec<_>>()
);
assert!(
tool_contents.contains(&unseen2.as_str()),
"UNSEEN2 must survive too"
);
assert!(out.messages.iter().any(|m| m["content"]
.as_str()
.is_some_and(|c| c.contains("read-only rounds"))));
println!(
"#270 repro trace: {} -> {} est. tokens (target {}), group intact",
out.tokens_before, out.tokens_after, 2_000
);
}
#[tokio::test]
async fn compaction_notice_after_fresh_group_does_not_defeat_the_protection() {
let task = "ACTIVE TASK GAUNTLET-7f3d9c: read both files then report";
let unseen1 = format!("1:{}", "u".repeat(8_000));
let unseen2 = format!("2:{}", "v".repeat(8_000));
let mut msgs = vec![sys("you are newt"), user(task)];
for i in 0..6 {
msgs.push(assistant_call(
"read_file",
json!({"path": format!("aged_{i}.rs")}),
));
msgs.push(tool_result(&format!("{i}:{}", "a".repeat(8_000))));
}
msgs.push(json!({"role": "assistant", "content": "", "tool_calls": [
{"function": {"name": "read_file", "arguments": {"path": "unseen1.rs"}}},
{"function": {"name": "read_file", "arguments": {"path": "unseen2.rs"}}},
]}));
msgs.push(tool_result(&unseen1));
msgs.push(tool_result(&unseen2));
msgs.push(summary_message("## Active Task\nreference summary"));
let mut state = CompressState::new();
let out = run_count_only(&msgs, 2_000, None, None, &mut state).await;
let tool_contents: Vec<&str> = out
.messages
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.map(|m| m["content"].as_str().unwrap())
.collect();
assert!(tool_contents.contains(&unseen1.as_str()), "UNSEEN1 intact");
assert!(tool_contents.contains(&unseen2.as_str()), "UNSEEN2 intact");
}
#[test]
fn within_group_reclaim_fires_only_when_group_alone_exceeds() {
let big = "z".repeat(20_000); let small = "s".repeat(1_200); let group = |contents: &[&str]| -> Vec<Value> {
let mut msgs = vec![sys("you are newt"), user("task")];
msgs.push(json!({"role": "assistant", "content": "", "tool_calls":
contents.iter().enumerate().map(|(i, _)| json!(
{"function": {"name": "read_file",
"arguments": {"path": format!("f{i}.txt")}}}
)).collect::<Vec<_>>()
}));
msgs.extend(contents.iter().map(|c| tool_result(c)));
msgs
};
let mut fits = group(&[&small, &small, &small]);
let before = fits.clone();
assert!(!reclaim_within_trailing_group(&mut fits, 10_000, EST));
assert_eq!(fits, before, "a group within its share is never touched");
let mut no_group = vec![sys("s"), user(&big)];
assert!(!reclaim_within_trailing_group(&mut no_group, 100, EST));
let mut single = group(&[&big]);
let before = single.clone();
assert!(!reclaim_within_trailing_group(&mut single, 1_000, EST));
assert_eq!(single, before);
let mut early = group(&[&big, &small, &small]);
assert!(reclaim_within_trailing_group(&mut early, 1_500, EST));
let results: Vec<&str> = early
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.map(|m| m["content"].as_str().unwrap())
.collect();
assert!(
results[0].starts_with("[read_file] read 'f0.txt'"),
"oldest one-lined with the re-read affordance: {}",
results[0]
);
assert_eq!(results[1], small, "middle untouched after early stop");
assert_eq!(results[2], small, "newest untouched");
assert!(estimate_tokens(&early, EST) <= 1_500, "the list now fits");
let mut residual = group(&[&small, &small, &big]);
assert!(reclaim_within_trailing_group(&mut residual, 1_000, EST));
let results: Vec<&str> = residual
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.map(|m| m["content"].as_str().unwrap())
.collect();
assert!(results[0].starts_with("[read_file] read 'f0.txt'"));
assert!(results[1].starts_with("[read_file] read 'f1.txt'"));
assert_eq!(results[2], big, "the newest member is never a candidate");
assert!(
estimate_tokens(&residual, EST) > 1_000,
"single-result-too-big: truthfully still over budget"
);
}
#[tokio::test]
async fn oversized_group_reclaims_within_keeping_newest_whole() {
let task = "ACTIVE TASK GAUNTLET-7f3d9c: summarize the three files";
let big = "z".repeat(50_000); let mut msgs = vec![sys("you are newt"), user(task)];
msgs.push(json!({"role": "assistant", "content": "", "tool_calls": [
{"function": {"name": "read_file", "arguments": {"path": "a.txt"}}},
{"function": {"name": "read_file", "arguments": {"path": "b.txt"}}},
{"function": {"name": "read_file", "arguments": {"path": "c.txt"}}},
]}));
for _ in 0..3 {
msgs.push(tool_result(&big));
}
let mut state = CompressState::new();
let out = run(&msgs, 3_000, None, None, &mut state).await;
assert!(out.fired);
let results: Vec<&str> = out
.messages
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.map(|m| m["content"].as_str().unwrap())
.collect();
assert_eq!(results.len(), 3, "pairing intact — nothing dropped");
assert!(
results[0].starts_with("[read_file] read 'a.txt'"),
"oldest one-lined, file named for re-read: {}",
results[0]
);
assert!(
results[1].starts_with("[read_file] read 'b.txt'"),
"older one-lined in order: {}",
results[1]
);
assert_eq!(results[2], big, "newest result reaches the model whole");
assert!(out
.messages
.iter()
.any(|m| m["content"].as_str() == Some(task)));
assert!(out.tokens_after > 3_000);
assert!(
out.tokens_after < out.tokens_before / 2,
"but the reclaim was real: {} -> {}",
out.tokens_before,
out.tokens_after
);
println!(
"#285 scenario trace: {} -> {} est. tokens (budget 3000), \
a/b one-lined, c whole",
out.tokens_before, out.tokens_after
);
}
#[tokio::test]
async fn under_budget_group_is_untouched_under_hard_pressure() {
let task = "ACTIVE TASK GAUNTLET-7f3d9c: read both files then report";
let unseen1 = format!("1:{}", "u".repeat(8_000)); let unseen2 = format!("2:{}", "v".repeat(8_000));
let mut msgs = vec![sys("you are newt"), user(task)];
for i in 0..6 {
msgs.push(assistant_call(
"read_file",
json!({"path": format!("aged_{i}.rs")}),
));
msgs.push(tool_result(&format!("{i}:{}", "a".repeat(8_000))));
}
msgs.push(json!({"role": "assistant", "content": "", "tool_calls": [
{"function": {"name": "read_file", "arguments": {"path": "unseen1.rs"}}},
{"function": {"name": "read_file", "arguments": {"path": "unseen2.rs"}}},
]}));
msgs.push(tool_result(&unseen1));
msgs.push(tool_result(&unseen2));
msgs.push(user(
"[3 consecutive read-only rounds with no file writes.]",
));
let mut state = CompressState::new();
let out = run(&msgs, 6_000, None, None, &mut state).await;
assert!(out.fired);
assert!(
out.tokens_after <= 6_000,
"must land under the hard budget ({} -> {})",
out.tokens_before,
out.tokens_after
);
let tool_contents: Vec<&str> = out
.messages
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.map(|m| m["content"].as_str().unwrap())
.collect();
assert!(tool_contents.contains(&unseen1.as_str()), "UNSEEN1 whole");
assert!(tool_contents.contains(&unseen2.as_str()), "UNSEEN2 whole");
}
#[tokio::test]
async fn max_messages_forces_summary_stage() {
let msgs = tool_heavy("task", 8, 50); let before = estimate_tokens(&msgs, EST);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "SUMMARY");
let mut state = CompressState::new();
let out = run_count_only(&msgs, before + 1_000, Some(8), Some(&*s), &mut state).await;
assert_eq!(out.action, CompressAction::Summarized);
assert!(out.messages.len() < msgs.len());
}
#[tokio::test]
async fn second_compression_still_shrinks_and_keeps_fresh_results() {
let fresh = format!("9:{}", "x".repeat(4_000));
let msgs = tool_heavy("fix the failing test", 10, 4_000);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "SUMMARY ONE");
let mut state = CompressState::new();
let budget = estimate_tokens(&msgs, EST) / 2;
let first = run_count_only(&msgs, budget, Some(8), Some(&*s), &mut state).await;
assert!(first.messages.len() < msgs.len(), "first pass shrinks");
assert!(first.messages.iter().any(is_compaction_message));
let mut grown = first.messages.clone();
for i in 10..16 {
grown.push(assistant_call(
"read_file",
json!({"path": format!("src/file_{i}.rs")}),
));
grown.push(tool_result(&format!("{i}:{}", "x".repeat(4_000))));
}
let grown_fresh = grown.last().unwrap()["content"]
.as_str()
.unwrap()
.to_string();
let budget2 = estimate_tokens(&grown, EST) / 2;
let second = run_count_only(&grown, budget2, Some(8), Some(&*s), &mut state).await;
assert!(
second.messages.len() < grown.len(),
"second compression must still shrink ({} -> {})",
grown.len(),
second.messages.len()
);
assert!(
second.messages.len() <= 10,
"count goal must stay reachable, got {}",
second.messages.len()
);
assert_eq!(
first.messages.last().unwrap()["content"].as_str(),
Some(fresh.as_str()),
"first pass fresh result intact"
);
assert_eq!(
second.messages.last().unwrap()["content"].as_str(),
Some(grown_fresh.as_str()),
"second pass fresh result intact"
);
assert!(!state.disabled);
assert_eq!(state.attempts, 0);
}
#[tokio::test]
async fn count_only_never_feeds_or_consults_anti_thrash() {
let mut msgs = vec![sys("you are newt"), user("task")];
for i in 0..10 {
msgs.push(user(&format!("note {i}")));
}
let mut state = CompressState::new();
for _ in 0..4 {
let budget = estimate_tokens(&msgs, EST) / 2;
let out = run_count_only(&msgs, budget, Some(6), None, &mut state).await;
assert_ne!(out.action, CompressAction::Refused);
}
assert!(!state.disabled, "count-only passes must never latch");
assert_eq!(state.attempts, 0, "count-only passes must never record");
let mut latched = CompressState::new();
latched.disabled = true;
latched.notified = true;
let budget = estimate_tokens(&msgs, EST) / 2;
let out = run_count_only(&msgs, budget, Some(6), None, &mut latched).await;
assert_ne!(out.action, CompressAction::Refused);
assert!(
out.messages.len() < msgs.len(),
"the VRAM guard must stay alive while anti-thrash is latched"
);
}
#[test]
fn boundary_head_is_system_plus_original_task() {
let msgs = tool_heavy("the task", 6, 1_000);
let b = compute_boundary(&msgs, 1_000, None, EST);
assert_eq!(b.head, 2, "system + original task");
let mut msgs2 = vec![sys("a"), sys("b"), user("task"), user("more")];
msgs2.extend(tool_heavy("x", 4, 1_000).split_off(2));
assert_eq!(compute_boundary(&msgs2, 1_000, None, EST).head, 3);
}
#[test]
fn boundary_tail_is_token_budgeted_with_minimum() {
let msgs = tool_heavy("task", 10, 1_000);
let b = compute_boundary(&msgs, 4_000, None, EST);
let tail_tokens: usize = msgs[b.tail_start..]
.iter()
.map(|m| estimate_value_tokens(m, EST))
.sum();
assert!(
tail_tokens <= 1_500,
"tail stays near the token budget, got {tail_tokens}"
);
assert!(
msgs.len() - b.tail_start >= TAIL_MIN_MESSAGES,
"at least the minimum tail"
);
assert!(b.tail_start > b.head, "a middle exists to summarize");
let msgs = tool_heavy("task", 6, 40_000);
let b = compute_boundary(&msgs, 4_000, None, EST);
assert!(msgs.len() - b.tail_start >= TAIL_MIN_MESSAGES);
}
#[test]
fn boundary_anchors_last_user_message_into_tail() {
let mut msgs = tool_heavy("task", 2, 500);
msgs.push(user("IMPORTANT FOLLOW-UP: also update the docs"));
let follow_up = msgs.len() - 1;
for i in 0..6 {
msgs.push(assistant_call(
"read_file",
json!({"path": format!("f{i}")}),
));
msgs.push(tool_result(&"q".repeat(4_000)));
}
let b = compute_boundary(&msgs, 2_000, None, EST);
assert!(
b.tail_start <= follow_up,
"tail (start {}) must include the last user message at {follow_up}",
b.tail_start
);
}
#[test]
fn boundary_anchor_skips_compaction_messages() {
let mut msgs = vec![sys("you are newt"), user("the task")];
msgs.push(summary_message("## Active Task\nthe task (summarized)"));
for i in 0..6 {
msgs.push(assistant_call(
"read_file",
json!({"path": format!("f{i}")}),
));
msgs.push(tool_result(&"q".repeat(4_000)));
}
let b = compute_boundary(&msgs, 2_000, None, EST);
assert!(
b.tail_start > 2,
"the tail must not pin to the compaction message at index 2 \
(tail_start {})",
b.tail_start
);
let mut msgs2 = msgs.clone();
msgs2.push(user("IMPORTANT FOLLOW-UP: also update the docs"));
let follow_up = msgs2.len() - 1;
for _ in 0..4 {
msgs2.push(assistant_call("read_file", json!({"path": "g"})));
msgs2.push(tool_result(&"q".repeat(4_000)));
}
let b2 = compute_boundary(&msgs2, 2_000, None, EST);
assert!(
b2.tail_start <= follow_up,
"a real user message still anchors the tail"
);
}
#[test]
fn boundary_count_cap_holds_after_the_anchor() {
let mut msgs = vec![
sys("you are newt"),
user("turn 1"),
json!({"role": "assistant", "content": "reply 1"}),
user("turn 2"),
json!({"role": "assistant", "content": "reply 2"}),
user("the current task"),
];
let task_idx = msgs.len() - 1;
for i in 0..12 {
msgs.push(assistant_call(
"read_file",
json!({"path": format!("f{i}")}),
));
msgs.push(tool_result(&"q".repeat(2_000)));
}
let b = compute_boundary(&msgs, 4_000, Some(10), EST);
let assembled = b.head + 1 + (msgs.len() - b.tail_start);
assert!(
assembled <= 12,
"the anchor must not defeat the count goal (assembled {assembled})"
);
assert!(
b.tail_start > task_idx,
"the cut advanced past the deep anchor (tail_start {})",
b.tail_start
);
let b_token = compute_boundary(&msgs, 4_000, None, EST);
assert!(b_token.tail_start <= task_idx);
}
#[test]
fn boundary_never_splits_a_tool_pair() {
for budget in [1_000usize, 2_000, 4_000, 8_000, 16_000] {
let msgs = tool_heavy("task", 8, 2_000);
let b = compute_boundary(&msgs, budget, None, EST);
assert_ne!(
msgs[b.tail_start]["role"].as_str(),
Some("tool"),
"budget {budget}: tail must not start inside a result group"
);
}
}
#[tokio::test]
async fn compress_output_has_no_orphan_tool_pairs() {
let msgs = tool_heavy("task", 8, 2_000);
let mut state = CompressState::new();
let out = run(&msgs, 2_500, None, None, &mut state).await;
let m = &out.messages;
for (i, msg) in m.iter().enumerate() {
if let Some(tcs) = msg["tool_calls"].as_array() {
let mut following = 0;
for next in &m[i + 1..] {
if next["role"].as_str() == Some("tool") {
following += 1;
} else {
break;
}
}
assert_eq!(
following,
tcs.len(),
"message {i}: {} tool_calls need {} contiguous results",
tcs.len(),
tcs.len()
);
}
}
}
#[tokio::test]
async fn anti_thrash_disables_notifies_once_then_refuses() {
let mut msgs = vec![sys(&"s".repeat(4_000)), user("task")];
for i in 0..3 {
msgs.push(user(&format!("note {i}")));
}
let mut state = CompressState::new();
let first = run(&msgs, 100, None, None, &mut state).await;
assert_ne!(first.action, CompressAction::Refused);
assert!(first.notice.is_none(), "one poor pass is not yet thrash");
let second = run(&msgs, 100, None, None, &mut state).await;
let notice = second.notice.expect("second poor pass must notify");
assert!(notice.contains("disabled for this session"), "{notice}");
let third = run(&msgs, 100, None, None, &mut state).await;
assert_eq!(third.action, CompressAction::Refused);
assert!(!third.fired);
assert!(
third.notice.is_none(),
"the notice must be delivered exactly once"
);
let ok = run(&msgs, 100_000, None, None, &mut state).await;
assert_eq!(ok.action, CompressAction::Fit);
}
#[tokio::test]
async fn non_authoritative_budget_fails_open_instead_of_refusing() {
let mut msgs = vec![sys(&"s".repeat(4_000)), user("task")];
for i in 0..3 {
msgs.push(user(&format!("note {i}")));
}
let mut state = CompressState::new();
let first = run_non_authoritative(&msgs, 100, None, None, &mut state).await;
assert_ne!(first.action, CompressAction::Refused);
let _second = run_non_authoritative(&msgs, 100, None, None, &mut state).await;
assert!(state.disabled, "two poor passes must latch the breaker");
let third = run_non_authoritative(&msgs, 100, None, None, &mut state).await;
assert_eq!(third.action, CompressAction::DispatchedOverBudget);
assert!(!third.fired, "messages pass through unchanged");
assert_eq!(third.messages.len(), msgs.len(), "nothing dropped");
let notice = third.notice.expect("fail-open is surfaced once");
assert!(notice.contains("no authoritative window"), "{notice}");
let fourth = run_non_authoritative(&msgs, 100, None, None, &mut state).await;
assert_eq!(fourth.action, CompressAction::DispatchedOverBudget);
assert!(fourth.notice.is_none(), "notice delivered exactly once");
}
#[tokio::test]
async fn authoritative_budget_still_refuses_when_latched() {
let mut msgs = vec![sys(&"s".repeat(4_000)), user("task")];
for i in 0..3 {
msgs.push(user(&format!("note {i}")));
}
let mut state = CompressState::new();
run(&msgs, 100, None, None, &mut state).await;
run(&msgs, 100, None, None, &mut state).await;
assert!(state.disabled);
let third = run(&msgs, 100, None, None, &mut state).await;
assert_eq!(
third.action,
CompressAction::Refused,
"an authoritative ceiling must still refuse, not truncate"
);
}
#[tokio::test]
async fn latched_authoritative_compacts_to_marker_instead_of_refusing() {
let mut msgs = vec![sys("sys"), user("task")];
for i in 0..24 {
msgs.push(user(&format!("middle note {i} {}", "m".repeat(200))));
}
msgs.push(user("recent tail"));
let mut state = CompressState::new();
state.latch_disabled_for_tests();
let budget = 300; let out = run(&msgs, budget, None, None, &mut state).await;
assert_ne!(
out.action,
CompressAction::Refused,
"a reducible middle must compact to a marker, not dead-end"
);
assert!(
out.tokens_after <= budget,
"forced marker compaction must fit the budget ({} > {budget})",
out.tokens_after
);
assert!(out.fired, "the marker compaction changed the working set");
}
#[tokio::test]
async fn compaction_store_captures_redacted_span_and_names_the_handle() {
use crate::agentic::spill::{SessionSpillStore, SpillStore};
let compaction = SessionSpillStore::default();
let mut msgs = vec![sys("sys"), user("task")];
msgs.push(user("config api_key=9f8e7d6c5b4a32100ffee and more"));
for i in 0..24 {
msgs.push(user(&format!("middle note {i} {}", "m".repeat(200))));
}
msgs.push(user("recent tail"));
let mut state = CompressState::new();
let out = compress(
CompressRequest {
messages: &msgs,
budget: 300,
max_messages: None,
task: "task",
hard_budget: true,
authoritative: true,
focus: None,
est: EST,
summary_input_cap_floor_chars: 8_192,
compaction_store: Some(&compaction),
},
None, &mut state,
)
.await;
assert!(out.fired);
assert!(
out.messages.iter().any(|m| m["content"]
.as_str()
.is_some_and(|c| c.contains("compaction:s0"))),
"the marker must name the compaction handle"
);
let span = compaction.fetch("s0").expect("span must be stored");
assert!(
!span.contains("9f8e7d6c5b4a32100ffee"),
"the secret must be redacted before store: {span}"
);
assert!(
span.contains("[REDACTED]"),
"redaction marker present: {span}"
);
}
#[tokio::test]
async fn knowledge_base_stable_base_survives_compression() {
let kb = "## Authoritative import surface\n\
from newt_agent._newt_agent.core import Router # real path, not a guess";
let mut msgs = vec![sys(kb), user("task")];
for i in 0..24 {
msgs.push(user(&format!("middle note {i} {}", "m".repeat(200))));
}
msgs.push(user("recent tail"));
let mut state = CompressState::new();
let out = run(&msgs, 300, None, None, &mut state).await;
assert!(out.fired, "a large conversation should compress");
assert!(
out.messages.iter().any(|m| m["role"] == "system"
&& m["content"]
.as_str()
.is_some_and(|c| c.contains("from newt_agent._newt_agent.core import Router"))),
"the knowledge_base import surface must survive compression VERBATIM \
(the protected head — the stable base E relies on)"
);
}
#[tokio::test]
async fn effective_compressions_do_not_disable() {
let mut state = CompressState::new();
for _ in 0..4 {
let msgs = tool_heavy("task", 6, 4_000);
let before = estimate_tokens(&msgs, EST);
let out = run(&msgs, before / 3, None, None, &mut state).await;
assert_ne!(out.action, CompressAction::Refused);
assert!(out.notice.is_none());
}
assert!(!state.disabled);
}
#[test]
fn thrash_window_requires_consecutive_poor_savings() {
let mut state = CompressState::new();
state.record(1_000, 990, 500); state.record(1_000, 400, 500); state.record(1_000, 990, 500); assert!(!state.disabled, "non-consecutive poor passes never disable");
state.record(1_000, 950, 500); assert!(state.disabled);
}
#[test]
fn budget_aware_gap_progress_is_not_a_strike() {
let mut state = CompressState::new();
state.record(1_000, 920, 800);
state.record(1_000, 920, 800);
assert!(
!state.is_disabled(),
"gap-shrinking passes must not latch the disable"
);
let mut dead = CompressState::new();
dead.record(1_000, 995, 500);
dead.record(1_000, 996, 500);
assert!(dead.is_disabled(), "truly ineffective passes still latch");
}
fn chat_history(turns: usize, chars: usize) -> Vec<Value> {
let mut msgs = vec![sys("you are newt"), user("ORIGINAL TASK: port the parser")];
for i in 0..turns {
msgs.push(user(&format!("q{i} {}", "u".repeat(chars))));
msgs.push(json!({"role": "assistant",
"content": format!("a{i} {}", "v".repeat(chars))}));
}
msgs
}
#[tokio::test]
async fn user_initiated_compresses_without_token_pressure() {
let msgs = chat_history(10, 400);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "## Active Task\nMANUAL SUMMARY");
let mut state = CompressState::new();
let out = compress_user_initiated(&msgs, None, Some(&*s), &mut state, EST, 8_192).await;
assert!(out.fired);
assert_eq!(out.how, CompressAction::Summarized.describe());
assert_eq!(out.messages_before, msgs.len());
assert_eq!(out.messages_after, out.messages.len());
assert!(
out.messages_after < out.messages_before,
"count must shrink"
);
assert!(out.tokens_after < out.tokens_before);
assert!(
out.messages.iter().any(|m| is_compaction_message(m)
&& m["content"].as_str().unwrap().contains("MANUAL SUMMARY")),
"marked summary message must be present"
);
let p = prompts.lock().unwrap();
assert!(p[0].contains("ORIGINAL TASK: port the parser"), "{}", p[0]);
let c = state.counters();
assert_eq!(c.compressions, 1);
assert_eq!(c.strikes, 0, "a good reclaim is not a strike");
assert!(c.last_reclaim.unwrap() > THRASH_MIN_SAVINGS);
assert!(!c.disabled);
}
#[tokio::test]
async fn user_initiated_focus_is_threaded_and_redacted() {
let msgs = chat_history(10, 400);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "SUMMARY");
let mut state = CompressState::new();
let secret = "sk-aaaaaaaaaaaaaaaaaaaaaaaa1234";
let focus = format!("the auth flow around {secret} handling");
let out =
compress_user_initiated(&msgs, Some(&focus), Some(&*s), &mut state, EST, 8_192).await;
assert!(out.fired);
let p = prompts.lock().unwrap();
assert_eq!(p.len(), 1);
assert!(
p[0].contains("emphasize anything about"),
"focus guidance line missing: {}",
p[0]
);
assert!(p[0].contains("the auth flow around"), "{}", p[0]);
assert!(
!p[0].contains(secret),
"a secret typed into the focus must never reach the summarizer"
);
assert!(p[0].contains("[REDACTED]"));
}
#[tokio::test]
async fn no_focus_means_no_guidance_line() {
let msgs = chat_history(10, 400);
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "SUMMARY");
let mut state = CompressState::new();
compress_user_initiated(&msgs, None, Some(&*s), &mut state, EST, 8_192).await;
assert!(!prompts.lock().unwrap()[0].contains("emphasize anything about"));
}
#[tokio::test]
async fn user_initiated_noop_records_nothing() {
let msgs = vec![sys("you are newt"), user("task"), user("note")];
let mut state = CompressState::new();
for _ in 0..3 {
let out = compress_user_initiated(&msgs, None, None, &mut state, EST, 8_192).await;
assert!(!out.fired, "nothing to reclaim — must not fire");
assert_eq!(out.messages, msgs);
assert_eq!(out.tokens_before, out.tokens_after);
assert!(out.notice.is_none());
}
let c = state.counters();
assert_eq!(c.compressions, 0, "no-op runs never count");
assert_eq!(c.strikes, 0);
assert!(!c.disabled);
assert_eq!(c.last_reclaim, None);
}
#[tokio::test]
async fn user_initiated_runs_while_latched() {
let msgs = chat_history(10, 400);
let mut state = CompressState::new();
state.latch_disabled_for_tests();
let out = compress_user_initiated(&msgs, None, None, &mut state, EST, 8_192).await;
assert!(out.fired, "an explicit ask must bypass the latch");
assert_eq!(out.how, CompressAction::StaticFallback.describe());
assert!(state.is_disabled(), "the latch itself stays set");
}
#[test]
fn counters_snapshot_projects_state() {
let mut state = CompressState::new();
let c = state.counters();
assert_eq!((c.compressions, c.strikes, c.disabled), (0, 0, false));
assert_eq!(c.last_reclaim, None);
state.record(1_000, 400, 500); let c = state.counters();
assert_eq!((c.compressions, c.strikes, c.disabled), (1, 0, false));
assert!((c.last_reclaim.unwrap() - 0.6).abs() < 0.01);
state.record(1_000, 990, 500); let c = state.counters();
assert_eq!((c.compressions, c.strikes, c.disabled), (2, 1, false));
state.record(1_000, 950, 500); let c = state.counters();
assert_eq!((c.compressions, c.strikes, c.disabled), (3, 2, true));
assert!(c.last_reclaim.unwrap() < THRASH_MIN_SAVINGS);
}
#[test]
fn counters_first_poor_attempt_is_one_strike() {
let mut state = CompressState::new();
state.record(1_000, 990, 500);
assert_eq!(state.counters().strikes, 1);
}
#[test]
fn trigger_fires_on_count_token_or_guard() {
assert!(compression_trigger(10, 1_000, 900, 40, None, None, 100).is_none());
assert_eq!(
compression_trigger(4, 60_000, 59_000, 40, Some(50_000), None, 100),
Some(CompressTrigger {
budget: 50_000,
max_messages: None,
hard_budget: true,
})
);
assert_eq!(
compression_trigger(4, 9_000, 8_600, 40, None, Some(8_000), 500),
Some(CompressTrigger {
budget: 7_500,
max_messages: None,
hard_budget: true,
})
);
assert_eq!(
compression_trigger(41, 1_000, 800, 40, None, None, 100),
Some(CompressTrigger {
budget: 400,
max_messages: Some(20),
hard_budget: false,
})
);
assert_eq!(
compression_trigger(41, 60_000, 59_000, 40, Some(50_000), Some(20_000), 500),
Some(CompressTrigger {
budget: 19_500,
max_messages: Some(20),
hard_budget: true,
})
);
assert!(compression_trigger(4, 7_999, 7_000, 40, Some(50_000), Some(8_000), 0).is_none());
}
#[test]
fn trigger_zero_token_budget_is_disabled() {
assert!(compression_trigger(4, 100, 90, 40, Some(0), None, 0).is_none());
assert!(compression_trigger(4, 100, 90, 40, None, Some(0), 10).is_none());
assert_eq!(
compression_trigger(41, 100, 90, 40, Some(0), Some(0), 10),
Some(CompressTrigger {
budget: 45,
max_messages: Some(20),
hard_budget: false,
})
);
}
#[test]
fn redaction_catches_true_positives() {
let cases = [
(
"the key is sk-AbCdEf1234567890AbCdEf1234567890",
"sk-AbCdEf",
),
("ghp_AbCdEf1234567890AbCdEf1234567890", "ghp_"),
("github_pat_11ABCDEFG0123456789_abcdefghij", "github_pat_"),
("aws id AKIAIOSFODNN7EXAMPLE", "AKIAIOSFODNN7"),
(
"Authorization: Bearer abc.def-ghi_jkl012345678901234567890",
"abc.def-ghi",
),
("api_key=9f8e7d6c5b4a32100ffee", "9f8e7d6c"),
("password: \"hunter2hunter2\"", "hunter2hunter2"),
(
"jwt eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abc123def456",
"eyJhbGci",
),
];
for (input, leaked) in cases {
let out = redact_secrets(input);
assert!(
!out.contains(leaked),
"secret fragment {leaked:?} survived: {out}"
);
assert!(out.contains("[REDACTED]"), "no redaction marker: {out}");
}
let key = "-----BEGIN RSA PRIVATE KEY-----\nMIIEow…\n-----END RSA PRIVATE KEY-----";
assert!(!redact_secrets(key).contains("MIIEow"));
let cut = "-----BEGIN PRIVATE KEY-----\nMIIEow… (truncated)";
assert!(!redact_secrets(cut).contains("MIIEow"));
}
#[test]
fn redaction_passes_benign_near_misses() {
let benign = [
"the api key is stored in the system keychain",
"the token budget is 4096 tokens per request",
"Bearer of good news: the build is green",
"sk-test was rejected (too short to be a real key)",
"set password: yes in sshd_config",
"AKIAFOO is not a full key id",
"ghp_short",
"the access_token field is documented in docs/api.md",
"run `cargo test -p newt-core` and check the password prompt",
];
for input in benign {
let out = redact_secrets(input);
assert_eq!(out, input, "benign text must pass unchanged");
}
}
#[test]
fn redaction_applies_inside_the_summary_request() {
let middle = vec![tool_result(
"config: api_key=9f8e7d6c5b4a32100ffee and more text",
)];
let request = redact_secrets(&summary_request(
"the task",
&middle,
usize::MAX,
None,
ConvShape::Coding,
));
assert!(!request.contains("9f8e7d6c5b4a32100ffee"), "{request}");
assert!(request.contains("api_key=[REDACTED]"), "{request}");
assert!(request.contains("the task"), "task still present verbatim");
}
#[test]
fn middle_shape_detects_coding_vs_general() {
let coding = vec![serde_json::json!({
"role": "assistant",
"tool_calls": [{"function": {"name": "edit_file", "arguments": "{}"}}],
})];
assert_eq!(middle_shape(&coding), ConvShape::Coding);
let general = vec![
serde_json::json!({"role": "user", "content": "what is a monad?"}),
serde_json::json!({"role": "assistant", "content": "a monoid in ..."}),
];
assert_eq!(middle_shape(&general), ConvShape::General);
}
#[test]
fn general_shape_swaps_the_section_template() {
let coding = summary_prompt_for("t", "body", None, None, 600, ConvShape::Coding);
assert!(coding.contains("## Completed Actions") && coding.contains("## Relevant Files"));
let general = summary_prompt_for("t", "body", None, None, 600, ConvShape::General);
assert!(general.contains("## Discussion") && general.contains("## Open Questions"));
assert!(
!general.contains("## Relevant Files"),
"no file-centric slot for a Q&A middle"
);
assert!(general.contains("## Active Task") && general.contains("## Critical Context"));
assert!(general.starts_with("You are compressing the middle of a conversation."));
}
#[test]
fn redaction_catches_json_quoted_credential_keys() {
let cases = [
(r#"{"api_key": "9f8e7d6c5b4a32100ffee"}"#, "9f8e7d6c"),
(r#"{"password": "hunter2hunter2"}"#, "hunter2hunter2"),
(
r#"body: "client_secret": "abcd1234efgh5678ijkl""#,
"abcd1234",
),
];
for (input, leaked) in cases {
let out = redact_secrets(input);
assert!(
!out.contains(leaked),
"secret fragment {leaked:?} survived: {out}"
);
assert!(out.contains("[REDACTED]"), "no redaction marker: {out}");
}
}
#[test]
fn redaction_survives_excerpt_truncation() {
let secret = "sk-AbCdEf1234567890AbCdEf1234567890";
let args = json!({
"command": format!("{} && export OPENAI_API_KEY={secret}", "x".repeat(140))
});
let m = assistant_call("run_command", args);
let line = render_message(&m);
assert!(!line.contains("sk-AbC"), "{line}");
assert!(!line.contains("AbCdEf123"), "no fragment may leak: {line}");
assert!(line.contains("[REDACTED]"), "{line}");
}
#[test]
fn summary_request_caps_total_middle_size() {
let middle: Vec<Value> = (0..50)
.map(|i| tool_result(&format!("MSG{i} {}", "m".repeat(1_900))))
.collect();
let capped = summary_request("the task", &middle, 8_192, None, ConvShape::Coding);
assert!(
capped.chars().count() < 12_000,
"total must be capped, got {}",
capped.chars().count()
);
assert!(capped.contains("older message(s) omitted"), "{capped:.200}");
assert!(capped.contains("MSG49 "), "most recent middle kept");
assert!(!capped.contains("MSG0 "), "oldest middle dropped");
assert!(capped.contains("the task"), "task always present");
let uncapped = summary_request("the task", &middle, usize::MAX, None, ConvShape::Coding);
assert!(uncapped.chars().count() > 90_000);
assert!(!uncapped.contains("older message(s) omitted"));
}
#[test]
fn chunk_strings_groups_consecutive_within_cap() {
let parts: Vec<String> = ["aaa", "bbb", "ccc", "ddddddd"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(
chunk_strings(&parts, 6),
vec![
"aaabbb".to_string(),
"ccc".to_string(),
"ddddddd".to_string()
]
);
assert_eq!(chunk_strings(&parts, 1_000).len(), 1);
}
#[tokio::test]
async fn summarize_middle_single_request_when_it_fits() {
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "SUMMARY");
let middle = vec![user("alpha"), user("beta")];
let out = summarize_middle(&*s, "do the task", &middle, 100_000, None).await;
assert_eq!(out.as_deref(), Some("SUMMARY"));
assert_eq!(prompts.lock().unwrap().len(), 1, "fits → one request");
}
#[tokio::test]
async fn summarize_middle_chunks_and_reduces_when_over_cap() {
let prompts = Arc::new(Mutex::new(Vec::new()));
let s = recording_summarizer(prompts.clone(), "PART");
let big = "x".repeat(1_000);
let middle: Vec<Value> = (0..6).map(|_| user(&big)).collect();
let out = summarize_middle(&*s, "do the task", &middle, 2_500, None).await;
assert_eq!(out.as_deref(), Some("PART"), "result is the reduce output");
let p = prompts.lock().unwrap();
assert!(
p.len() > 1,
"over-cap middle is chunked: {} requests",
p.len()
);
assert!(
p.iter().any(|r| r.contains("[part 1/")),
"chunks carry part labels"
);
assert!(
p.iter().any(|r| r.contains("consolidate")),
"a reduce/consolidation pass ran"
);
assert!(
p.iter().all(|r| r.chars().count() < 2_500 + 2_000),
"each request stays under the cap (+ template)"
);
}
#[tokio::test]
async fn summarize_middle_all_chunks_fail_degrades_to_none() {
let calls = Arc::new(AtomicUsize::new(0));
let s = failing_summarizer(calls.clone());
let big = "x".repeat(1_000);
let middle: Vec<Value> = (0..6).map(|_| user(&big)).collect();
let out = summarize_middle(&*s, "task", &middle, 2_500, None).await;
assert!(out.is_none(), "all chunks failing → None (→ static marker)");
assert!(
calls.load(Ordering::SeqCst) >= 3,
"every chunk was attempted, got {}",
calls.load(Ordering::SeqCst)
);
}
#[test]
fn render_message_includes_calls_and_caps_content() {
let m = assistant_call("read_file", json!({"path": "src/lib.rs"}));
let line = render_message(&m);
assert!(line.starts_with("[assistant] called read_file("), "{line}");
assert!(line.contains("src/lib.rs"), "{line}");
let long = tool_result(&"w".repeat(10_000));
let line = render_message(&long);
assert!(
line.chars().count() < SUMMARY_INPUT_MSG_CAP + 50,
"{}",
line.len()
);
assert!(line.contains('…'));
}
}