{"record":{"id":"c4d1aab986e6e784","repo":"Hmbown/CodeWhale","slug":"label-cannot-contain-leading-or-trailing-whitesp","errorCode":null,"errorMessage":"{label} cannot contain leading or trailing whitespace","messagePattern":"(.+?) cannot contain leading or trailing whitespace","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":268,"sourceCode":"    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());\n    let digest = digest\n        .iter()\n        .map(|byte| format!(\"{byte:02x}\"))","sourceCodeStart":250,"sourceCodeEnd":286,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L250-L286","documentation":"validated_record_id rejects ids where trim() changes the string, i.e. ids with leading or trailing whitespace. Because ids are used verbatim as path components and lookup keys, invisible padding would create records that can never be addressed again, so it is treated as data corruption at the door.","triggerScenarios":"Passing an id like \" turn-1\" or \"turn-1\\n\" to store APIs (load_thread, list_turns_for_thread, persist_turn with a padded thread_id, etc.); ids read from files or environment variables that include a trailing newline.","commonSituations":"Ids assembled from file lines without trimming the newline; copy-paste into prompts adding a trailing space; ids built by concatenating user tokens with careless separators.","solutions":["Trim the id once at its origin (when read from input or a file) and store the trimmed form.","When accepting ids from users, normalize with trim() before any store call.","Audit concatenation sites like format!(\"{a}-{b}\") where either part may carry whitespace."],"exampleFix":"// before\nlet id = line_reader.read_line()?;      // \"turn-1\\n\"\nstore.load_turn(&id)?;                  // bails: leading/trailing whitespace\n\n// after\nlet id = line_reader.read_line()?.trim().to_string();\nstore.load_turn(&id)?;","handlingStrategy":"validation","validationCode":"// Rust: trim at the boundary, once, before any store call\nlet id = raw_id.trim();\nanyhow::ensure!(!id.is_empty(), \"id cannot be empty\");\nstore.list_turns_for_thread(id)?;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Trim ids when reading from files, env vars, or stdin (newlines are the usual culprit).","Never build ids by concatenating untrimmed fragments.","Store the trimmed form so lookups never need ad-hoc normalization."],"tags":["validation","identifier","whitespace","thread-store","rust"],"backgroundTag":"whitespace-in-identifier","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}