Skip to content

One name, two identities: the name key has no Unicode normalization and keeps invisible characters #2691

Closed

Description

@Birfy

Written-by: Chief

core/src/host/text.js:23 builds the identity key every name comparison uses — Bot names, room names, @-mentions:

export const normalize = (value) => String(value ?? '').trim().toLowerCase()

Two gaps, both reachable from the surfaces that already call it:

  1. No Unicode normalization. Café written NFC (U+0043 U+0061 U+0066 U+00E9) and the same name written NFD (Cafe + U+0301) are the same visible name and two different keys. Nothing in the repository calls normalize('NFC') on a name.
  2. Invisible characters survive. trim() removes ASCII whitespace only; U+200B (zero-width space) is not White_Space, so Dev + U+200B is a second name that reads as Dev.

Reproduction (real modules, main 6f086fa528f6ac99437b3942a8115c6eaa86052e)

/data/dsh/bot/workspace/audit42/audit42.test.mjs, cases B, F and G — the real install()/load() store, temp home, fake data:

  • B — with Café (NFC) stored, findBot('Cafe' + U+0301) is undefined: the duplicate guard at roster.js:72/:191 lets a second Bot in under a name that renders identically, list_bots prints two identical lines and @Café reaches only the first. The reverse also holds: İlker (U+0130) and i + U+0307 + lker normalize to one key, so findBot for the second name returns the wrong record (id: 'b' where the exact name belongs to 'c').
  • F — with Dev stored, findBot('Dev' + U+200B) is undefined; nameProblem() (shared/mentions.js:15) accepts the invisible twin, so a second Bot lives under a name nothing on screen can distinguish.
  • G — with the room Café (NFC) stored, findRooms('Cafe' + U+0301) is [], so cleanRoomName (roster.js:281) admits a second group chat with the same visible name; once both exist, a keyboard that writes NFC can never name the second one again.

Reverse control in every case: value.normalize('NFC') on both sides before comparing makes the two spellings one identity.

Why it matters

The identity key decides which Bot a message goes to, which chat a name opens, which memory scope a remember writes into, and whether a duplicate Bot is allowed to exist. Being wrong in the first direction creates a twin nobody can address; being wrong in the second direction (İlker) resolves a name to a different Bot — a message, a memory write or a stop_bot aimed at one record landing on another.

Expected

One identity key that is right for names: NFC-normalize, drop invisible format characters (U+200B–U+200D, U+FEFF, U+00AD), then trim and lowercase — applied in normalize() so every existing call site inherits it, and mirrored in the guards that compare names directly (roster.js:72, :191, :281, shared/mentions.js:15). A name that is only invisible characters must be refused as empty. Existing stored names keep resolving: the NFC form of what is already in state.json is what the key must produce.

Change scope

core/src/host/text.js (the one key), the host guards that compare names without it, core/src/shared/mentions.js (the name rule), plus tests. core/test/ has store/roster cases to extend. Same class, different surfaces, tracked separately in #2692 (tree, task board, memory topics) and #2693 (the 60-unit cut in plugins/social).

Activity

  1. added
    bugSomething isn't working
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Nov 8, 2026
  2. self-assigned this
    on Nov 8, 2026
  3. added
    agent-prPull request opened by an agent
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Nov 8, 2026
  4. Birfy commented on Nov 8, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev

    Claimed #2691 — first step: the one name key in core/src/host/text.js.

    Opened #2694: #2694 — NFC, the invisible format characters and both duplicate guards now share one identity key, and a stored name keeps only its visible characters.

  5. added 3 commits that reference this issue on Nov 9, 2026
    6d35b4b
    e3e488d
    af9a30b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions