{"record":{"id":"626ebc13fbeac592","repo":"openai/codex","slug":"local-queued-attachment-is-invalid-0","errorCode":null,"errorMessage":"local queued attachment is invalid: {0}","messagePattern":"local queued attachment is invalid: (.+?)","errorType":"validation","errorClass":"QueueServiceError","httpStatus":null,"severity":"error","filePath":"codex-rs/ext/queue/src/service.rs","lineNumber":52,"sourceCode":"use tokio::sync::Mutex;\nuse tokio::sync::OwnedMutexGuard;\nuse tokio::sync::broadcast::error::TryRecvError;\nuse uuid::Uuid;\n\n/// One user message waiting to start on its thread.\n#[derive(Clone, Debug, PartialEq)]\npub struct QueuedItem {\n    pub id: String,\n    pub input: TurnInput,\n}\n\n#[derive(Debug, Error)]\npub enum QueueServiceError {\n    #[error(\"queue storage failed: {0}\")]\n    Storage(#[from] ThreadStoreError),\n    #[error(\"queued submission payload is invalid: {0}\")]\n    InvalidPayload(#[from] serde_json::Error),\n    #[error(\"local queued attachment is invalid: {0}\")]\n    InvalidAttachment(#[from] std::io::Error),\n    #[error(\"Core failed to submit queued user message: {0}\")]\n    CoreSubmissionError(#[from] CodexErr),\n    #[error(\"only user input can be added to the user-message queue\")]\n    InvalidInput,\n    #[error(\n        \"queued user input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters ({actual_chars} provided)\"\n    )]\n    InputTooLarge { actual_chars: usize },\n}\n\n#[derive(Clone)]\npub struct QueuedItemService {\n    queue: Arc<dyn QueueStore>,\n    thread_manager: Weak<ThreadManager>,\n    event_sink: Arc<dyn ExtensionEventSink>,\n    dispatch_locks: Arc<StdMutex<HashMap<ThreadId, Weak<Mutex<()>>>>>,\n    resumed_threads: Arc<StdMutex<HashSet<ThreadId>>>,","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/ext/queue/src/service.rs#L34-L70","documentation":"Wraps std::io::Error raised while preparing a queued message for durable storage (codex-rs/ext/queue/src/service.rs:52-53). prepare_queued_user_input (service.rs:484-523) runs snapshot_local_user_input on every LocalImage/LocalAudio item inside spawn_blocking; if any attachment cannot be read or snapshotted, the io::Error converts into this variant, and a panicked blocking task also lands here as io::Error::other. The failure happens before the item is enqueued, so nothing is persisted.","triggerScenarios":"enqueue() or update() with a TurnInput containing LocalImage/LocalAudio whose path no longer exists or is unreadable; an attachment written to a temp directory that has since been cleaned up; permission changes between attach time and queue time.","commonSituations":"Temp-file cleanup races (the file is deleted between selection and send); paths captured on another machine or container; attachments on removable or permission-restricted storage.","solutions":["Check the embedded io::Error kind; NotFound and PermissionDenied name the failing attachment path.","Verify each attachment path exists and is readable before calling enqueue().","Re-attach the file from its current location if it moved.","Keep attachment sources alive (do not clean temp files) until the enqueue future completes."],"exampleFix":"// before\nservice.enqueue(thread_id, input_with_local_image).await?; // InvalidAttachment if the file vanished\n\n// after\nif let TurnInput::UserInput { content, .. } = &input_with_local_image {\n    for item in content {\n        let path = match item {\n            UserInput::LocalImage { path, .. } | UserInput::LocalAudio { path } => path,\n            _ => continue,\n        };\n        tokio::fs::metadata(path).await?; // fail fast with a clear path\n    }\n}\nservice.enqueue(thread_id, input_with_local_image).await?;","handlingStrategy":"validation","validationCode":"// Rust - verify local attachments before enqueue\nasync fn attachments_ok(content: &[UserInput]) -> std::io::Result<()> {\n    for item in content {\n        let path = match item {\n            UserInput::LocalImage { path, .. } | UserInput::LocalAudio { path } => path,\n            _ => continue,\n        };\n        tokio::fs::metadata(path).await?; // surfaces NotFound/PermissionDenied early\n    }\n    Ok(())\n}","typeGuard":"// Rust - does this input need attachment verification before queueing?\nfn has_local_attachments(content: &[UserInput]) -> bool {\n    content.iter().any(|item| matches!(\n        item,\n        UserInput::LocalImage { .. } | UserInput::LocalAudio { .. }\n    ))\n}","tryCatchPattern":null,"preventionTips":["Keep attachment files alive until enqueue completes; do not clean temp dirs mid-send.","Validate paths exist and are readable before queueing.","Check the embedded io::Error kind; it identifies the failing file.","Prefer durable locations over temp paths for attachments that may wait in the queue."],"tags":["rust","codex","queue","attachment","io","file-not-found"],"backgroundTag":"attachment-read-failed","analyzedSha":"339751715c64496cb86246bfb3935f40e309dd3d","analyzedAt":"2026-08-25T05:35:09.876Z","schemaVersion":2},"datasetVersion":"2026-08-25T06:17:31.827Z"}