Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions crates/tui/src/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,16 @@ pub fn write_session_relative_immutable(
uuid::Uuid::new_v4()
));
let publish = (|| -> io::Result<()> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&temp_path)?;
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
// The temporary inode must already be private before any payload
// is written or published through the immutable hard link.
options.mode(0o600);
}
let mut file = options.open(&temp_path)?;
file.write_all(content)?;
file.sync_all()?;
match std::fs::hard_link(&temp_path, &absolute_path) {
Expand Down Expand Up @@ -417,4 +423,36 @@ mod tests {
assert!(write_session_relative_immutable("session-a", &relative, b"payload").is_err());
assert!(!sessions.join("session-a/artifacts/art_failed.txt").exists());
}

#[cfg(unix)]
#[test]
fn forkguard_immutable_artifacts_are_private_and_replay_safe() {
use std::os::unix::fs::PermissionsExt;

let _guard = TEST_ARTIFACT_SESSIONS_GUARD
.lock()
.unwrap_or_else(|err| err.into_inner());
let tmp = tempfile::tempdir().unwrap();
let _root = set_test_sessions_root(tmp.path().join("sessions"));
let relative = PathBuf::from("artifacts/art_private.txt");
let bytes = b"private complete output";
let path = write_session_relative_immutable("private-session", &relative, bytes).unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().permissions().mode() & 0o077,
0
);
assert_eq!(std::fs::read(&path).unwrap(), bytes);
assert_eq!(
write_session_relative_immutable("private-session", &relative, bytes).unwrap(),
path
);
assert!(
write_session_relative_immutable("private-session", &relative, b"changed").is_err()
);
assert_eq!(std::fs::read(&path).unwrap(), bytes);
assert_eq!(
std::fs::read_dir(path.parent().unwrap()).unwrap().count(),
1
);
}
}
25 changes: 15 additions & 10 deletions crates/tui/src/client/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1995,15 +1995,17 @@ fn push_text_part(parts: &mut Vec<Value>, text: &str) {

pub(crate) const CACHE_WARMUP_USER_TAIL: &str = "请只回复 OK";
pub(crate) const CACHE_WARMUP_MAX_TOKENS: u32 = 8;
const TOOL_RESULT_SENT_CHAR_BUDGET: usize = 12_000;

/// Wire backstop for tool results (#6508). The engine already fits every
/// result it gives the model to the route's inline budget, and marks any cut
/// with a recovery footer. This pass only catches history that never went
/// through the engine (legacy or restored raw results), so it uses the
/// largest value that budget can take — it never cuts a result the engine
/// kept whole.
fn tool_result_sent_char_budget() -> usize {
crate::tools::large_output_router::WorkshopConfig::active_tool_result_max_bytes()
.map(|bytes| bytes.clamp(TOOL_RESULT_SENT_CHAR_BUDGET, 2 * 1024 * 1024))
.unwrap_or(TOOL_RESULT_SENT_CHAR_BUDGET)
crate::route_budget::route_inline_char_budget(None)
}
const TOOL_RESULT_HEAD_CHARS: usize = 4_000;
const TOOL_RESULT_TAIL_CHARS: usize = 4_000;
/// Characters of an excerpted wire result spent on its labelled header.
const TOOL_RESULT_EXCERPT_FRAME_CHARS: usize = 1_024;
/// Tool results shorter than this stay inline even when repeated. The
/// extra prompt bytes are cheaper than adding an earlier-message reference
/// for tiny command outputs.
Expand Down Expand Up @@ -2521,7 +2523,8 @@ fn compact_tool_result_for_wire(
// a back-reference would falsely imply the exact bytes remain available.
let sent_budget = tool_result_sent_char_budget();
let dedup_eligible = (TOOL_RESULT_DEDUP_MIN_CHARS..=sent_budget).contains(&original_chars)
&& !is_mutation_tool(tool_name);
&& !is_mutation_tool(tool_name)
&& !content.contains(crate::tools::truncate::SPILLOVER_RECOVERY_HINT);

if dedup_eligible && let Some(previous) = seen_tool_results.get(&sha) {
let content = format!(
Expand Down Expand Up @@ -2575,8 +2578,10 @@ fn compact_tool_result_for_wire(
};
}

let head = first_chars(content, TOOL_RESULT_HEAD_CHARS);
let tail = last_chars(content, TOOL_RESULT_TAIL_CHARS);
let excerpt_chars = sent_budget.saturating_sub(TOOL_RESULT_EXCERPT_FRAME_CHARS);
let head_chars = excerpt_chars * 2 / 3;
let head = first_chars(content, head_chars);
let tail = last_chars(content, excerpt_chars - head_chars);
let kept = head.chars().count() + tail.chars().count();
let omitted = original_chars.saturating_sub(kept);
let compacted = format!(
Expand Down
53 changes: 41 additions & 12 deletions crates/tui/src/client/chat/tests/stream_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,8 @@ fn cache_inspect_reports_turn_meta_dedup_metadata() {

#[test]
fn request_builder_truncates_large_tool_result_for_wire() {
let long_output = format!("{}{}", "A".repeat(7_000), "Z".repeat(7_000));
let _workshop = crate::tools::large_output_router::active_workshop_test_guard();
let long_output = format!("{}{}", "A".repeat(70_000), "Z".repeat(70_000));
let messages = vec![
tool_use_message(
"tool-long",
Expand All @@ -1190,18 +1191,27 @@ fn request_builder_truncates_large_tool_result_for_wire() {
assert!(sent.contains("[TOOL_RESULT_TRUNCATED]"), "got: {sent}");
assert!(sent.contains("tool_name: shell_command"), "got: {sent}");
assert!(sent.contains("command_or_query: cargo test"), "got: {sent}");
assert!(sent.contains("original_chars: 14000"), "got: {sent}");
assert!(sent.contains("original_chars: 140000"), "got: {sent}");
assert!(sent.contains("sha256:"), "got: {sent}");
assert!(
sent.contains("exact_detail: unavailable; no session-owned artifact was recorded"),
"got: {sent}"
);
assert!(!sent.contains("retrieve_tool_result"), "got: {sent}");
assert!(sent.contains(&"A".repeat(4_000)), "got: {sent}");
assert!(sent.contains(&"Z".repeat(4_000)), "got: {sent}");
// The wire backstop is the ceiling of the one inline budget (#6508):
// a result the engine kept whole is never cut here.
let budget = tool_result_sent_char_budget();
assert!(budget >= 100_000);
let excerpt = budget - TOOL_RESULT_EXCERPT_FRAME_CHARS;
let head = excerpt * 2 / 3;
assert!(sent.contains(&"A".repeat(head)), "head kept");
assert!(sent.contains(&"Z".repeat(excerpt - head)), "tail kept");
assert!(
sent.contains("truncated 6000 chars from middle"),
"got: {sent}"
sent.contains(&format!(
"truncated {} chars from middle",
140_000 - excerpt
)),
"omitted count"
);
assert_ne!(sent, long_output);
}
Expand Down Expand Up @@ -1231,7 +1241,7 @@ fn request_builder_keeps_unowned_extreme_tool_output_bounded_without_false_hint(
assert!(sent.contains("exact_detail: unavailable"), "got: {sent}");
assert!(!sent.contains("retrieve_tool_result"), "got: {sent}");
assert!(
sent.chars().count() <= TOOL_RESULT_SENT_CHAR_BUDGET,
sent.chars().count() <= tool_result_sent_char_budget(),
"truncated result should stay bounded, sent {} chars",
sent.chars().count()
);
Expand Down Expand Up @@ -1266,7 +1276,7 @@ fn request_builder_does_not_dedup_short_tool_results_for_wire() {
fn request_builder_deduplicates_medium_identical_tool_results_to_earlier_message() {
with_tool_result_sha_spillover_root(|| {
// 2,000 chars is intentionally above TOOL_RESULT_DEDUP_MIN_CHARS
// (1,024) but below TOOL_RESULT_SENT_CHAR_BUDGET (12,000). This
// (1,024) but below the wire backstop budget. This
// verifies the cache-saving path for repeated medium outputs that
// do not otherwise need truncation.
let output = "A".repeat(2_000);
Expand Down Expand Up @@ -1300,6 +1310,25 @@ fn request_builder_deduplicates_medium_identical_tool_results_to_earlier_message
});
}

#[test]
fn request_builder_never_deduplicates_unsaved_output_previews() {
let _workshop = crate::tools::large_output_router::active_workshop_test_guard();
let output = format!(
"{}\n[the full output could not be saved; {}]",
"preview\n".repeat(200),
crate::tools::truncate::unsaved_output_recovery_instruction(),
);
let messages = vec![
tool_use_message("tool-1", "bash", json!({"command": "probe"})),
tool_result_message("tool-1", &output),
tool_use_message("tool-2", "bash", json!({"command": "probe"})),
tool_result_message("tool-2", &output),
];
let built = build_chat_messages(None, &messages, "deepseek-v4-flash");
assert_eq!(tool_message_content(&built, 0), output);
assert_eq!(tool_message_content(&built, 1), output);
}

#[test]
fn request_builder_never_dedups_large_identical_write_file_confirmations() {
with_tool_result_sha_spillover_root(|| {
Expand Down Expand Up @@ -1351,7 +1380,7 @@ fn large_unowned_results_stay_bounded_without_false_retrieval_handles() {
// session-owned artifact receipt before this provider-wire fallback.
// If legacy/raw history reaches here, it may be excerpted but must not
// advertise the process-wide SHA store as retrievable.
let big_diff = "D".repeat(20_000);
let big_diff = "D".repeat(120_000);
let sha = sha256_hex(big_diff.as_bytes());

let messages = vec![
Expand Down Expand Up @@ -1406,7 +1435,7 @@ fn large_unowned_results_stay_bounded_without_false_retrieval_handles() {

#[test]
fn tool_result_budget_is_wire_only_and_does_not_mutate_session_message() {
let long_output = format!("{}{}", "A".repeat(7_000), "Z".repeat(7_000));
let long_output = format!("{}{}", "A".repeat(70_000), "Z".repeat(70_000));
let messages = vec![
tool_use_message(
"tool-long",
Expand All @@ -1428,7 +1457,7 @@ fn tool_result_budget_is_wire_only_and_does_not_mutate_session_message() {

#[test]
fn cache_inspect_reports_bounded_unowned_tool_result_metadata() {
let long_output = format!("{}{}", "A".repeat(7_000), "Z".repeat(7_000));
let long_output = format!("{}{}", "A".repeat(70_000), "Z".repeat(70_000));
let request = MessageRequest {
model: "deepseek-v4-flash".to_string(),
messages: vec![
Expand Down Expand Up @@ -1458,7 +1487,7 @@ fn cache_inspect_reports_bounded_unowned_tool_result_metadata() {

assert_eq!(tool_layers.len(), 2);
for layer in tool_layers {
assert_eq!(layer.original_chars, 14_000);
assert_eq!(layer.original_chars, 140_000);
assert!(layer.sent_chars < layer.original_chars);
assert!(layer.truncated);
assert!(!layer.deduplicated);
Expand Down
131 changes: 78 additions & 53 deletions crates/tui/src/commands/groups/debug/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,60 +725,85 @@ fn cache_inspect_reports_divergence_from_previous_request() {

#[test]
fn cache_inspect_displays_tool_result_budget_metadata() {
let mut app = create_test_app();
let long_output = format!("{}{}", "A".repeat(7_000), "Z".repeat(7_000));
app.api_messages.push(Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "tool-1".to_string(),
name: "shell_command".to_string(),
input: serde_json::json!({"command": "cargo test"}),
caller: None,
thought_signature: None,
}],
});
app.api_messages.push(Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: "tool-1".to_string(),
content: long_output.clone(),
is_error: None,
content_blocks: None,
}],
});
app.api_messages.push(Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "tool-2".to_string(),
name: "shell_command".to_string(),
input: serde_json::json!({"command": "cargo test"}),
caller: None,
thought_signature: None,
}],
});
app.api_messages.push(Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: "tool-2".to_string(),
content: long_output,
is_error: None,
content_blocks: None,
}],
});

let result = cache(&mut app, Some("inspect"));
let msg = result.message.expect("inspect output");

let tool_budget_lines: Vec<_> = msg
.lines()
.filter(|line| line.contains("original_chars=14000"))
.collect();
assert_eq!(tool_budget_lines.len(), 2, "got: {msg}");
let budget = crate::route_budget::route_inline_char_budget(None);
for output_chars in [14_000, budget, budget + 1] {
let mut app = create_test_app();
let long_output = format!("{}Z", "A".repeat(output_chars - 1));
app.api_messages.push(Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "tool-1".to_string(),
name: "shell_command".to_string(),
input: serde_json::json!({"command": "cargo test"}),
caller: None,
thought_signature: None,
}],
});
app.api_messages.push(Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: "tool-1".to_string(),
content: long_output.clone(),
is_error: None,
content_blocks: None,
}],
});
app.api_messages.push(Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "tool-2".to_string(),
name: "shell_command".to_string(),
input: serde_json::json!({"command": "cargo test"}),
caller: None,
thought_signature: None,
}],
});
app.api_messages.push(Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: "tool-2".to_string(),
content: long_output,
is_error: None,
content_blocks: None,
}],
});

for sighting in tool_budget_lines {
assert!(sighting.contains("sent_chars="), "got: {msg}");
assert!(sighting.contains("truncated=true"), "got: {msg}");
assert!(sighting.contains("deduplicated=false"), "got: {msg}");
let result = cache(&mut app, Some("inspect"));
let msg = result.message.expect("inspect output");

let tool_budget_lines: Vec<_> = msg
.lines()
.filter(|line| line.contains(&format!("original_chars={output_chars}")))
.collect();
assert_eq!(tool_budget_lines.len(), 2, "got: {msg}");

for (index, sighting) in tool_budget_lines.iter().enumerate() {
let sent_chars: usize = sighting
.split("sent_chars=")
.nth(1)
.expect("sent count")
.split_whitespace()
.next()
.expect("count")
.trim_end_matches(',')
.parse()
.expect("numeric count");
assert!(sent_chars <= budget, "got: {msg}");
assert!(
sighting.contains(&format!("truncated={}", output_chars > budget)),
"got: {msg}"
);
let deduplicated = index == 1 && output_chars <= budget;
assert!(
sighting.contains(&format!("deduplicated={deduplicated}")),
"got: {msg}"
);
if output_chars <= budget && !deduplicated {
assert_eq!(sent_chars, output_chars, "got: {msg}");
} else {
assert!(sent_chars < output_chars, "got: {msg}");
}
}
}
}

Expand Down
Loading
Loading