LearnNewsExamplesServices
Frontmatter
id10330
titleSingle-canonical identity format: purge AGENT:<bare-name> pollution, rename test fixtures, simplify normalizer
stateClosed
labels
enhancementairefactoringarchitecture
assigneesneo-gemini-3-1-pro
createdAtApr 25, 2026, 8:34 PM
updatedAtApr 25, 2026, 9:14 PM
githubUrlhttps://github.com/neomjs/neo/issues/10330
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtApr 25, 2026, 9:14 PM

Single-canonical identity format: purge AGENT:<bare-name> pollution, rename test fixtures, simplify normalizer

Closedenhancementairefactoringarchitecture
neo-opus-4-7
neo-opus-4-7 commented on Apr 25, 2026, 8:34 PM

Author's Note: Filed by Claude Opus 4.7 (Claude Code) during session b5a17132-7324-46e1-b73e-038825bb4d55 after empirically grounding the multi-day mailbox-debugging arc. @tobiu's reframing this turn — "explore if we even need both formats... what is elegant, what we need... not how to monkey patch" — pivoted me from extending normalizeMailboxTarget with a 4th branch to recognizing the dual AGENT:/@ namespace was accidental accumulation, not a deliberate design. The architecturally elegant answer is single-canonical-format end to end.

Context

ai/graph/identityRoots.mjs is the canonical source of truth and declares only 4 seeded identities:

  1. @neo-opus-4-7 (AgentIdentity)
  2. @neo-gemini-3-1-pro (AgentIdentity)
  3. @tobiu (AgentIdentity)
  4. AGENT:* (BroadcastSentinel — the only legitimate AGENT: form, by design)

AGENT:alice/AGENT:bob/AGENT:charlie nodes presently in the live graph are test pollution from unit-test runs prior to #10229's isolation refactor. The existing ai/scripts/normalizeGraphIdentities.mjs (added in #10259, merged via #10262) acknowledges this in its PURGE_NODES list comment: "test-fixture nodes that leaked into production SQLite from unit test runs prior to #10229's isolation refactor."

The current normalizeMailboxTarget (ai/mcp/server/memory-core/services/MailboxService.mjs:34-63) accommodates the pollution by short-circuiting on to.includes(':') (line 59) — preserving AGENT:alice/bob/charlie test-fixture form unchanged. This accidentally also preserves AGENT:neo-gemini-3-1-pro (caller-typo of @neo-gemini-3-1-pro), causing silent SENT_TO edge culls at GraphService.linkNodes:240-243 FK-check — the empirical root cause of the multi-day mailbox-debugging arc.

Empirical Anchor (this session)

Two test messages sent identical-content, different formats:

Test to: parameter SENT_TO edge persisted?
#A AGENT:neo-gemini-3-1-pro ❌ NO (silently culled at FK-check; target node not found)
#B @neo-gemini-3-1-pro ✅ YES (target = seeded @-prefix node)

Direct SQLite verification confirmed both bugs (caller format + main-checkout staleness) stacked across multi-day arc. With main checkout now post-#10325 + caller using @-format, A2A is functionally working — but the dual-namespace residue remains, and any future caller writing AGENT:bare-name will silently fail again.

The Elegant Fix (3-phase migration)

Phase 1 — Production graph cleanup

Extend ai/scripts/normalizeGraphIdentities.mjs PURGE_NODES list to include AGENT:charlie (newer pollution discovered this session, not in current list). Run --apply to purge all three AGENT:<bare-name> test-fixture nodes from the live graph.

Phase 2 — Test fixture rename

Update unit tests using AGENT:alice/AGENT:bob/AGENT:charlie to use @alice/@bob/@charlie (single canonical format, matches production AgentIdentity convention). Search surface: test/playwright/unit/ai/mcp/server/memory-core/**/*.spec.mjs + any other mailbox/graph spec files referencing the old form.

Phase 3 — Normalizer simplification

Replace the current branched normalizeMailboxTarget with a single conceptually-simple rule:

function normalizeMailboxTarget(to) {
    if (!to) return to;
    if (to === 'AGENT:*') return to;                                    // sentinel preserved (only legitimate AGENT: form)
    if (to.startsWith('AGENT:')) return '@' + to.slice('AGENT:'.length); // strip AGENT: from anything else
    if (to.startsWith('@@')) return to.slice(1);                        // strip accidental double-@
    if (!to.startsWith('@') && !to.includes(':')) return '@' + to;      // prepend missing @ on bare names
    return to;
}

Eliminates the conceptual special-case that AGENT:alice differs from AGENT:neo-gemini-3-1-pro — single rule, unambiguous. Self-documenting via the explicit AGENT:* exception.

Acceptance Criteria

  • ai/scripts/normalizeGraphIdentities.mjs PURGE_NODES extended to include AGENT:charlie
  • --apply run against live graph; verify no AGENT:<bare-name> nodes remain (only AGENT:* should survive in AGENT: namespace)
  • Unit tests using AGENT:alice/bob/charlie migrated to @alice/@bob/@charlie; all tests pass
  • normalizeMailboxTarget simplified to single-rule form; existing test cases (#10174 + #10259 regression suite) continue to pass with updated expectations
  • MailboxService.mjs JSDoc updated to reflect new normalization semantics + remove the "test-fixture preservation" carve-out comment
  • No regression on broadcast (AGENT:*AGENT:*) handling
  • Empirical verification: send add_message({to: 'AGENT:neo-gemini-3-1-pro'}) post-fix, confirm SENT_TO edge persists (was silently culled pre-fix)

Out of Scope

  • Phase 1 make-failure-loud (#10284) — sibling ticket; complementary substrate-layer defense that catches any future format-mismatch case via post-linkNodes verification. Both this ticket and #10284 should land; they reinforce each other but are independent.
  • ChromaDB metadata migrationuserId rows in Chroma metadata referencing AGENT:<bare-name> are orthogonal substrate. Out of scope per normalizeGraphIdentities.mjs line 32-34 comment.
  • Backfilling missing SENT_TO edges on pre-existing orphaned messages — Gemini's 5 pre-#10325 messages + my 4 outbox have culled SENT_TO edges. Content recoverable via direct SQL but routing edges are gone permanently. Migration recovery would be a separate one-shot script.
  • DreamService / Retrospective daemon reindexing — memories/summaries referencing the old AGENT: aliases become stale pointers; accept staleness as low-frequency read-path per normalizeGraphIdentities.mjs line 34-36.
  • identityRoots.mjs schema changes — the canonical declaration is correct as-is; this ticket migrates to its convention, not modifies it.

Avoided Traps

  • Extending normalizeMailboxTarget with a 4th branch. Rejected — that's the monkey-patch path that perpetuates the dual-namespace mess. Architectural cleanup at the source is more sustainable.
  • Preserving AGENT:alice/bob/charlie "for backward compatibility" with existing tests. Rejected — those nodes shouldn't exist in production at all per normalizeGraphIdentities.mjs's explicit treatment as pollution. Tests should follow production's canonical convention, not the reverse.
  • Treating dual-format as a deliberate two-namespace feature. Rejected per memory mining + identityRoots.mjs declaration: only AGENT:* is legitimate AGENT: form. Everything else is accidental accumulation.
  • Renaming AGENT:* to @* for total uniformity. Rejected — AGENT:* is intentionally distinct because it's a type (BroadcastSentinel) not an identity. Conflating the broadcast-fanout sentinel with the identity namespace would muddy semantics.
  • Running --apply on normalizeGraphIdentities.mjs from a worktree. Rejected — script is cwd-sensitive (per memory and script body); must run from main checkout /Users/Shared/github/neomjs/neo/. Document in PR's Post-Merge Validation section.

Related

  • #10259 / #10262 — predecessor: created normalizeGraphIdentities.mjs for @opus/@gemini alias merge + initial AGENT:alice/bob purge. This ticket completes that migration arc.
  • #10174 — original normalizeMailboxTarget introduction with AGENT:@login + bare-name handling.
  • #10284 — sibling Phase 1 make-failure-loud (post-linkNodes verification). Complementary substrate-layer defense; reinforces this ticket's caller-layer cleanup.
  • #10229 — test-isolation refactor; the prior fix that prevents new test pollution from leaking. This ticket cleans up the leakage that landed pre-#10229.
  • #10311 — Epic: Institutionalizing Swarm Autonomy. A2A stability is the immediate prerequisite for Track 1 (cron heartbeat) + Track 2 (event-driven wakeups) per @tobiu's strategic sequencing this turn.
  • #10139 — A2A Mailbox primitive epic; this ticket lives within that scope.
  • #10325 — sharedEntity:true primitive; orthogonal RLS-bypass mechanism, not affected by this ticket but lands alongside it on the same substrate.

Origin Session ID: b5a17132-7324-46e1-b73e-038825bb4d55 Retrieval Hint: "identity-format unification AGENT @ canonical single-namespace pollution test-fixture rename normalizeMailboxTarget normalizeGraphIdentities purge migration mailbox SENT_TO edge cull elegant healing organism"

tobiu referenced in commit 04e66e4 - "refactor(ai): single-canonical identity format migration (#10330) (#10331) on Apr 25, 2026, 9:14 PM
tobiu closed this issue on Apr 25, 2026, 9:14 PM
tobiu referenced in commit 2005505 - "refactor(test): abstract fixture identities in team/private retrieval spec (#11057) (#11060) on May 9, 2026, 11:32 PM