zeroclaw-labs/zeroclaw · error

{field_name} must start with an ASCII letter or underscore;

Error message

{field_name} must start with an ASCII letter or underscore; got '{value}'

What it means

The second identifier rule: PostgreSQL identifiers used by the memory store must start with an ASCII letter or underscore. Unquoted SQL identifiers cannot start with a digit, and since these names are formatted into query text, a leading digit or symbol would produce broken or dangerous SQL, so the constructor rejects it.

Source

Thrown at crates/zeroclaw-memory/src/postgres.rs:342

                .with_outcome(::zeroclaw_log::EventOutcome::Failure),
            "PostgreSQL operation thread terminated unexpectedly"
        );
        anyhow::Error::msg("PostgreSQL operation thread terminated unexpectedly")
    })?
}

pub(super) fn validate_identifier(value: &str, field_name: &str) -> Result<()> {
    if value.is_empty() {
        anyhow::bail!("{field_name} must not be empty");
    }

    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        anyhow::bail!("{field_name} must not be empty");
    };

    if !(first.is_ascii_alphabetic() || first == '_') {
        anyhow::bail!("{field_name} must start with an ASCII letter or underscore; got '{value}'");
    }

    if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
        anyhow::bail!(
            "{field_name} can only contain ASCII letters, numbers, and underscores; got '{value}'"
        );
    }

    Ok(())
}

pub(super) fn quote_identifier(value: &str) -> String {
    format!("\"{value}\"")
}

fn recall_time_filter(since: bool, until: bool, first_placeholder: usize) -> String {
    match (since, until) {
        (true, true) => format!(

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Rename to start with a letter or underscore (archive_2026, _2026_archive).
  2. Apply a sanitizer that prefixes an underscore when the first character is a digit.
  3. Keep storage alias names in the same [A-Za-z_][A-Za-z0-9_]* shape so derived identifiers inherit it.

Example fix

# before
[storage.postgres.main]
table_prefix = "2026-memories"

# after
[storage.postgres.main]
table_prefix = "memories_2026"
Defensive patterns

Strategy: type-guard

Validate before calling

if !is_valid_pg_identifier(&name) {
    anyhow::bail!("identifier '{name}' must match [A-Za-z_][A-Za-z0-9_]*");
}

Type guard

fn is_valid_pg_identifier(name: &str) -> bool {
    let mut chars = name.chars();
    matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
        && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
        && !name.is_empty()
}

Prevention

When it happens

Trigger: Schema or table names like "2026_archive" or "-prod", or identifiers derived from agent/storage aliases beginning with a digit (agents.7writer), passed to the postgres store constructor or validated_schema_identifier.

Common situations: Numeric-prefixed autogenerated names (counters, years); kebab or symbol-prefixed aliases reused verbatim as table/schema names; names pasted with leading punctuation.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/6980963fc777a038. Report an issue: GitHub.