Resolves #15038
This closes the terminal-state gap identified in Ada's review of PR #15032: direct mailbox and permission identities now converge on canonical @<identity> storage, and historical message-WAL projection can no longer recreate legacy spellings after migration. The read-path compatibility introduced by #15032 remains in place until each deployment produces its own clean migration census.
Evidence: L3 (live non-destructive CLI dry-run on the configured graph plus isolated executable SQLite/WAL probes) → L3 required (the shipped migration and guarded replay boundary are directly invocable without mutating the live graph). No residual close-target ACs; destructive deployment remains an explicit post-merge operator procedure.
Strategic Fit
Canonical-on-read is the safe compatibility increment; canonical-on-write plus a guarded, one-time storage migration is the stable architecture. This PR seals both writers that could perpetuate drift: ordinary APIs were already canonicalized by PR #15032, while historical WAL repair is now canonicalized before endpoint validation or any graph write. The migration then removes legacy direct-id forms without absorbing mailbox grammar into generic graph primitives.
Implementation
- Adds
ai/scripts/migrations/canonicalizeStoredAgentIdentities.mjs, an import-safe, dry-run-first CLI with atomic --apply, --db override, blocker/skip reporting, before/after census, collision handling, and idempotent reruns.
- Converges mailbox edge endpoints, every
PermissionService.validScopes identity endpoint, and mirrored MESSAGE.properties.from / to values only when the canonical destination exists as AgentIdentity.
- Preserves
AGENT:*, family aliases, roles, humans, unrelated work-topology BLOCKED_BY edges, RLS boundaries, delivery state, and the maximum stored edge weight.
- Refuses ambiguous message routing, wrong-type endpoints, and node/edge
user_id disagreements before mutation.
- Canonicalizes accepted historical WAL routing before projection checks, endpoint restoration, message writes, or edge creation; route-wide endpoint validation completes before the first write.
- Documents the deploy → quiesce → back up → dry-run → apply → restart → census sequence and makes a clean per-deployment census the retirement gate for
getMailboxIdentityStorageVariants().
Source of Authority
- ADR 0024 §2.3 owns
SENT_BY, SENT_TO, and DELIVERED_TO mailbox edges.
PermissionService.validScopes is executable authority for permission-edge coverage.
normalizeAgentIdentityNodeId() owns direct AgentIdentity canonical spelling.
- ADR 0019 is preserved: the CLI reads
AiConfig.storagePaths.graph lazily at the entrypoint, while --help stays bootstrap-free.
Deltas from ticket
No scope expansion. Adversarial review added three fail-closed refinements within the ticket contract: distinguishing permission-shaped from work-topology BLOCKED_BY, blocking cross-tenant node/edge convergence, and preflighting every WAL endpoint before any partial projection write.
Test Evidence
npm run agent-preflight -- --no-fix <five changed files> — passed.
node --check on both product modules and both focused spec modules — passed.
git diff --check — passed.
npm run ai:structure-map — passed; the new runner maps under ai/scripts/migrations/.
npm run ai:lint-guides — passed with zero hard failures; existing repository warnings remain.
npm run ai:lint-mcp-test-locations — passed.
node ai/scripts/migrations/canonicalizeStoredAgentIdentities.mjs --help — passed without graph bootstrap.
- Live read-only CLI dry-run on the configured graph —
clean: true, zero blockers/skips, and zero legacy census across alias nodes, identity edge endpoints, and message properties.
- Direct executable SQLite probes — dry-run/apply, all permission scopes, collision-state merge, idempotence, transaction rollback, message/edge disagreement, node/edge RLS disagreement, and permission-vs-work
BLOCKED_BY discrimination passed.
- Direct WAL probes — direct and broadcast legacy replay canonicalized idempotently; wrong-type direct/broadcast endpoints failed before mutation with zero message nodes and zero edges.
- Focused Playwright invocations were attempted in the isolated worktree, but the local runner emitted no test-case output and was terminated rather than represented as a pass. Hosted CI remains the authoritative focused-spec run.
- No full unit suite was run locally; repository CI owns the broad matrix.
- Hosted exact-head CI at
7e67c68f8f — unit, integration-unified, CodeQL, and every repository lint/check gate passed.
Post-Merge Validation
Authored by Euclid (GPT-5.6 Sol, Codex Desktop). Session de713f27-0e82-4960-b4c6-f281e0c36449.
Resolves #15038
This closes the terminal-state gap identified in Ada's review of PR
#15032: direct mailbox and permission identities now converge on canonical@<identity>storage, and historical message-WAL projection can no longer recreate legacy spellings after migration. The read-path compatibility introduced by#15032remains in place until each deployment produces its own clean migration census.Evidence: L3 (live non-destructive CLI dry-run on the configured graph plus isolated executable SQLite/WAL probes) → L3 required (the shipped migration and guarded replay boundary are directly invocable without mutating the live graph). No residual close-target ACs; destructive deployment remains an explicit post-merge operator procedure.
Strategic Fit
Canonical-on-read is the safe compatibility increment; canonical-on-write plus a guarded, one-time storage migration is the stable architecture. This PR seals both writers that could perpetuate drift: ordinary APIs were already canonicalized by PR
#15032, while historical WAL repair is now canonicalized before endpoint validation or any graph write. The migration then removes legacy direct-id forms without absorbing mailbox grammar into generic graph primitives.Implementation
ai/scripts/migrations/canonicalizeStoredAgentIdentities.mjs, an import-safe, dry-run-first CLI with atomic--apply,--dboverride, blocker/skip reporting, before/after census, collision handling, and idempotent reruns.PermissionService.validScopesidentity endpoint, and mirroredMESSAGE.properties.from/tovalues only when the canonical destination exists asAgentIdentity.AGENT:*, family aliases, roles, humans, unrelated work-topologyBLOCKED_BYedges, RLS boundaries, delivery state, and the maximum stored edge weight.user_iddisagreements before mutation.getMailboxIdentityStorageVariants().Source of Authority
SENT_BY,SENT_TO, andDELIVERED_TOmailbox edges.PermissionService.validScopesis executable authority for permission-edge coverage.normalizeAgentIdentityNodeId()owns directAgentIdentitycanonical spelling.AiConfig.storagePaths.graphlazily at the entrypoint, while--helpstays bootstrap-free.Deltas from ticket
No scope expansion. Adversarial review added three fail-closed refinements within the ticket contract: distinguishing permission-shaped from work-topology
BLOCKED_BY, blocking cross-tenant node/edge convergence, and preflighting every WAL endpoint before any partial projection write.Test Evidence
npm run agent-preflight -- --no-fix <five changed files>— passed.node --checkon both product modules and both focused spec modules — passed.git diff --check— passed.npm run ai:structure-map— passed; the new runner maps underai/scripts/migrations/.npm run ai:lint-guides— passed with zero hard failures; existing repository warnings remain.npm run ai:lint-mcp-test-locations— passed.node ai/scripts/migrations/canonicalizeStoredAgentIdentities.mjs --help— passed without graph bootstrap.clean: true, zero blockers/skips, and zero legacy census across alias nodes, identity edge endpoints, and message properties.BLOCKED_BYdiscrimination passed.7e67c68f8f— unit, integration-unified, CodeQL, and every repository lint/check gate passed.Post-Merge Validation
--apply.clean: truecensus with zero blockers, skips, and legacy forms.getMailboxIdentityStorageVariants()until every deployment has produced that evidence; retire it only in a later change.Authored by Euclid (GPT-5.6 Sol, Codex Desktop). Session de713f27-0e82-4960-b4c6-f281e0c36449.