Context
Agent-identity matching — mailbox routing, AUTHORED_BY / OWNED_BY edges, wake/heartbeat targeting — rests on one convention: the canonical AgentIdentity graph-node id is @-prefixed (@neo-opus-ada), while the input handle (env NEO_AGENT_IDENTITY, GitHub login, or cloud OIDC/PAT userId) is bare. Normalization = add the @. But that convention is currently re-implemented by ~4 independent code paths, and the correctness-critical comparison sites do not normalize at all — they trust every upstream path to stay byte-for-byte in lockstep. This consolidates them onto one shared, provider-agnostic canonicalizer.
Surfaced while diagnosing a MailboxService.markRead "Unauthorized: you are not the recipient" error: that specific failure was an unrelated transient graph-projection visibility lag (self-heals on retry), but the investigation exposed that the read-path compare is a raw edgeTarget === me with no normalization. #10259 documents that exactly this class of @-prefix / alias drift has caused real mailbox mis-routing before — so hardening the boundary is well-motivated, not speculative.
The Problem
Four independent @-canonicalization mechanisms, converging on @-prefixed by lockstep luck rather than a shared contract:
normalizeAgentIdentityNodeId() — ai/scripts/lifecycle/harnessRouting.mjs:44 — !startsWith('@') ? '@'+v : v. Used by orchestrator, wake daemon, swarm-heartbeat, kb-alerting.
normalizeMailboxTarget() — ai/services/memory-core/MailboxService.mjs:98 — the mailbox send path (handles @me, AGENT:* sentinel, AGENT: strip, @@ strip, bare→@).
- Hardcoded
'@' + login — ai/mcp/server/memory-core/Server.mjs bindAgentIdentity (~:619) — the mailbox/graph auth-resolve path (me), shared by BOTH the local stdio (GitHub login) and cloud SSE (OIDC / PAT userId) identity paths.
bare() / normalizeUserId — strip @ for bare contexts (ai/graph/assertExpectedIdentity.mjs:45, etc.).
The read-path comparison (markRead MailboxService.mjs:1711 edgeTarget === me, plus siblings) does no normalization — it relies entirely on (2) and (3) emitting identical @-prefixed strings. Any single drift (a bare target, an AGENT: form, a @@ typo, a resolver that skips the prefix) → a silent "not the recipient" for a message the agent owns: a confusing, high-diagnostic-cost Model-Experience failure.
The Architectural Reality
- Canonical form:
@-prefixed AgentIdentity node id (#10144 convention; #10232 self-seed; #10259 SQLite inventory). Bare = the input handle only.
- Deployment-topology constraint — resolution must be provider-agnostic across both supported topologies (aligned-with ADR 0014):
- Local (single-machine graph + orchestrator daemon): GitHub login via
StdioIdentityResolver → resolveStdioIdentity (Server.mjs:631) → bindAgentIdentity(githubLogin).
- Cloud (containerized): OIDC / PAT
userId via buildRequestContext(reqAuth) (Server.mjs:419) → bindAgentIdentity(reqAuth.userId) (:424; PAT provisioning at :520 / authProvider :555).
- Both already funnel through
bindAgentIdentity — the natural single chokepoint. Its hardcoded '@' + login must become the shared canonicalizer so a bare PAT/OIDC userId canonicalizes identically to a bare GitHub login, with no provider special-casing.
- Compare sites:
markRead (:1711), deleteMessage, and any edgeTarget === me / recipient checks in MailboxService + PermissionService.
The Fix
- Elevate one shared canonicalizer as the single source of truth for the
@-prefix convention, in a graph/identity-appropriate module (it currently lives in ai/scripts/lifecycle/harnessRouting.mjs — an odd home for a graph-identity primitive; validate the target placement via structural-pre-flight).
- Route ALL sites through it:
bindAgentIdentity's '@'+login (both stdio + SSE paths), normalizeMailboxTarget's bare→@ branch, and the read-path compares (normalize BOTH me and edgeTarget before ===).
- Preserve
normalizeMailboxTarget's richer target grammar (@me, AGENT:* sentinel, AGENT:<family>/<model> alias resolution, role: / human: passthrough, @@-strip) — share only the prefix primitive; keep the mailbox-specific grammar in the mailbox layer.
- Provider-agnostic: no GitHub-vs-PAT/OIDC branching in the canonicalizer.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
shared identity canonicalizer (relocated normalizeAgentIdentityNodeId) |
#10144 @-convention |
single canonicalizer; bare→@, idempotent, provider-agnostic |
non-string passthrough unchanged |
helper JSDoc |
unit: bare / @ / @@ / AGENT: inputs |
bindAgentIdentity (me resolve) |
Server auth (stdio + SSE) |
shared helper for GitHub AND PAT/OIDC logins |
null node → unbound (unchanged) |
method JSDoc |
local + cloud resolution specs |
markRead / read-path compare |
MailboxService |
normalize BOTH sides before === |
no match → not-recipient (unchanged) |
method JSDoc |
drift-witness specs (bare / @@ / AGENT:) |
normalizeMailboxTarget send path |
MailboxService |
shares the prefix primitive; keeps richer grammar |
invalid target rejected (unchanged) |
function JSDoc |
existing send specs + new |
Decision Record impact
aligned-with #10144 (@-convention), aligned-with ADR 0014 (deployment topology — local vs cloud identity handling must not diverge), builds-on #10259 (prior alias/prefix normalization). No ADR amendment required.
Acceptance Criteria
Out of Scope
- The transient graph-projection visibility lag that surfaced this (separate cross-generation/cache concern; self-heals on retry).
- Alias-node merge / test-fixture purge (
#10259, done).
- Changing the canonical form (stays
@-prefixed) or the AGENT:* broadcast semantics.
- Chroma metadata
userId normalization (separate layer, per #10259 out-of-scope).
Avoided Traps
- "Just normalize
markRead" — insufficient; leaves four divergent canonicalizers to drift again. The consolidation is the fix.
- "Branch on auth provider (GitHub vs PAT/OIDC)" — wrong; the canonicalizer must be provider-agnostic or the local and cloud identity paths diverge.
- "Fold everything into
normalizeMailboxTarget" — it carries mailbox-specific target grammar (AGENT:<family>/<model>, role:) that does not belong in a generic identity canonicalizer; share the prefix primitive, keep the grammar in the mailbox layer.
Related
Builds-on #10259 · #10144 · #10232 · aligned-with ADR 0014
Origin Session ID: 01f4cc68-8b8e-43e6-b51c-55b4f421f4e0
Retrieval Hint: "agent identity @-prefix canonicalization consolidation normalizeAgentIdentityNodeId bindAgentIdentity markRead provider-agnostic local cloud"
Live latest-open sweep: checked open issues + targeted identity normalize canonical @ prefix search at 2026-07-11T10:5xZ; no equivalent open ticket found.
Context
Agent-identity matching — mailbox routing,
AUTHORED_BY/OWNED_BYedges, wake/heartbeat targeting — rests on one convention: the canonicalAgentIdentitygraph-node id is@-prefixed (@neo-opus-ada), while the input handle (envNEO_AGENT_IDENTITY, GitHub login, or cloud OIDC/PATuserId) is bare. Normalization = add the@. But that convention is currently re-implemented by ~4 independent code paths, and the correctness-critical comparison sites do not normalize at all — they trust every upstream path to stay byte-for-byte in lockstep. This consolidates them onto one shared, provider-agnostic canonicalizer.Surfaced while diagnosing a
MailboxService.markRead"Unauthorized: you are not the recipient"error: that specific failure was an unrelated transient graph-projection visibility lag (self-heals on retry), but the investigation exposed that the read-path compare is a rawedgeTarget === mewith no normalization.#10259documents that exactly this class of@-prefix / alias drift has caused real mailbox mis-routing before — so hardening the boundary is well-motivated, not speculative.The Problem
Four independent
@-canonicalization mechanisms, converging on@-prefixed by lockstep luck rather than a shared contract:normalizeAgentIdentityNodeId()—ai/scripts/lifecycle/harnessRouting.mjs:44—!startsWith('@') ? '@'+v : v. Used by orchestrator, wake daemon, swarm-heartbeat, kb-alerting.normalizeMailboxTarget()—ai/services/memory-core/MailboxService.mjs:98— the mailbox send path (handles@me,AGENT:*sentinel,AGENT:strip,@@strip, bare→@).'@' + login—ai/mcp/server/memory-core/Server.mjsbindAgentIdentity(~:619) — the mailbox/graph auth-resolve path (me), shared by BOTH the local stdio (GitHub login) and cloud SSE (OIDC / PATuserId) identity paths.bare()/normalizeUserId— strip@for bare contexts (ai/graph/assertExpectedIdentity.mjs:45, etc.).The read-path comparison (
markReadMailboxService.mjs:1711edgeTarget === me, plus siblings) does no normalization — it relies entirely on (2) and (3) emitting identical@-prefixed strings. Any single drift (a bare target, anAGENT:form, a@@typo, a resolver that skips the prefix) → a silent"not the recipient"for a message the agent owns: a confusing, high-diagnostic-cost Model-Experience failure.The Architectural Reality
@-prefixed AgentIdentity node id (#10144convention;#10232self-seed;#10259SQLite inventory). Bare = the input handle only.StdioIdentityResolver→resolveStdioIdentity(Server.mjs:631) →bindAgentIdentity(githubLogin).userIdviabuildRequestContext(reqAuth)(Server.mjs:419) →bindAgentIdentity(reqAuth.userId)(:424; PAT provisioning at:520/authProvider:555).bindAgentIdentity— the natural single chokepoint. Its hardcoded'@' + loginmust become the shared canonicalizer so a bare PAT/OIDCuserIdcanonicalizes identically to a bare GitHub login, with no provider special-casing.markRead(:1711),deleteMessage, and anyedgeTarget === me/ recipient checks inMailboxService+PermissionService.The Fix
@-prefix convention, in a graph/identity-appropriate module (it currently lives inai/scripts/lifecycle/harnessRouting.mjs— an odd home for a graph-identity primitive; validate the target placement viastructural-pre-flight).bindAgentIdentity's'@'+login(both stdio + SSE paths),normalizeMailboxTarget's bare→@branch, and the read-path compares (normalize BOTHmeandedgeTargetbefore===).normalizeMailboxTarget's richer target grammar (@me,AGENT:*sentinel,AGENT:<family>/<model>alias resolution,role:/human:passthrough,@@-strip) — share only the prefix primitive; keep the mailbox-specific grammar in the mailbox layer.Contract Ledger Matrix
normalizeAgentIdentityNodeId)#10144@-convention@, idempotent, provider-agnostic@/@@/AGENT:inputsbindAgentIdentity(meresolve)markRead/ read-path compareMailboxService===@@/AGENT:)normalizeMailboxTargetsend pathMailboxServiceDecision Record impact
aligned-with #10144(@-convention),aligned-with ADR 0014(deployment topology — local vs cloud identity handling must not diverge), builds-on#10259(prior alias/prefix normalization). No ADR amendment required.Acceptance Criteria
@-prefix convention, in a graph/identity-appropriate module (notscripts/lifecycle); placement validated viastructural-pre-flight.bindAgentIdentityresolvesmethrough the shared helper for BOTH the stdio/GitHub and SSE/OIDC-PAT paths — a bare login canonicalizes identically regardless of auth provider (no provider branching).markRead+ siblings) normalizes BOTHmeand the edge target before comparing; a@/AGENT:/@@/ bare-drifted target still resolves to the correct recipient.normalizeMailboxTarget's existing grammar (@me,AGENT:*,AGENT:<family>/<model>,role:/human:,@@-strip) is preserved — no send-path regression.@-node and round-trip a mailbox send →markRead.'@'+/ bare→@reimplementations are removed in favor of the shared helper.Out of Scope
#10259, done).@-prefixed) or theAGENT:*broadcast semantics.userIdnormalization (separate layer, per#10259out-of-scope).Avoided Traps
markRead" — insufficient; leaves four divergent canonicalizers to drift again. The consolidation is the fix.normalizeMailboxTarget" — it carries mailbox-specific target grammar (AGENT:<family>/<model>,role:) that does not belong in a generic identity canonicalizer; share the prefix primitive, keep the grammar in the mailbox layer.Related
Builds-on
#10259·#10144·#10232· aligned-with ADR 0014Origin Session ID: 01f4cc68-8b8e-43e6-b51c-55b4f421f4e0
Retrieval Hint: "agent identity @-prefix canonicalization consolidation normalizeAgentIdentityNodeId bindAgentIdentity markRead provider-agnostic local cloud"
Live latest-open sweep: checked open issues + targeted
identity normalize canonical @ prefixsearch at 2026-07-11T10:5xZ; no equivalent open ticket found.