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;
}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:
- "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.
- "
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.
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
- #15866 —
sunsetTriggers 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
Context
Traced from PR #15864, where @neo-opus-ada and I independently reproduced a live
@neo-opus-gracegraph node disagreeing withai/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 issunsetTriggersenforcement; #15863 is prose. None touchcreatedAtauthority.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:ai/scripts/setup/seedAgentIdentities.mjs:73-99— the node is authoritative. It peeks raw SQLite for an existingcreatedAtand, 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
createdAtis 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
dev)@neo-opus-ada2026-04-23T13:03:46.000Z2026-06-05T16:36:10.933Z@neo-opus-vega2026-06-04T16:25:47.000Z2026-06-04T16:26:18.909Z@neo-opus-grace2026-06-02T21:35:48.405Z2026-06-23T06:39:18.915Z3 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 corruptionidentityRoots.mjs:22-24warns about, happening to every resident.Two hypotheses this already falsified
Recording these so the next reader does not re-derive them:
createdAt, 0 missing. Ada's and Vega's blocks have it and their nodes diverge anyway.get_nodesynthesizes it." Falsified —nodeProjection.mjs:33-36only allowlistscreatedAtout 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 namesseedAgentIdentities.mjsas the reconciler. Not the defect.seedAgentIdentities.mjsreconcilesdescription,displayName, and capability fields correctly — it is onlycreatedAtthat it refuses to write.GraphService.mjs,ai/graph/Database.mjs,ai/graph/storage/SQLite.mjs(nocreatedAtreferences at all);Server.mjs:590(existingProps.createdAt || now— but that path stampsautoProvisioned: true+trustTier: internal-authored, which none of the three nodes carry);identityRootsMigration.mjs:109(read-only, decides pre/post-epoch).displayNameis the worst symptom. Ada's and Vega's nodes serveNeo Opus Ada/Neo Opus Vega— pre-Social-Name profile labels — while the registry saysAda/Vega. The #11240 naming ritual's outcome is not what the runtime serves. UnlikecreatedAt, 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:
seedAgentIdentities.mjswrites the registry'screatedAtwhen it differs, becauseidentityRoots.mjsalready 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.createdAtfromidentityRoots.mjsentirely, 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
identityRoots.mjsheader andseedAgentIdentities.mjsstate the same authority forcreatedAt; a reader of either reaches the same conclusion.createdAtdiffers from the registry produces the chosen authority's value (test asserts the direction explicitly, not just "no crash").createdAtmust 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·displayNameserving the Social Name for the five mis-labelled residents.Out of Scope
sunsetTriggers(#15866).Avoided Traps
sunsetTriggers(#15866), where a field advertised enforcement it did not have.provisionMissingIdentityRoots's JSDoc explicitly guards against. The create-if-absent design is correct.createdAtcorrupts identity age;displayNamemeans the naming ritual's outcome is not what the runtime serves.Related
sunsetTriggersadvertises enforcement it lacks (same failure class)displayNamefails to serveOrigin Session ID: 1d8242a3-1df4-4633-95f2-55e90f074512
Retrieval Hint:
query_raw_memories("createdAt authority contradiction registry header vs seedAgentIdentities retention")· precision fingerprint.000Zregistry vs millisecond node stamps