use serde_json::Value;
use crate::cvm_store::{build_stub, CvmStore};
use crate::digest::{Digestor, SkeletonDigestor, DEFAULT_DIGEST_THRESHOLD_TOKENS};
pub fn choose_ttl(long_gap_count: u32, threshold: u32) -> Option<&'static str> {
(long_gap_count >= threshold).then_some("1h")
}
pub fn next_long_gap_count(
last_ts: u64,
now_unix: u64,
count: u32,
gap_threshold_secs: u64,
) -> u32 {
if last_ts > 0 && now_unix.saturating_sub(last_ts) > gap_threshold_secs {
count + 1
} else {
count
}
}
pub fn frozen_fingerprint(frozen: &[Value]) -> (usize, String) {
use sha2::{Digest, Sha256};
let bytes = serde_json::to_vec(frozen).unwrap_or_default();
(frozen.len(), format!("{:x}", Sha256::digest(&bytes)))
}
pub fn is_genuine_break(prev_len: usize, prev_hash: &str, frozen: &[Value]) -> bool {
if frozen.len() < prev_len {
return true; }
let (_, head_hash) = frozen_fingerprint(&frozen[..prev_len]);
head_hash != prev_hash
}
pub fn set_newest_cache_ttl(messages: &mut [Value], ttl: &str) -> bool {
for msg in messages.iter_mut().rev() {
let Some(content) = msg.get_mut("content").and_then(Value::as_array_mut) else {
continue;
};
for block in content.iter_mut().rev() {
if let Some(cc) = block.get_mut("cache_control").and_then(Value::as_object_mut) {
cc.insert("ttl".to_string(), Value::String(ttl.to_string()));
return true;
}
}
}
false
}
pub(crate) fn tool_result_text(block: &Value) -> Option<String> {
if block.get("type").and_then(Value::as_str) != Some("tool_result") {
return None;
}
match block.get("content") {
Some(Value::String(s)) if !s.is_empty() => Some(s.clone()),
Some(Value::Array(parts)) => {
if parts
.iter()
.any(|p| p.get("type").and_then(Value::as_str) != Some("text"))
{
return None;
}
let text: String = parts
.iter()
.filter_map(|p| p.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join("\n");
(!text.is_empty()).then_some(text)
}
_ => None,
}
}
fn token_estimate(text: &str) -> usize {
text.split_whitespace().count()
}
fn stub_replacement(page_id: &str, session_id: &str, text: &str) -> String {
let orig_tokens = token_estimate(text);
let budget = ((orig_tokens as f64) * 0.15).round() as usize;
let stub = build_stub(page_id, session_id, orig_tokens, "tool_result", text);
let digest = SkeletonDigestor.digest(text, budget);
format!("{stub}\n{digest}\n[AXIOM-PAGE-END expand with axiom_expand(symbol=\"{page_id}\", session_id=\"{session_id}\")]")
}
fn is_already_stubbed(text: &str) -> bool {
text.trim_start().starts_with("[AXIOM-PAGE ")
}
fn rewrite_heavy_blocks(msg: &Value, replace: &mut dyn FnMut(&str) -> Option<String>) -> Value {
let Some(content) = msg.get("content").and_then(Value::as_array) else {
return msg.clone();
};
let mut changed = false;
let new_content: Vec<Value> = content
.iter()
.map(|block| match tool_result_text(block) {
Some(text)
if token_estimate(&text) >= DEFAULT_DIGEST_THRESHOLD_TOKENS
&& !is_already_stubbed(&text) =>
{
match replace(&text) {
Some(replacement) => {
let mut b = block.clone();
if let Some(obj) = b.as_object_mut() {
obj.insert("content".to_string(), Value::String(replacement));
}
changed = true;
b
}
None => block.clone(),
}
}
_ => block.clone(),
})
.collect();
if changed {
let mut m = msg.clone();
if let Some(obj) = m.as_object_mut() {
obj.insert("content".to_string(), Value::Array(new_content));
}
m
} else {
msg.clone()
}
}
pub fn rebase_transcript(messages: &[Value], store: &CvmStore, session_id: &str) -> Vec<Value> {
if messages.len() <= 1 {
return messages.to_vec();
}
let newest_idx = messages.len() - 1;
messages
.iter()
.enumerate()
.map(|(i, msg)| {
if i == newest_idx {
return msg.clone(); }
rewrite_heavy_blocks(msg, &mut |text| match store
.put(session_id, "tool_result", text)
{
Ok(page_id) => Some(stub_replacement(&page_id, session_id, text)),
Err(e) => {
eprintln!("[axiom-pss] rebase store.put failed: {e}");
None
}
})
})
.collect()
}
pub fn reapply_stubs(
messages: &[Value],
store: &CvmStore,
session_id: &str,
protect_last: bool,
) -> Vec<Value> {
let protected_idx = if protect_last && !messages.is_empty() {
Some(messages.len() - 1)
} else {
None
};
messages
.iter()
.enumerate()
.map(|(i, msg)| {
if Some(i) == protected_idx {
return msg.clone();
}
rewrite_heavy_blocks(msg, &mut |text| {
let page_id = CvmStore::page_id_for(text);
store
.get(session_id, &page_id)
.map(|_| stub_replacement(&page_id, session_id, text))
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn tempdir(tag: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!(
"pss-rebase-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn choose_ttl_picks_1h_only_after_repeated_long_gaps() {
assert_eq!(choose_ttl(0, 3), None);
assert_eq!(choose_ttl(2, 3), None);
assert_eq!(choose_ttl(3, 3), Some("1h"));
assert_eq!(choose_ttl(9, 3), Some("1h"));
}
#[test]
fn set_newest_cache_ttl_annotates_only_the_last_breakpoint() {
let mut messages = vec![
json!({"role":"user","content":[
{"type":"text","text":"old","cache_control":{"type":"ephemeral"}}]}),
json!({"role":"user","content":[
{"type":"text","text":"a"},
{"type":"text","text":"newest","cache_control":{"type":"ephemeral"}}]}),
];
assert!(set_newest_cache_ttl(&mut messages, "1h"));
assert_eq!(messages[1]["content"][1]["cache_control"]["ttl"], json!("1h"));
assert!(messages[0]["content"][0]["cache_control"].get("ttl").is_none());
}
#[test]
fn appending_to_the_frozen_prefix_is_not_a_break() {
let turn1 = vec![json!({"role":"user","content":"a"})];
let (len1, hash1) = frozen_fingerprint(&turn1);
let turn2 = vec![
json!({"role":"user","content":"a"}),
json!({"role":"assistant","content":"b"}),
];
assert!(!is_genuine_break(len1, &hash1, &turn2), "append-only growth is not a break");
assert!(!is_genuine_break(len1, &hash1, &turn1), "unchanged prefix is not a break");
}
#[test]
fn shrinking_or_mutating_the_frozen_prefix_is_a_break() {
let prev = vec![
json!({"role":"user","content":"a"}),
json!({"role":"assistant","content":"b"}),
];
let (len, hash) = frozen_fingerprint(&prev);
let shrunk = vec![json!({"role":"user","content":"summary of a+b"})];
assert!(is_genuine_break(len, &hash, &shrunk), "a shrunken prefix is a break");
let mutated = vec![
json!({"role":"user","content":"a CHANGED"}),
json!({"role":"assistant","content":"b"}),
];
assert!(is_genuine_break(len, &hash, &mutated), "a mutated prefix is a break");
}
#[test]
fn multipart_tool_result_is_joined_with_a_separator() {
let block = json!({
"type":"tool_result","tool_use_id":"a",
"content":[{"type":"text","text":"alpha"},{"type":"text","text":"beta"}]
});
let text = tool_result_text(&block).unwrap();
assert_eq!(text, "alpha\nbeta", "parts keep a boundary");
assert_eq!(token_estimate(&text), 2);
}
#[test]
fn next_long_gap_count_increments_only_past_the_window() {
assert_eq!(next_long_gap_count(0, 10_000, 0, 240), 0);
assert_eq!(next_long_gap_count(1_000, 1_100, 2, 240), 2);
assert_eq!(next_long_gap_count(1_000, 1_500, 2, 240), 3);
assert_eq!(next_long_gap_count(1_000, 1_240, 0, 240), 0);
}
#[test]
fn set_newest_cache_ttl_is_false_without_a_breakpoint() {
let mut messages = vec![json!({"role":"user","content":[{"type":"text","text":"no cc"}]})];
assert!(!set_newest_cache_ttl(&mut messages, "1h"));
}
#[test]
fn rebase_digests_all_old_heavy_but_never_the_newest_turn() {
let dir = tempdir("basic");
let store = CvmStore::open(&dir).unwrap();
let big = "x ".repeat(9000); let messages = vec![
json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big}]}),
json!({"role":"user","content":"newest turn stays whole"}),
];
let out = rebase_transcript(&messages, &store, "s1");
assert!(out[0].to_string().contains("AXIOM-PAGE"), "old heavy digested");
assert!(!out[0].to_string().contains(&"x ".repeat(9000)), "raw text removed");
assert_eq!(out[1]["content"], json!("newest turn stays whole"), "newest untouched");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn rebase_leaves_light_tool_results_alone() {
let dir = tempdir("light");
let store = CvmStore::open(&dir).unwrap();
let messages = vec![
json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content":"exit 0"}]}),
json!({"role":"user","content":"newest"}),
];
let out = rebase_transcript(&messages, &store, "s1");
assert_eq!(out[0], messages[0], "sub-threshold tool_result untouched");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn rebase_original_is_recoverable_from_the_store() {
let dir = tempdir("recover");
let store = CvmStore::open(&dir).unwrap();
let big = "y ".repeat(9000);
let messages = vec![
json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big.clone()}]}),
json!({"role":"user","content":"newest"}),
];
let _ = rebase_transcript(&messages, &store, "s1");
let page_id = CvmStore::page_id_for(&big);
assert_eq!(store.get("s1", &page_id), Some(big));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn rebase_single_message_is_a_noop() {
let dir = tempdir("single");
let store = CvmStore::open(&dir).unwrap();
let big = "z ".repeat(9000);
let messages = vec![json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big}]})];
let out = rebase_transcript(&messages, &store, "s1");
assert_eq!(out, messages, "a lone newest turn is never rebased");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reapply_stubs_reproduces_rebase_transcript_byte_for_byte() {
let dir = tempdir("reapply-parity");
let store = CvmStore::open(&dir).unwrap();
let big = "p ".repeat(9000);
let original = vec![
json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big.clone()}]}),
json!({"role":"assistant","content":"ack"}),
];
let rebased = rebase_transcript(&original, &store, "s1");
let replayed = reapply_stubs(&original, &store, "s1", false);
assert_eq!(replayed, rebased, "reapply must match the original rebase exactly");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reapply_stubs_never_touches_an_unpaged_block() {
let dir = tempdir("reapply-unpaged");
let store = CvmStore::open(&dir).unwrap();
let big = "q ".repeat(9000);
let messages = vec![json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big.clone()}]})];
let out = reapply_stubs(&messages, &store, "s1", false);
assert_eq!(out, messages, "unpaged heavy block is left untouched");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reapply_stubs_protects_the_last_message_when_requested() {
let dir = tempdir("reapply-protect-last");
let store = CvmStore::open(&dir).unwrap();
let big = "r ".repeat(9000);
let original = vec![json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big.clone()}]})];
let mut two = original.clone();
two.push(json!({"role":"assistant","content":"ack"}));
let _ = rebase_transcript(&two, &store, "s1");
let out = reapply_stubs(&original, &store, "s1", true);
assert_eq!(out, original, "protect_last shields the final message");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reapply_stubs_is_idempotent_on_an_already_stubbed_transcript() {
let dir = tempdir("reapply-idempotent");
let store = CvmStore::open(&dir).unwrap();
let big = "s ".repeat(9000);
let original = vec![
json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big.clone()}]}),
json!({"role":"assistant","content":"ack"}),
];
let once = reapply_stubs(&rebase_transcript(&original, &store, "s1"), &store, "s1", false);
let twice = reapply_stubs(&once, &store, "s1", false);
assert_eq!(once, twice, "reapplying to an already-stubbed transcript is a no-op");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reapply_with_different_session_leaves_stubbed_blocks_byte_identical() {
let dir = tempdir("reapply-cross-session");
let store = CvmStore::open(&dir).unwrap();
let big = "t ".repeat(9000);
let original = vec![
json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big.clone()}]}),
json!({"role":"assistant","content":"ack"}),
];
let rebased = rebase_transcript(&original, &store, "s1");
let stub_text = rebased[0]["content"][0]["content"].as_str().unwrap();
assert!(
stub_text.contains("session=\"s1\""),
"rebased stub must carry the storing session"
);
let reapplied = reapply_stubs(&rebased, &store, "s2", false);
assert_eq!(
reapplied, rebased,
"cross-session reapply must not rewrite stub bytes"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn rebase_transcript_paging_the_same_text_twice_does_not_duplicate_store_rows() {
let dir = tempdir("no-dup-rows");
let store = CvmStore::open(&dir).unwrap();
let big = "t ".repeat(9000);
let messages = vec![
json!({"role":"user","content":[
{"type":"tool_result","tool_use_id":"a","content": big.clone()}]}),
json!({"role":"assistant","content":"ack"}),
];
let first = rebase_transcript(&messages, &store, "s1");
let second = rebase_transcript(&messages, &store, "s1");
assert_eq!(first, second, "re-paging identical content is deterministic");
let page_id = CvmStore::page_id_for(&big);
assert_eq!(store.get("s1", &page_id), Some(big));
let row_count = std::fs::read_to_string(store.session_path_for_test("s1"))
.unwrap()
.lines()
.filter(|l| !l.trim().is_empty())
.count();
assert_eq!(row_count, 1, "identical content paged twice must add exactly one row");
let _ = std::fs::remove_dir_all(&dir);
}
}