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
25 changes: 19 additions & 6 deletions crates/zeroclaw-channels/src/orchestrator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -981,20 +981,33 @@ fn timestamp_channel_user_content(content: &str) -> String {
}

fn channel_history_content_for_user_turn(content: &str) -> String {
let (cleaned, image_refs) = zeroclaw_providers::multimodal::parse_image_markers(content);
let (_cleaned, image_refs) = zeroclaw_providers::multimodal::parse_image_markers(content);
if image_refs.is_empty() {
return content.to_string();
}

let mut cleaned = cleaned.trim().to_string();
while cleaned.contains("\n\n\n") {
cleaned = cleaned.replace("\n\n\n", "\n\n");
// Only strip inline base64 data URIs from history; keep filesystem path
// references so they can be re-loaded on later turns. This fixes the issue
// where deferred image attachments lose their re-loadable reference.
// See issue #8151.
let mut result = content.to_string();
for ref_path in &image_refs {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Same issue as #8208: this is a production behavior change to channel_history_content_for_user_turn, not a test, and it duplicates #8167 (which owns this exact image-marker history fix for the now-CLOSED #8151). This hunk was added after @Audacity88's review of the one-file test patch, so it is unreviewed scope creep. Drop it; let #8167 be the single home for the fix.

if ref_path.starts_with("data:") {
// Strip inline base64 payload (too large to keep in history)
result = result.replace(&format!("[IMAGE:{}]", ref_path), "");
}
// Filesystem paths and URLs are kept as-is for re-loading
}

let mut result = result.trim().to_string();
while result.contains("\n\n\n") {
result = result.replace("\n\n\n", "\n\n");
}

if cleaned.is_empty() {
if result.is_empty() {
"[Image attachment processed by vision model]".to_string()
} else {
cleaned
result
}
}

Expand Down
10 changes: 9 additions & 1 deletion crates/zeroclaw-providers/src/openrouter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,8 @@ mod tests {

#[test]
fn chat_request_serializes_with_system_and_user() {
let request = OpenRouterModelProvider::build_chat_with_system_request(
let provider = OpenRouterModelProvider::new("test", Some("key"), None);
let request = provider.build_chat_with_system_request(
Some("You are helpful"),
"Summarize this",
"anthropic/claude-sonnet-4",
Expand Down Expand Up @@ -1279,6 +1280,7 @@ mod tests {

let request = ChatRequest {
model: "google/gemini-2.5-pro".into(),
models: None,
messages: messages
.iter()
.map(|msg| Message {
Expand Down Expand Up @@ -1975,6 +1977,7 @@ mod tests {
let model_provider = OpenRouterModelProvider::new("test", Some("key"), None);
let request = ChatRequest {
model: "test-model".into(),
models: None,
messages: vec![],
temperature: Some(0.5),
max_tokens: None,
Expand All @@ -1991,6 +1994,7 @@ mod tests {
.with_extra_body(serde_json::json!({}));
let request = ChatRequest {
model: "test-model".into(),
models: None,
messages: vec![],
temperature: Some(0.5),
max_tokens: None,
Expand All @@ -2007,6 +2011,7 @@ mod tests {
.with_extra_body(serde_json::json!({"model_provider": {"only": ["Anthropic"]}}));
let request = ChatRequest {
model: "test-model".into(),
models: None,
messages: vec![],
temperature: Some(0.5),
max_tokens: None,
Expand All @@ -2028,6 +2033,7 @@ mod tests {
.with_extra_body(serde_json::json!({"temperature": 0.9}));
let request = ChatRequest {
model: "test-model".into(),
models: None,
messages: vec![],
temperature: Some(0.5),
max_tokens: None,
Expand All @@ -2044,6 +2050,7 @@ mod tests {
.with_extra_body(serde_json::json!({"transforms": ["middle-out"]}));
let request = ChatRequest {
model: "test-model".into(),
models: None,
messages: vec![],
temperature: Some(0.5),
max_tokens: None,
Expand All @@ -2065,6 +2072,7 @@ mod tests {
);
let request = NativeChatRequest {
model: "anthropic/claude-sonnet-4".into(),
models: None,
messages: vec![],
temperature: Some(0.7),
tools: None,
Expand Down
35 changes: 34 additions & 1 deletion src/commands/self_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ async fn check_websocket_handshake(config: &crate::config::Config) -> CheckResul

#[cfg(test)]
mod tests {
use super::{format_probe_url, resolve_probe_host, web_dist_dir_expansion_reason_key};
use super::{check_sqlite, check_workspace, format_probe_url, resolve_probe_host, web_dist_dir_expansion_reason_key};

#[test]
fn web_dist_dir_with_tilde_resolves_to_tilde_reason_key() {
Expand Down Expand Up @@ -585,4 +585,37 @@ mod tests {
"ws://127.0.0.1:42617/ws/chat"
);
}

#[test]
fn check_sqlite_with_nonexistent_workspace_dir() {
let temp_dir = tempfile::tempdir().unwrap();
// Pass a nonexistent subdirectory as workspace_dir; the function appends "memory.db"
let workspace_dir = temp_dir.path().join("nonexistent_workspace");
let result = check_sqlite(&workspace_dir);
// The parent dir does not exist, so SQLite cannot create the file
assert!(!result.passed, "nonexistent workspace directory should fail; got: {}", result.detail);
assert!(
result.detail.contains("cannot open"),
"detail should mention open failure, got: {}",
result.detail
);
}

#[tokio::test]
async fn check_workspace_with_valid_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let result = check_workspace(temp_dir.path()).await;
assert!(result.passed, "valid writable directory should pass");
assert!(result.detail.contains("writable"));
}

#[tokio::test]
async fn check_workspace_with_file_instead_of_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("not_a_dir.txt");
std::fs::write(&file_path, "test").unwrap();
let result = check_workspace(&file_path).await;
assert!(!result.passed, "file instead of directory should fail");
assert!(result.detail.contains("is not a directory"));
}
}