{"record":{"id":"cb7be02eb49083ae","repo":"Hmbown/CodeWhale","slug":"label-contains-unsupported-characters","errorCode":null,"errorMessage":"{label} contains unsupported characters","messagePattern":"(.+?) contains unsupported characters","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":274,"sourceCode":"            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());\n    let digest = digest\n        .iter()\n        .map(|byte| format!(\"{byte:02x}\"))\n        .collect::<String>();\n    Ok(format!(\"ws_{digest}\"))\n}\n\nfn agent_mail_sender_identity(thread: &ThreadRecord) -> Result<String> {\n    thread","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L256-L292","documentation":"validated_record_id only accepts ASCII alphanumerics plus '-' and '_'; any other character bails with '<label> contains unsupported characters'. The allowlist exists because ids become filenames in the on-disk thread store and must be portable and injection-safe across platforms.","triggerScenarios":"Passing ids containing spaces, dots, slashes, colons, unicode, or any symbol outside [A-Za-z0-9_-] to the thread store APIs; generating ids from free-form text such as session titles.","commonSituations":"Using human-readable names ('My Session: draft #2') as ids; ids derived from timestamps with colons (12:30:00); unicode identifiers from internationalized input; separators like '.' or '/' copied from other systems' id formats.","solutions":["Use generated opaque ids (UUID/hex) for thread, turn, and item ids and keep human names in a separate display field.","If you must derive ids from text, slugify first: lowercase, strip to [a-z0-9_-], and append a random suffix to avoid collisions.","Replace clock-format separators, e.g. use 20260820T123000 not 12:30:00."],"exampleFix":"// before\nlet thread_id = format!(\"session {}\", title);   // may contain spaces/colons\nstore.load_thread(&thread_id)?;                // bails: unsupported characters\n\n// after\nlet thread_id = uuid::Uuid::new_v4().simple().to_string(); // [0-9a-f]{32}\nstore.load_thread(&thread_id)?;","handlingStrategy":"validation","validationCode":"// Rust: slugify free-form text into a store-safe id\nfn slug_id(text: &str) -> String {\n    let slug: String = text\n        .to_ascii_lowercase()\n        .chars()\n        .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })\n        .collect();\n    let slug = slug.trim_matches('-').to_string();\n    format!(\"{}-{}\", slug, uuid::Uuid::new_v4().simple())\n}\n\nlet thread_id = slug_id(&title); // only [a-z0-9_-]\nstore.load_thread(&thread_id)?;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep opaque generated ids for storage and human names for display only.","Forbid spaces, colons, dots, slashes, and non-ASCII in id fields at input validation.","Use timestamp formats without colons (20260820T123000) when dates appear in ids."],"tags":["validation","identifier","filename-safety","thread-store","rust"],"backgroundTag":"invalid-identifier-characters","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}