Hmbown/CodeWhale · error

{label} contains unsupported characters

Error message

{label} contains unsupported characters

What it means

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.

Source

Thrown at crates/tui/src/runtime_threads.rs:274

            out.push_str(tail);
        }
    }
    out
}

fn validated_record_id<'a>(id: &'a str, label: &str) -> Result<&'a str> {
    let trimmed = id.trim();
    if trimmed.is_empty() {
        bail!("{label} cannot be empty");
    }
    if trimmed != id {
        bail!("{label} cannot contain leading or trailing whitespace");
    }
    if !trimmed
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
    {
        bail!("{label} contains unsupported characters");
    }
    Ok(trimmed)
}

fn agent_mail_workspace_id(workspace: &Path) -> Result<String> {
    let canonical = workspace
        .canonicalize()
        .with_context(|| format!("resolve Agent Mail workspace {}", workspace.display()))?;
    let digest = Sha256::digest(canonical.to_string_lossy().as_bytes());
    let digest = digest
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<String>();
    Ok(format!("ws_{digest}"))
}

fn agent_mail_sender_identity(thread: &ThreadRecord) -> Result<String> {
    thread

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Use generated opaque ids (UUID/hex) for thread, turn, and item ids and keep human names in a separate display field.
  2. If you must derive ids from text, slugify first: lowercase, strip to [a-z0-9_-], and append a random suffix to avoid collisions.
  3. Replace clock-format separators, e.g. use 20260820T123000 not 12:30:00.

Example fix

// before
let thread_id = format!("session {}", title);   // may contain spaces/colons
store.load_thread(&thread_id)?;                // bails: unsupported characters

// after
let thread_id = uuid::Uuid::new_v4().simple().to_string(); // [0-9a-f]{32}
store.load_thread(&thread_id)?;
Defensive patterns

Strategy: validation

Validate before calling

// Rust: slugify free-form text into a store-safe id
fn slug_id(text: &str) -> String {
    let slug: String = text
        .to_ascii_lowercase()
        .chars()
        .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
        .collect();
    let slug = slug.trim_matches('-').to_string();
    format!("{}-{}", slug, uuid::Uuid::new_v4().simple())
}

let thread_id = slug_id(&title); // only [a-z0-9_-]
store.load_thread(&thread_id)?;

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/cb7be02eb49083ae. Report an issue: GitHub.