{"record":{"id":"a192c843412f73b0","repo":"Hmbown/CodeWhale","slug":"label-cannot-be-empty","errorCode":null,"errorMessage":"{label} cannot be empty","messagePattern":"(.+?) cannot be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":297,"sourceCode":"        .find(COMPACTION_SUMMARY_END)\n        .map(|rel| start + rel + COMPACTION_SUMMARY_END.len());\n    let mut out = base[..start].trim_end().to_string();\n    if let Some(end) = end {\n        let tail = base[end..].trim_start();\n        if !tail.is_empty() {\n            if !out.is_empty() {\n                out.push_str(\"\\n\\n\");\n            }\n            out.push_str(tail);\n        }\n    }\n    out\n}\n\nfn validated_record_id<'a>(id: &'a str, label: &str) -> Result<&'a str> {\n    let trimmed = id.trim();\n    if trimmed.is_empty() {\n        bail!(\"{label} cannot be empty\");\n    }\n    if trimmed != id {\n        bail!(\"{label} cannot contain leading or trailing whitespace\");\n    }\n    if !trimmed\n        .chars()\n        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')\n    {\n        bail!(\"{label} contains unsupported characters\");\n    }\n    Ok(trimmed)\n}\n\nfn agent_mail_workspace_id(workspace: &Path) -> Result<String> {\n    let canonical = workspace\n        .canonicalize()\n        .with_context(|| format!(\"resolve Agent Mail workspace {}\", workspace.display()))?;\n    let digest = Sha256::digest(canonical.to_string_lossy().as_bytes());","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L279-L315","documentation":"`validated_record_id` in the runtime threads store requires every record id (thread, turn, or item) to be a non-empty string before it is used to build a file path or a store query. An empty id would produce an invalid or dangerous path, so it is rejected.","triggerScenarios":"Calling `record_path`, `save_turn`, `save_item`, `list_turns_for_thread`, `list_items_for_turn`, or `list_items_for_turns_map` with an id that is empty or all whitespace.","commonSituations":"A record was never assigned an id before saving; a lookup uses an unset/optional field defaulted to `\"\"`; deserialized records with missing ids.","solutions":["Generate or assign the id before calling the store (e.g. a UUID or ULID).","Check the id is non-empty at the call site before invoking the store API.","Fix the deserialization so missing ids surface earlier instead of defaulting to an empty string."],"exampleFix":"// before\nstore.save_turn(&thread_id, &turn_id, record)?; // turn_id == \"\"\n// after\nlet turn_id = turn_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());\nstore.save_turn(&thread_id, &turn_id, record)?;","handlingStrategy":"validation","validationCode":"fn require_id(id: &str) -> Result<&str, String> { if id.trim().is_empty() { Err(\"record id is empty\".into()) } else { Ok(id) } }","typeGuard":"fn has_id(id: &str) -> bool { !id.trim().is_empty() }","tryCatchPattern":"match store.save_turn(thread_id, turn_id, record) { Err(e) if e.contains(\"cannot be empty\") => { let id = uuid::Uuid::new_v4().to_string(); retry_with(id) }, Err(e) => return Err(e.into()), Ok(v) => v }","preventionTips":["Generate ids at record creation, never lazily at save","Treat optional id fields as required before store calls","Fail fast on empty ids in deserialization"],"tags":["validation","storage","rust"],"backgroundTag":"empty-required-field","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"}