Skip to main content

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)
These two characters are always positional, never part of a name segment. A literal - 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 composes rig/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’s session_name.go by name. __ and every other run still collapses to _, so gastown--mayor and gastown__mayor are now correctly distinct — the widening this contract was written to prevent is fixed upstream.
  • The / axis is therefore no longer missed. gastown/mayor and gastown--mayor now canonicalize to the same value, so a /-spelled identity does compare equal to its ---spelled encoding.
That second point is a real behavior change, not just a doc correction: two gascity identities differing only in -- 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 is internal/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.
If this document and internal/agent/session_name.go ever disagree, the code wins. File a correction against this doc rather than reimplementing the table anywhere else.

5. Worked examples

Last modified on September 11, 2026