Repository navigation
Description
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:
- 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 callsnormalize('NFC')on a name. - Invisible characters survive.
trim()removes ASCII whitespace only;U+200B(zero-width space) is not White_Space, soDev+U+200Bis a second name that reads asDev.
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)isundefined: the duplicate guard atroster.js:72/:191lets a second Bot in under a name that renders identically,list_botsprints two identical lines and@Caféreaches only the first. The reverse also holds:İlker(U+0130) andi+U+0307+lkernormalize to one key, sofindBotfor the second name returns the wrong record (id: 'b'where the exact name belongs to'c'). - F — with
Devstored,findBot('Dev' + U+200B)isundefined;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[], socleanRoomName(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
Birfy commentedon Nov 8, 2026
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.
Make one visible name one identity (ThinkFlowLab#2694)
Fold canonical equivalents into the three identity keys that bypassed…
Release v0.2.27 (ThinkFlowLab#2701)