What this is
gascity mints qualified agent identities —rig/agent and city.agent
combinations — and renders them into tmux-safe session name strings. beads
stores and compares those same strings in its own records (session-name
metadata, routing fields) without minting them itself. Two independent
codebases have to agree, byte for byte, on how the structural separators /
and . get encoded, or a qualified identity that round-trips through beads
and back can silently turn into a different identity than the one gascity
minted.
This contract exists so that agreement is written down once, instead of
re-derived by reading internal/agent/session_name.go from two different
repos and hoping both readings match.
1. The separator alphabet
A qualified identity is built from name segments joined by structural separators. Exactly two characters are reserved as positional separators in a raw (unencoded) qualified identity:/— the rig/agent boundary (rig/agent).— the city/agent boundary on an imported identity (city.agent)
- or _, by contrast, is an ordinary character a name segment MAY
contain: gascity’s own agent-name validation permits both
(internal/config/config.go:27, enforced at :4015 — a name must match
[a-zA-Z0-9][a-zA-Z0-9_-]*, so hello-world and builder-1 are valid agent
names in their own right, not /-delimited pairs).
Encoding (§2) represents each positional separator as a doubled pair rather
than touching - or _ on their own: a literal -, doubled, stands in for
/ (as --); a literal _, doubled, stands in for . (as __). A single
- or single _ is never itself a positional separator — only the exact
two-byte sequences -- and __ carry structural meaning under this
contract.
2. The two-axis encoding table
The two axes are kept distinct on purpose:
/ and . encode to different
two-character sequences (-- vs __) so that the two boundaries stay
separable. A canonicalizer implementing this rule MUST NOT collapse -- and
__ to the same normal form — doing so can silently merge two distinct
minted identities into one. For example, the encoded names
hello-world--polecat and hello_world__polecat both reduce to
hello_world_polecat under a canonicalizer that collapses any run of .,
_, or - to a single _, even though they decode to the structurally
distinct identities hello-world/polecat and hello_world.polecat.
Decoding is best-effort, not lossless. A name segment may itself contain
-- or __: builder--1 is a legal agent name under the §1 regex, so
rig/builder--1 encodes to rig--builder--1 and decodes back to the
different identity rig/builder/1. The encode direction is total; the
decode direction guesses. Do not build an exact round-trip on this table —
treat an encoded session name as a display and lookup key, and carry the raw
qualified identity alongside it wherever you need to recover it exactly.
3. Who mints, who compares
gascity is the only side that mints qualified identities. It composesrig/agent and city.agent strings when constructing an agent’s
qualified name, and it is the only codebase that calls the encode direction
(§4) to produce a tmux-safe session name from one.
beads never mints and has no equivalent of the encode direction, and it
does not implement the decode direction described in §4 at all. What it
has instead is issueops.ActorMatches (beads repo,
internal/storage/issueops/identity.go, backed by the unexported
canonicalActor at lines 29-49; package-local-duplicated as
validation.ActorMatches/CanonicalActor in internal/validation/issue.go
because storage may not import validation) — a narrower, dot-axis-only
canonicalizer used purely for assignee/actor comparison. It collapses any
run of ., _, or - down to a single _. Per its own doc comment, this
exists to reconcile the different contextual spellings of a dot that the
same identity can arrive in — __ in a session name, _ in a Dolt
table/database name, an unspecified - “elsewhere” — not to decode the /
rig-boundary. It MISSES the slash axis entirely: / is not one of its
cases, so a raw / always passes through untouched, and a /-spelled
identity does NOT compare equal to its ---spelled encoding under this
function (see the architecture ruling this contract implements,
ga-qv1d2d, which measures exactly this: beads’ matcher never reconciles
the / axis). What it does do is collapse -- and __ to that same
generic _ — the widening this contract exists to prevent, since two
structurally distinct gascity identities (a rig/agent pair and a
city/agent pair built from the same segment names) can be driven to
compare equal on the beads side.
Superseded upstream as of beads v1.3.0-rc.1 (carried unchanged into
v1.3.0-rc.2, gascity’s go.mod pin since 2026-09-10 —
internal/storage/issueops/identity.go is byte-identical between the two
RCs; the description above held at the previous pin
v1.1.1-0.20260805093327-bf97b73749ac, where that file did not exist at
all). Two of the claims above are no longer true of the pinned beads:
--no longer collapses to the generic_. An exact two-byte--run now decodes to a literal/, citing gascity’ssession_name.goby name.__and every other run still collapses to_, sogastown--mayorandgastown__mayorare now correctly distinct — the widening this contract was written to prevent is fixed upstream.- The
/axis is therefore no longer missed.gastown/mayorandgastown--mayornow canonicalize to the same value, so a/-spelled identity does compare equal to its---spelled encoding.
-- versus / now compare equal
on beads’ assignee/actor path. Audit live actor names for such a pair
before relying on a claim guard to keep them apart.
4. Source of truth
The tables above describe the code; they do not replace it. The authoritative implementation isinternal/agent/session_name.go:
sessionNameQualifiedReplacer(internal/agent/session_name.go:16-19) — the encode direction (§2, raw → encoded).sessionNameQualifiedReverseReplacer(internal/agent/session_name.go:21-24) — the best-effort decode direction (§2, encoded → raw); see the decoding caveat in §2.
internal/agent/session_name.go ever disagree, the
code wins. File a correction against this doc rather than reimplementing
the table anywhere else.