{"record":{"id":"5a18d812b6907de5","repo":"zeroclaw-labs/zeroclaw","slug":"attachment-path-not-found","errorCode":null,"errorMessage":"attachment path not found: {}","messagePattern":"attachment path not found: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/wechat.rs","lineNumber":1231,"sourceCode":"            file_name: self.remote_file_name(url, content_type.as_deref(), kind),\n            bytes,\n        })\n    }\n\n    async fn load_attachment_payload(\n        &self,\n        attachment: &WeChatAttachment,\n    ) -> anyhow::Result<WeChatMediaPayload> {\n        let target = attachment.target.trim();\n        if is_remote_url(target) {\n            return self\n                .download_remote_attachment(target, attachment.kind)\n                .await;\n        }\n\n        let path = self.resolve_local_attachment_path(target)?;\n        if !path.exists() {\n            anyhow::bail!(\"attachment path not found: {}\", path.display());\n        }\n\n        let file_name = sanitize_attachment_filename(\n            path.file_name()\n                .and_then(|name| name.to_str())\n                .unwrap_or(\"attachment.bin\"),\n        )\n        .unwrap_or_else(|| {\n            format!(\n                \"wechat_attachment_{}.{}\",\n                uuid::Uuid::new_v4().simple(),\n                attachment.kind.default_extension()\n            )\n        });\n\n        let bytes = tokio::fs::read(&path)\n            .await\n            .with_context(|| format!(\"attachment read failed: {}\", path.display()))?;","sourceCodeStart":1213,"sourceCodeEnd":1249,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wechat.rs#L1213-L1249","documentation":"`load_attachment_payload` resolved a local attachment target to a workspace path successfully (the sandbox check passed — note `canonicalize_within_workspace` deliberately returns non-existent candidates unchanged), but the path does not exist on disk at send time. The message prints the fully resolved absolute path so you can see exactly where the channel looked.","triggerScenarios":"Sending a WeChat attachment whose target resolves inside `workspace_dir` but names a missing file: wrong filename/extension, file deleted between generation and send, attachment path produced by another machine/container, or the workspace root mounted at a different location. Fires for relative targets (`report.png` when `/workspace/report.png` is absent), `/workspace/...` targets, and in-workspace absolute targets alike.","commonSituations":"Agent writes a file to a temp directory and passes that name as if it were in the workspace; filename mismatches (`.jpeg` vs `.jpg`, URL-encoded or spaced names); race where cleanup deletes the artifact before the send; Docker deployments where the generator and the channel see different volumes; typo'd attachment targets in message templates.","solutions":["Check the printed resolved path: compare it with where the file actually is, then fix the target (usually switch to the correct workspace-relative name).","If the file lives elsewhere, move/copy it into `workspace_dir` first, or fix the `workspace_dir` channel configuration to the real workspace root.","Guard against races: write attachment files to stable paths and send immediately, or verify existence right before send.","For files produced by external processes, pass absolute paths that are already inside the workspace so resolution is unambiguous."],"exampleFix":"// before: assuming the renderer's output path is the attachment target\nlet attachment = WeChatAttachment { target: \"/tmp/chart.png\".into(), .. }; // resolves? no: /tmp is outside workspace\nlet attachment = WeChatAttachment { target: \"char.png\".into(), .. }; // typo -> attachment path not found: /workspace/char.png\n\n// after: write into the workspace and verify before sending\nlet path = workspace.join(\"chart.png\");\nrenderer.render(&path).await?;\nassert!(path.exists(), \"renderer produced no output\");\nlet attachment = WeChatAttachment { target: \"chart.png\".into(), .. };","handlingStrategy":"validation","validationCode":"// existence check inside the workspace before send\nasync fn attachment_ready(workspace: &std::path::Path, rel: &str) -> bool {\n    let p = workspace.join(rel);\n    p.is_file() // resolves symlinks like the channel will\n}\nif !attachment_ready(workspace, \"chart.png\").await {\n    anyhow::bail!(\"attachment missing before send: {rel}\");\n}","typeGuard":"fn is_existing_attachment(target: &str, workspace: &std::path::Path) -> bool {\n    let p = std::path::Path::new(target.trim().strip_prefix(\"file://\").unwrap_or(target.trim()));\n    let resolved = if p.is_absolute() { p.to_path_buf() } else { workspace.join(p) };\n    resolved.is_file()\n}","tryCatchPattern":"match channel.send(msg_with_attachment(rel)).await {\n    Err(err) if err.to_string().contains(\"attachment path not found\") => {\n        let abs = err.to_string(); // message contains the resolved absolute path\n        tracing::warn!(%abs, \"attachment vanished or misnamed; regenerating\");\n        regenerate_attachment(workspace, rel).await?;\n        channel.send(msg_with_attachment(rel)).await?;\n    }\n    other => other?,\n}","preventionTips":["Have producers return the final workspace-relative path and use exactly that string as the target (no manual retyping of filenames).","Verify existence right before send when files are cleaned up concurrently (temp scrubbers, CI artifact pruning).","In containers, mount the artifact directory and workspace_dir at identical paths so names resolve the same everywhere.","Watch for extension/case mismatches and URL-encoded characters in generated filenames."],"tags":["filesystem","attachment","file-not-found","wechat","workspace"],"backgroundTag":"file-not-found","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}