{"record":{"id":"d6334217fcbb8953","repo":"Hmbown/CodeWhale","slug":"persisted-image-input-is-missing-its-prompt","errorCode":null,"errorMessage":"persisted image input is missing its prompt","messagePattern":"persisted image input is missing its prompt","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":1441,"sourceCode":"            || (self.schema_version == OUTPUT_LIMIT_RUNTIME_SCHEMA_VERSION\n                && self\n                    .metadata\n                    .as_ref()\n                    .is_some_and(|meta| meta.get(\"runtime_image_content\").is_some())))\n            && self.kind == TurnItemKind::UserMessage\n        {\n            let content = self\n                .metadata\n                .as_ref()\n                .and_then(|meta| meta.get(\"runtime_image_content\"))\n                .context(\"persisted image input is missing its content\")?;\n            let blocks: Vec<ContentBlock> =\n                serde_json::from_value(content.clone()).context(\"invalid persisted image input\")?;\n            crate::image_attach::validate_stored_image_content(&blocks)?;\n            if self.detail.as_deref().is_some_and(|text| !text.trim().is_empty())\n                && !blocks.iter().any(|block| matches!(block, ContentBlock::Text { text, .. } if !text.trim().is_empty()))\n            {\n                bail!(\"persisted image input is missing its prompt\");\n            }\n            return Ok(blocks);\n        }\n        let text = self.detail.as_ref().unwrap_or(&self.summary);\n        Ok(if text.trim().is_empty() {\n            Vec::new()\n        } else {\n            vec![ContentBlock::Text {\n                text: text.clone(),\n                cache_control: None,\n            }]\n        })\n    }\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct RuntimeEventRecord {\n    #[serde(default = \"default_runtime_schema_version\")]","sourceCodeStart":1423,"sourceCodeEnd":1459,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L1423-L1459","documentation":"When loading a persisted image-attachment turn input, the runtime requires a non-empty user prompt (text) to accompany the image content blocks. The store decoded the content successfully, but every text block was empty or whitespace-only, so the record is rejected. This guards against sessions that would otherwise send an image to the model with no instruction.","triggerScenarios":"Calling the persisted-record loader (detail-to-content-blocks conversion in runtime_threads.rs) for a turn whose ContentBlock JSON parses but contains only image blocks, or whose `detail` field is empty/whitespace while no non-empty text block exists in the content.","commonSituations":"A session file was hand-edited or migrated and its text block was stripped; an older build crashed mid-save leaving image blocks without the prompt; a bug in the attachment flow wrote image content before capturing the detail text.","solutions":["Re-open the message in the TUI and type a prompt before sending, then re-save/persist the record.","Edit the persisted session record to add a non-empty text ContentBlock (or a non-empty `detail`) alongside the image blocks.","Restore the session file from backup or start a new thread, re-attaching the image with a prompt.","If caused by a version/migration path, update to a build whose attachment flow persists the prompt with the image."],"exampleFix":"// before: persisted content has only an image block\n[{\"type\":\"image\", \"source\":{...}}]\n\n// after: include the prompt text block\n[{\"type\":\"text\", \"text\":\"Describe this screenshot\"},{\"type\":\"image\", \"source\":{...}}]","handlingStrategy":"validation","validationCode":"let has_prompt = blocks.iter().any(|b| matches!(b, ContentBlock::Text { text, .. } if !text.trim().is_empty()));\nif !has_prompt {\n    anyhow::bail!(\"attach a prompt before persisting image input\");\n}","typeGuard":"fn has_nonempty_text(blocks: &[ContentBlock]) -> bool {\n    blocks.iter().any(|b| matches!(b, ContentBlock::Text { text, .. } if !text.trim().is_empty()))\n}","tryCatchPattern":"match load_persisted_image_input(record) {\n    Ok(blocks) => send(blocks),\n    Err(e) if e.to_string().contains(\"missing its prompt\") => prompt_user_for_text_then_resend(),\n    Err(e) => return Err(e),\n}","preventionTips":["Always capture the user prompt in the same save as the image attachment (single atomic write).","Reject image-only submissions in the UI before persisting.","Add a round-trip test that loads every persisted record and re-validates it."],"tags":["persistence","image-attachment","validation"],"backgroundTag":"missing-required-argument","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}