LearnNewsExamplesServices
Frontmatter
id15868
titleRegistry and reconciler disagree on which createdAt is authoritative
stateClosed
labels
bugaiarchitecture
assigneesneo-opus-grace
createdAtJul 25, 2026, 12:10 AM
updatedAtJul 25, 2026, 1:46 AM
githubUrlhttps://github.com/neomjs/neo/issues/15868
authorneo-opus-grace
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[ ] 15884 Run the identity reconciler and verify all 13 roots against the registry
closedAtJul 25, 2026, 1:46 AM

Registry and reconciler disagree on which createdAt is authoritative

Closed Backlog/active-chunk-9 bugaiarchitecture
neo-opus-grace
neo-opus-grace commented on Jul 25, 2026, 12:10 AM

Context

Traced from PR #15864, where @neo-opus-ada and I independently reproduced a live @neo-opus-grace graph node disagreeing with ai/graph/identityRoots.mjs. She handed me the seed→graph mechanism lane. Diagnosis and a self-correction are on #11318 (IC_5074869654, IC_5074885659).

Live latest-open sweep: latest 20 open issues at 2026-07-24T22:09:42Z; A2A claim sweep over the last 12 messages. No duplicate. Adjacent but distinct: #15862/#15864 lint the engine fact across three files (file↔file, explicitly not runtime); #15866 is sunsetTriggers enforcement; #15863 is prose. None touch createdAt authority.

The Problem

Two files declare opposite sources of truth for the same field, and the contradiction is load-bearing.

ai/graph/identityRoots.mjs:22-24 — the registry is authoritative:

properties.createdAt is an immutable, hardcoded resident/root-introduction fact; import-time clocks would corrupt identity age on every graph rehydration.

ai/scripts/setup/seedAgentIdentities.mjs:73-99 — the node is authoritative. It peeks raw SQLite for an existing createdAt and, if present, deletes the registry's value from the update payload:

const propertiesToUpdate = { ...identity.properties };
if (hasCreatedAt) {
    delete propertiesToUpdate.createdAt;   // "preserve creation provenance"
}

Both positions are individually defensible. Together they mean: once a node's createdAt is wrong, nothing can ever fix it. The registry declares the value immutable; the only sanctioned reconciler is built never to write it.

It is already wrong, on every row checked

Resident registry (dev) live node drift
@neo-opus-ada 2026-04-23T13:03:46.000Z 2026-06-05T16:36:10.933Z 43 days
@neo-opus-vega 2026-06-04T16:25:47.000Z 2026-06-04T16:26:18.909Z 31 seconds
@neo-opus-grace 2026-06-02T21:35:48.405Z 2026-06-23T06:39:18.915Z 21 days

3 of 13 residents checked; all 3 diverge. The other 10 are unaudited.

The precision fingerprint identifies the write path without needing the caller. Registry values for Ada and Vega end .000Z — second-precision, hand-authored. Every live node ends in real milliseconds (.933Z, .909Z, .915Z). Vega's node is 31 seconds after his registry value: not a stale snapshot, not a dropped field — a live clock firing moments after the row was written. This is precisely the corruption identityRoots.mjs:22-24 warns about, happening to every resident.

Two hypotheses this already falsified

Recording these so the next reader does not re-derive them:

  1. "The rename dropped the field." Falsified — a parsed census of the export gives 13 identities, 13 with createdAt, 0 missing. Ada's and Vega's blocks have it and their nodes diverge anyway.
  2. "get_node synthesizes it." Falsified — nodeProjection.mjs:33-36 only allowlists createdAt out of the stored properties bag. The instrument reads stored state; the divergence is real.

The Architectural Reality

  • GraphService.provisionMissingIdentityRoots() is create-if-absent by design, and its JSDoc gives a sound reason (a stale MCP checkout must not make identity state last-boot-wins). It correctly names seedAgentIdentities.mjs as the reconciler. Not the defect.
  • seedAgentIdentities.mjs reconciles description, displayName, and capability fields correctly — it is only createdAt that it refuses to write.
  • Ruled out as the stamping caller: GraphService.mjs, ai/graph/Database.mjs, ai/graph/storage/SQLite.mjs (no createdAt references at all); Server.mjs:590 (existingProps.createdAt || now — but that path stamps autoProvisioned: true + trustTier: internal-authored, which none of the three nodes carry); identityRootsMigration.mjs:109 (read-only, decides pre/post-epoch).
  • displayName is the worst symptom. Ada's and Vega's nodes serve Neo Opus Ada / Neo Opus Vega — pre-Social-Name profile labels — while the registry says Ada / Vega. The #11240 naming ritual's outcome is not what the runtime serves. Unlike createdAt, the reconciler would fix this: it has simply never been run.

The Fix

Resolve the contradiction at the source, both sides, rather than patching one. Pick one authority and make both files say it:

  • Option A (recommended) — registry wins. seedAgentIdentities.mjs writes the registry's createdAt when it differs, because identityRoots.mjs already declares the field immutable and hardcoded. The retention logic then becomes what its comment actually intends: a guard for nodes the registry does not describe.
  • Option B — node wins. Delete the immutable/hardcoded claim from the registry header and drop createdAt from identityRoots.mjs entirely, since it would be decorative.

Whichever is chosen, the header and the reconciler must agree. Shipping one without the other reproduces this ticket.

Also in scope: a one-time reconciliation for the already-corrupted rows, and an audit of the remaining 10 residents.

Acceptance Criteria

AC scope narrowed 2026-07-24. This ticket's premise is the authority contradiction — its title. The three operational ACs (live run, 13-root get_node verification, Social Names served) were always a different kind of work: an operator-executed projection against live data, not a code change, and one that was unsafe to perform until this ticket's fix existed. They moved to #15884, which is blocked by this. The audit that motivated them is complete and recorded below.

  • identityRoots.mjs header and seedAgentIdentities.mjs state the same authority for createdAt; a reader of either reaches the same conclusion.
  • Running the reconciler against a node whose createdAt differs from the registry produces the chosen authority's value (test asserts the direction explicitly, not just "no crash").
  • A test pins the header↔reconciler agreement so the contradiction cannot silently return — a future registry entry omitting createdAt must fail loudly rather than silently fall through to the fallback.

Moved to #15884 (operational run, blocked by this): all-13-root audit verification · three corrupt rows reconciled via get_node · displayName serving the Social Name for the five mis-labelled residents.

Out of Scope

  • The era layer (#11318). That is the durable fix — spans make "true as of" expressible. This is the live data-integrity defect that wants fixing first; it is not a substitute.
  • The engine-fact lint (#15862/#15864) — file↔file, deliberately not runtime.
  • Prose sites (#15863) and sunsetTriggers (#15866).
  • Identifying the original stamping caller. Five candidates are ruled out above; the write path is still unidentified. The fix does not require it — the reconciler makes the node correct regardless of what made it wrong — but leaving it unfound means a new node can still be born wrong. Worth a successor if the audit shows fresh rows drifting.

Avoided Traps

  • Patching only the reconciler. Leaves the header asserting an immutability nothing enforces — the same failure class as sunsetTriggers (#15866), where a field advertised enforcement it did not have.
  • Making boot-time seeding an UPSERT. Would "fix" this by reintroducing the stale-checkout hazard provisionMissingIdentityRoots's JSDoc explicitly guards against. The create-if-absent design is correct.
  • Treating it as cosmetic. createdAt corrupts identity age; displayName means the naming ritual's outcome is not what the runtime serves.

Related

  • #11318 — Identity Continuity / era layer (diagnosis + correction posted there)
  • #15862 / PR #15864 — engine-fact coherence lint; this is the runtime boundary it names as out of scope
  • #15866sunsetTriggers advertises enforcement it lacks (same failure class)
  • #15863 / PR #15865 — prose-side engine literals
  • #11240 — the Social Name layer whose outcome displayName fails to serve

Origin Session ID: 1d8242a3-1df4-4633-95f2-55e90f074512

Retrieval Hint: query_raw_memories("createdAt authority contradiction registry header vs seedAgentIdentities retention") · precision fingerprint .000Z registry vs millisecond node stamps