use serde_json::Value;
pub(crate) fn is_headless_client(user_agent: Option<&str>) -> bool {
user_agent
.and_then(|ua| ua.split_once('('))
.and_then(|(_, rest)| rest.split_once(')'))
.is_some_and(|(inside, _)| {
inside
.split(',')
.any(|part| part.trim().starts_with("sdk-"))
})
}
fn shorten_from(messages: &mut [Value], from: usize) -> usize {
let mut shortened = 0;
for message in &mut messages[from..] {
let Some(blocks) = message.get_mut("content").and_then(Value::as_array_mut) else {
continue;
};
for block in blocks {
if let Some(ttl) = block.pointer_mut("/cache_control/ttl")
&& ttl.as_str() == Some("1h")
{
*ttl = Value::String("5m".into());
shortened += 1;
}
}
}
shortened
}
const MAX_MARKERS: usize = 4;
fn is_tool_result_turn(message: &Value) -> bool {
message.get("role").and_then(Value::as_str) == Some("user")
&& message
.get("content")
.and_then(Value::as_array)
.is_some_and(|blocks| {
blocks
.iter()
.any(|b| b.get("type").and_then(Value::as_str) == Some("tool_result"))
})
}
fn marker_count(value: Option<&Value>) -> usize {
value.and_then(Value::as_array).map_or(0, |blocks| {
blocks
.iter()
.filter(|b| b.get("cache_control").is_some())
.count()
})
}
fn hour_marker() -> Value {
serde_json::json!({ "type": "ephemeral", "ttl": "1h" })
}
pub(crate) fn apply_loop_ttl(doc: &mut Value, headless: bool) -> usize {
let outside = marker_count(doc.get("system")) + marker_count(doc.get("tools"));
let mut system_shortened = 0;
if headless
&& marker_count(doc.get("system")) >= 2
&& let Some(ttl) = doc
.get_mut("system")
.and_then(Value::as_array_mut)
.and_then(|blocks| {
blocks
.iter_mut()
.rev()
.find(|b| b.get("cache_control").is_some())
})
.and_then(|b| b.pointer_mut("/cache_control/ttl"))
&& ttl.as_str() == Some("1h")
{
*ttl = Value::String("5m".into());
system_shortened = 1;
}
let Some(messages) = doc.get_mut("messages").and_then(Value::as_array_mut) else {
return system_shortened;
};
if headless {
return system_shortened + shorten_from(messages, 0);
}
if !messages.last().is_some_and(is_tool_result_turn) {
return 0;
}
let Some(prompt) = messages.iter().rposition(|m| {
m.get("role").and_then(Value::as_str) == Some("user") && !is_tool_result_turn(m)
}) else {
return 0;
};
let inside: usize = messages
.iter()
.map(|m| marker_count(m.get("content")))
.sum();
let loop_has_hour_marker = messages[prompt + 1..].iter().any(|m| {
m.get("content")
.and_then(Value::as_array)
.is_some_and(|blocks| {
blocks
.iter()
.any(|b| b.pointer("/cache_control/ttl").and_then(Value::as_str) == Some("1h"))
})
});
if !loop_has_hour_marker {
return 0;
}
let Some(last) = messages[prompt]
.get_mut("content")
.and_then(Value::as_array_mut)
.and_then(|blocks| blocks.last_mut())
.and_then(Value::as_object_mut)
else {
return 0;
};
match last.get_mut("cache_control") {
Some(cc) => *cc = hour_marker(),
None if outside + inside < MAX_MARKERS => {
last.insert("cache_control".into(), hour_marker());
}
None => return 0,
}
shorten_from(messages, prompt + 1)
}
#[cfg(test)]
mod tests {
use super::*;
fn loop_request(prompt: &Value) -> Value {
serde_json::json!({
"system": [
{ "type": "text", "text": "a", "cache_control": { "type": "ephemeral", "ttl": "1h" } },
{ "type": "text", "text": "b", "cache_control": { "type": "ephemeral", "ttl": "1h" } }
],
"messages": [
{ "role": "user", "content": prompt },
{ "role": "assistant", "content": [ { "type": "tool_use", "id": "t1", "name": "Bash", "input": {} } ] },
{ "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "t1", "content": "ok",
"cache_control": { "type": "ephemeral", "ttl": "1h" } } ] }
]
})
}
fn prompt_blocks() -> Value {
serde_json::json!([ { "type": "text", "text": "fix the test" } ])
}
#[test]
fn the_loop_is_written_for_five_minutes_and_the_prompt_for_an_hour() {
let mut doc = loop_request(&prompt_blocks());
assert_eq!(apply_loop_ttl(&mut doc, false), 1);
assert_eq!(
doc["messages"][2]["content"][0]["cache_control"]["ttl"],
"5m"
);
assert_eq!(
doc["messages"][0]["content"][0]["cache_control"]["ttl"],
"1h"
);
assert_eq!(doc["messages"][0]["content"][0]["text"], "fix the test");
assert_eq!(
doc["system"][1]["cache_control"]["ttl"], "1h",
"system keeps its lifetime"
);
let mut again = loop_request(&prompt_blocks());
apply_loop_ttl(&mut again, false);
assert_eq!(doc, again, "same request, same bytes (#498)");
let mut plain = loop_request(&serde_json::json!("fix the test"));
let before = plain.clone();
assert_eq!(
apply_loop_ttl(&mut plain, false),
0,
"a string prompt is never rewrapped"
);
assert_eq!(plain, before);
}
#[test]
fn a_turn_that_starts_with_the_prompt_is_left_alone() {
let mut doc = serde_json::json!({ "messages": [ { "role": "user", "content": [
{ "type": "text", "text": "hi", "cache_control": { "type": "ephemeral", "ttl": "1h" } } ] } ] });
let before = doc.clone();
assert_eq!(apply_loop_ttl(&mut doc, false), 0);
assert_eq!(doc, before);
}
#[test]
fn without_a_free_marker_the_loop_keeps_an_hour() {
let mut doc = loop_request(&prompt_blocks());
doc["tools"] = serde_json::json!([ { "name": "x", "cache_control": { "type": "ephemeral", "ttl": "1h" } } ]);
let before = doc.clone();
assert_eq!(
apply_loop_ttl(&mut doc, false),
0,
"four markers in use: nothing changes"
);
assert_eq!(doc, before);
}
#[test]
fn a_headless_client_writes_every_message_marker_for_five_minutes() {
assert!(is_headless_client(Some(
"claude-cli/2.1.316 (external, sdk-cli)"
)));
assert!(!is_headless_client(Some(
"claude-cli/2.1.316 (external, cli)"
)));
assert!(!is_headless_client(None));
let first = || {
serde_json::json!({ "messages": [ { "role": "user", "content": [
{ "type": "text", "text": "hi", "cache_control": { "type": "ephemeral", "ttl": "1h" } } ] } ] })
};
let mut doc = first();
assert_eq!(apply_loop_ttl(&mut doc, true), 1);
assert_eq!(
doc["messages"][0]["content"][0]["cache_control"]["ttl"],
"5m"
);
let mut loop_doc = loop_request(&prompt_blocks());
assert_eq!(
apply_loop_ttl(&mut loop_doc, true),
2,
"message + session system marker"
);
assert!(
loop_doc["messages"][0]["content"][0]
.get("cache_control")
.is_none(),
"no prompt marker without a person to come back"
);
assert_eq!(
loop_doc["system"][0]["cache_control"]["ttl"], "1h",
"the shared static part keeps its hour"
);
assert_eq!(
loop_doc["system"][1]["cache_control"]["ttl"], "5m",
"the session's own part does not"
);
}
}