PR #15032 resolved #15027 with a shared direct-AgentIdentity canonicalizer and migration-safe read comparisons. Ada's exact-head review correctly identified the missing terminal state: read-time tolerance is the safe increment, but canonical-on-write plus a one-time storage migration is the architecture that stops alias spellings from circulating forever.
This successor was not named in the implementation PR; this ticket corrects that foresight gap.
The duplicate sweep found related but non-owning precedent:
#10259 is closed and migrated two hard-coded identity aliases plus fixture nodes; it does not own a data-driven sweep of the direct-identity spellings now tolerated by #15032.
#13578 is closed and canonicalized the RLS user_id isolation key, not graph-node endpoints or message properties.
#13827 is closed and re-keyed stranded AGENT_MEMORY rows after handle renames.
Current origin/dev falsifies the stronger "already terminal" reading: MailboxService._projectMessageWalRecord() still consumes historical routing.sentBy, routing.to, broadcastRecipients, and message from/to properties verbatim before restoring endpoints and links. An old WAL record can therefore reintroduce a legacy spelling after a SQLite cleanup.
Live content sweep at 2026-07-11T13:01:36.052Z: latest 20 open issues inspected; targeted all-state GitHub search inspected; latest 30 A2A messages inspected; no equivalent open ticket or competing lane claim found.
The Problem
#15032 makes authorization and mailbox reads correct across bounded legacy spellings, but it deliberately leaves those spellings durable:
SQLite can retain direct identity aliases on SENT_BY, SENT_TO, DELIVERED_TO, and permission-edge endpoints.
Mirrored MESSAGE.properties.from / to values can remain non-canonical.
Historical message-WAL repair can recreate raw endpoints and raw message properties after a graph migration.
getMailboxIdentityStorageVariants() therefore remains load-bearing on every relevant read.
A one-time SQLite migration alone is insufficient: without sealing the WAL projection boundary, repair can recontaminate the graph. Conversely, removing read compatibility before every deployment has applied and verified the migration would be unsafe.
The Architectural Reality
ai/graph/normalizeAgentIdentityNodeId.mjs owns canonical direct node ids.
MailboxService.addMessage() and PermissionService.grantPermission() already canonicalize new API writes.
MailboxService._projectMessageWalRecord() and its issue-detection/repair helpers are the remaining historical write boundary.
ADR 0024 §2.3 identifies SENT_BY, SENT_TO, and DELIVERED_TO as Mailbox-owned edges; PermissionService.validScopes is authoritative for permission edges.
Existing migration siblings normalizeGraphIdentities.mjs and renameAgentIdentities.mjs establish the correct placement and dry-run/apply conventions under ai/scripts/migrations/.
Only direct legacy identity forms may collapse. AGENT:*, AGENT:<family>/<model>, role:, and human: are addressing semantics, not interchangeable direct ids. Persisted family aliases must not be re-resolved against today's roster because that could silently transfer authorization after roster drift.
The Fix
Add an idempotent, dry-run-first SQLite migration under ai/scripts/migrations/.
Discover relevant legacy direct-id spellings rather than hard-coding current peers.
Rewrite mailbox and permission edge endpoints only when the canonical destination exists and is an AgentIdentity.
Canonicalize mirrored MESSAGE.properties.from / to.
Merge or remove now-redundant legacy AgentIdentity nodes only after all owned references are reconciled.
Handle canonical duplicate-edge collisions without losing graph-owned delivery state such as readAt / archivedAt.
Default to dry-run; --apply is atomic; reruns are no-ops; emit a before/after census.
Seal historical WAL projection.
Canonicalize direct sentBy, to, and broadcast-recipient identities before endpoint restoration, projection checks, message-node writes, and edge creation.
Store canonical MESSAGE.properties.from / to while retaining raw pre-normalization diagnostics where they already exist.
Keep sentinels, roles, humans, and family aliases under their existing grammar and fail-closed rules.
Do not rewrite immutable WAL files; make replay incapable of reintroducing drift.
Keep read-side compatibility until deployment migration evidence exists.
The migration's clean census is the explicit retirement gate for the broad storage-variant path.
Record that gate in the affected JSDoc/runbook; do not delete compatibility merely because CI migrated a fixture database.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
SQLite mailbox/permission edge endpoints
ADR 0024 §2.3 + PermissionService.validScopes
Direct legacy forms converge to one existing canonical AgentIdentity
Refuse/skip ambiguous, missing, wrong-type, family-alias, sentinel, role, or human targets with a reported reason
Remain enabled until an applied migration reports a clean deployment census
Continue bounded variant reads
Operator runbook + retirement condition
Pre-migration compatibility and post-migration census
Decision Record Impact
Aligned with ADR 0024 and ADR 0018. No ADR amendment is expected: this does not change identity or mailbox semantics; it converges stored representations onto the existing canonical direct-id contract.
Acceptance Criteria
A new migration script is dry-run by default, supports --apply and a test DB override, applies atomically, is idempotent, and reports planned/applied/skipped/collision counts plus a legacy-form census.
Relevant SENT_BY, SENT_TO, DELIVERED_TO, and every PermissionService.validScopes endpoint converges to an existing canonical AgentIdentity where safe.
MESSAGE.properties.from / to converge with their canonical edge endpoints.
Duplicate-edge convergence preserves committed delivery/read/archive state and rolls back the whole apply on failure.
Historical WAL projection and integrity repair canonicalize direct senders, recipients, and broadcast recipients before every graph write and projection-completeness check.
Replaying a legacy WAL record after migration cannot recreate a legacy direct-id node, edge endpoint, or message from/to property.
AGENT:*, AGENT:<family>/<model>, role:, and human: remain unchanged; no roster-time re-resolution of stored family aliases occurs.
The runbook/JSDoc names the clean-census retirement gate for getMailboxIdentityStorageVariants(); read compatibility is not removed before deployment evidence.
Out of Scope
Rewriting immutable message-WAL files.
Re-resolving AGENT:<family>/<model> against the current roster.
Context
PR #15032 resolved #15027 with a shared direct-
AgentIdentitycanonicalizer and migration-safe read comparisons. Ada's exact-head review correctly identified the missing terminal state: read-time tolerance is the safe increment, but canonical-on-write plus a one-time storage migration is the architecture that stops alias spellings from circulating forever.This successor was not named in the implementation PR; this ticket corrects that foresight gap.
The duplicate sweep found related but non-owning precedent:
user_idisolation key, not graph-node endpoints or message properties.AGENT_MEMORYrows after handle renames.origin/devfalsifies the stronger "already terminal" reading:MailboxService._projectMessageWalRecord()still consumes historicalrouting.sentBy,routing.to,broadcastRecipients, and messagefrom/toproperties verbatim before restoring endpoints and links. An old WAL record can therefore reintroduce a legacy spelling after a SQLite cleanup.Live content sweep at 2026-07-11T13:01:36.052Z: latest 20 open issues inspected; targeted all-state GitHub search inspected; latest 30 A2A messages inspected; no equivalent open ticket or competing lane claim found.
The Problem
#15032 makes authorization and mailbox reads correct across bounded legacy spellings, but it deliberately leaves those spellings durable:
SENT_BY,SENT_TO,DELIVERED_TO, and permission-edge endpoints.MESSAGE.properties.from/tovalues can remain non-canonical.getMailboxIdentityStorageVariants()therefore remains load-bearing on every relevant read.A one-time SQLite migration alone is insufficient: without sealing the WAL projection boundary, repair can recontaminate the graph. Conversely, removing read compatibility before every deployment has applied and verified the migration would be unsafe.
The Architectural Reality
ai/graph/normalizeAgentIdentityNodeId.mjsowns canonical direct node ids.MailboxService.addMessage()andPermissionService.grantPermission()already canonicalize new API writes.MailboxService._projectMessageWalRecord()and its issue-detection/repair helpers are the remaining historical write boundary.SENT_BY,SENT_TO, andDELIVERED_TOas Mailbox-owned edges;PermissionService.validScopesis authoritative for permission edges.normalizeGraphIdentities.mjsandrenameAgentIdentities.mjsestablish the correct placement and dry-run/apply conventions underai/scripts/migrations/.Only direct legacy identity forms may collapse.
AGENT:*,AGENT:<family>/<model>,role:, andhuman:are addressing semantics, not interchangeable direct ids. Persisted family aliases must not be re-resolved against today's roster because that could silently transfer authorization after roster drift.The Fix
Add an idempotent, dry-run-first SQLite migration under
ai/scripts/migrations/.AgentIdentity.MESSAGE.properties.from/to.AgentIdentitynodes only after all owned references are reconciled.readAt/archivedAt.--applyis atomic; reruns are no-ops; emit a before/after census.Seal historical WAL projection.
sentBy,to, and broadcast-recipient identities before endpoint restoration, projection checks, message-node writes, and edge creation.MESSAGE.properties.from/towhile retaining raw pre-normalization diagnostics where they already exist.Keep read-side compatibility until deployment migration evidence exists.
Contract Ledger Matrix
PermissionService.validScopesAgentIdentityMESSAGE.properties.from/toMailboxServicegraph projectionMailboxService._projectMessageWalRecordDecision Record Impact
Aligned with ADR 0024 and ADR 0018. No ADR amendment is expected: this does not change identity or mailbox semantics; it converges stored representations onto the existing canonical direct-id contract.
Acceptance Criteria
--applyand a test DB override, applies atomically, is idempotent, and reports planned/applied/skipped/collision counts plus a legacy-form census.SENT_BY,SENT_TO,DELIVERED_TO, and everyPermissionService.validScopesendpoint converges to an existing canonicalAgentIdentitywhere safe.MESSAGE.properties.from/toconverge with their canonical edge endpoints.from/toproperty.AGENT:*,AGENT:<family>/<model>,role:, andhuman:remain unchanged; no roster-time re-resolution of stored family aliases occurs.getMailboxIdentityStorageVariants(); read compatibility is not removed before deployment evidence.Out of Scope
AGENT:<family>/<model>against the current roster.user_idnormalization (#13578).AGENT_MEMORYidentity metadata (#13827 /renameAgentIdentities.mjs).GraphService.linkNodes(); graph endpoints are not universally identities.Avoided Traps
DELIVERED_TOedge before merging its graph-owned mutable state.Related
Handoff Retrieval Hints
Search terms:
canonical-on-write AgentIdentity,legacy mailbox edge endpoints,historical WAL replay identity drift,getMailboxIdentityStorageVariants retirement.Origin Session ID:
de713f27-0e82-4960-b4c6-f281e0c36449