A name that two nodes use resolves to no key, so it is grey

A name is a label any key can claim, and a channel message carries only its
sender's name. When two nodes use one name, the resolver used to let the last
contact win, which handed one node's hue, map, and page to a message that
nothing tied to it: a second "LOG.IC 3R" with a new key took the first one's
green and location. Such a name now resolves to None, like any unplaced name.
A contact's full key and the stored id of the same node still count as one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-10-11 04:02:17 -04:00
1 parent 2d2128718a
commit ad8792f34b
4 files changed
+61 -11

No files matched your search

+5
View File
@@ -32,6 +32,11 @@ can change how things behave. It does not only add new things.
keys hold what only one screen has: `Retry` or `Resend`, and `Log in` on a room. The
`Day ↑` and `Day ↓` chips are gone. On the desktop, ^PgUp and ^PgDn still move from one
day to the next.
- **A name that two nodes use is grey.** A channel message names its sender only by the
name that the sender typed, and any node can use any name. When two nodes use the same
name, MeshTerm cannot know which one wrote the message. Thus the name is grey, as an
unknown node is, and ^L and ^O do not open anything for it. Before, MeshTerm chose one
of the two nodes, and the name got the colour, the map, and the page of that node.
- **Chips show where a route starts and where it ends.** Where your terminal draws path
chips, the author of a message has a point on the right of its chip. A mention is a chip
with a notch on its left. The packet viewer, the live feed, the Messages list, and the
+3 -2
View File
@@ -496,8 +496,9 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
node's key (its first byte — any known prefix agrees) so a rename keeps the colour.
One rule on **every** platform; only the resolution changes (see **Platforms**).
A surface holding only a name resolves it first (`make_name_key_resolver`); a node no
key can place — an unresolved sender, a bare hash standing in as a name, an `○` ring —
takes `node.unknown`, THE light grey for an unidentified node, everywhere and on both
key can place — an unresolved sender, a name two or more keys use (a name is a claim
anyone can make, so MeshTerm never picks between them; JP, 2026-10-11), a bare hash
standing in as a name, an `○` ring — takes `node.unknown`, THE light grey for an unidentified node, everywhere and on both
platforms. Colour is reserved for keyed identities, never seeded from a name's
characters. `node.unknown` is deliberately not `muted`: muted is chrome and may sit a
step darker, while an unidentified node is content you can still act on. Our own node
+30 -7
View File
@@ -188,11 +188,22 @@ def make_name_key_resolver(
The app-wide colour rule takes the hue of each name from the key of the node. Some
places in the UI (the inline sender of a channel message, an ``@mention``, the origin
of a route graph) have only a *name*. This resolver changes that name (casefolded)
back into a key. The contacts of the device win (their keys are canonical). Then the
back into a key. The contacts of the device give the canonical key of a node. Then the
stored names of the recorder add the nodes that the companion never added as
contacts. Thus a known node that is not a contact still gets its own hue. A name that
no node has resolves to ``None``. The caller renders it muted, because colour is only
for identities with a key.
no node has resolves to ``None``. The caller renders it in the grey of an unknown node,
because colour is only for identities with a key.
**A name that two or more nodes use also resolves to** ``None``. A name is a label that
the owner of a key chooses, and nothing stops a second key from taking it. A channel
message carries only the name of its sender, as text that each holder of the channel
secret can write. Thus, when two keys use one name, MeshTerm cannot know which one
sent the message, and a choice between the two is a guess that the hue, the map (^L),
and the node page (^O) then show as a fact. Before, the last contact in the list won.
A second "LOG.IC 3R" with a new key then took the hue and the map of the first, which
nothing proved (JP, 2026-10-11). Two ids are one node when one is a prefix of the
other: the full key of a contact and the stored id of the same node are one claim, not
two.
Args:
contacts: The known contacts to resolve against (name → public key/key prefix).
@@ -202,17 +213,29 @@ def make_name_key_resolver(
Returns:
A callable that takes a display name and returns the best key hex that we have
for it, or ``None`` when the name matches no known node.
for it, or ``None`` when the name matches no known node, or more than one.
"""
keys: dict[str, str] = {}
# The ids of the nodes that use each name, one entry for each node.
claims: dict[str, list[str]] = {}
def claim(name: str, ident: str, *, canonical: bool) -> None:
held = claims.setdefault(name.casefold(), [])
for i, other in enumerate(held):
if other.startswith(ident) or ident.startswith(other):
if canonical:
held[i] = ident # the key of a contact replaces the stored id
return
held.append(ident)
for node, name in (stored_names or {}).items():
ident = node.lower().removeprefix("0x")
if name and ident:
keys.setdefault(name.casefold(), ident)
claim(name, ident, canonical=False)
for c in contacts or []:
ident = (c.public_key or c.key_prefix or "").lower().removeprefix("0x")
if c.name and ident:
keys[c.name.casefold()] = ident # contacts win over stored names
claim(c.name, ident, canonical=True)
keys = {name: ids[0] for name, ids in claims.items() if len(ids) == 1}
def key_of(name: str) -> str | None:
if not name:
+23 -2
View File
@@ -162,12 +162,12 @@ def test_path_text_dims_the_tail_from_dim_from() -> None:
def test_make_name_key_resolver_contacts_win_over_stored_names() -> None:
"""Contacts resolve first, stored advert names resolve strangers, and misses are None."""
"""Contacts give the canonical key, stored advert names resolve strangers, misses are None."""
from meshterm.core.models import Contact
from meshterm.services.trace_runner import make_name_key_resolver
contacts = [Contact(name="Alice", public_key="d4" + "0" * 62)]
stored = {"60aabbccdd11": "Bob", "77ee00112233": "Alice"} # an old stored Alice
stored = {"60aabbccdd11": "Bob", "d40000000000": "Alice"} # Alice's own stored id
key_of = make_name_key_resolver(contacts, stored)
assert key_of("Alice") == "d4" + "0" * 62 # the key of the contact, not the stored id
assert key_of("alice") == "d4" + "0" * 62 # case-folded
@@ -175,6 +175,27 @@ def test_make_name_key_resolver_contacts_win_over_stored_names() -> None:
assert key_of("Zed") is None # no node has the name
def test_a_name_that_two_keys_use_resolves_to_no_key() -> None:
"""Two nodes with one name: MeshTerm cannot know which one a name means, so it is grey.
A channel message carries only the name of its sender. A second key that takes a name
must not get the hue, the map, and the page of the first (JP, 2026-10-11).
"""
from meshterm.core.models import Contact
from meshterm.services.trace_runner import make_name_key_resolver
old = Contact(name="LOG.IC 3R", public_key="5d" + "0" * 62)
new = Contact(name="LOG.IC 3R", public_key="47" + "0" * 62)
assert make_name_key_resolver([old, new])("LOG.IC 3R") is None
assert make_name_key_resolver([new, old])("log.ic 3r") is None # the order does not matter
# A stranger that we only heard is a claim too, and it makes the name of a contact unsure.
stranger = {"77ee00112233": "Alice"}
alice = Contact(name="Alice", public_key="d4" + "0" * 62)
assert make_name_key_resolver([alice], stranger)("Alice") is None
assert make_name_key_resolver([alice])("Alice") == "d4" + "0" * 62
def test_name_style_without_a_key_is_the_unknown_grey() -> None:
"""A keyless name has no hue, because colour is only for identities that have a key."""
from meshterm.ui.theme import name_style, node_style