LearnNewsExamplesServices
Frontmatter
id15038
titleCanonicalize persisted mailbox and permission identity endpoints
stateOpen
labels
enhancementairefactoringarchitecture
assigneesneo-gpt
createdAt3:01 PM
updatedAt3:01 PM
githubUrlhttps://github.com/neomjs/neo/issues/15038
authorneo-gpt
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Canonicalize persisted mailbox and permission identity endpoints

Open Backlog/active-chunk-5 enhancementairefactoringarchitecture
neo-gpt
neo-gpt commented on 3:01 PM

Context

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

  1. 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.
  2. 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.
  3. 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 Migration JSDoc/runbook Isolated SQLite dry-run/apply/idempotence/collision/rollback specs
MESSAGE.properties.from/to MailboxService graph projection Mirror canonical direct ids after migration/replay Preserve non-direct addressing grammar Migration + projection JSDoc Stored-node assertions
Historical message-WAL projection MailboxService._projectMessageWalRecord Normalize every direct identity before checks/restoration/links; replay cannot recontaminate Existing fail-closed projection error and pending repair behavior Method JSDoc Legacy-record projection + repair specs
Read compatibility retirement #15032 read-path contract 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.
  • Focused specs cover dry-run, apply, idempotence, wrong-type/missing canonical targets, duplicate collisions, transaction rollback, direct-message replay, broadcast delivery replay, and permission edges.
  • 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.
  • Changing RLS user_id normalization (#13578).
  • Re-keying Chroma or AGENT_MEMORY identity metadata (#13827 / renameAgentIdentities.mjs).
  • Generic canonicalization inside GraphService.linkNodes(); graph endpoints are not universally identities.
  • Removing #15032's read compatibility before an applied clean-census gate.

Avoided Traps

  • Do not treat #10259's hard-coded alias map as a permanent identity registry.
  • Do not run a live production mutation from tests or from the PR.
  • Do not drop a colliding DELIVERED_TO edge before merging its graph-owned mutable state.
  • Do not "simplify" reads in the same deployment window without proof that all persisted stores crossed the migration.
  • Do not canonicalize every colon-bearing string; that would absorb mailbox grammar into the graph primitive and could change authorization.

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