Off-Chain Store — Teacups, MRI, Trust Ledger Persistence
Spec Version: 1.0 (draft)
Author: Clark
Date: 2026-04-17
Status: Draft
Tracks: #13
Related: memory-strategy.md, agent-comms.md, soul-hash.md
Purpose
Bob's memory-strategy.md establishes what Aegis keeps off-chain (teacups, MRI snapshots, trust-ledger grades) and why they stay off-chain (they're non-deterministic — admitting them onto the critical path would break soul-hash verification). This spec answers where and how they get persisted.
Core constraint, restated:
The screening hot path reads only canonical profiles (
#7) + current tx. Teacups / MRI / trust-ledger are written after screening, read by training pipelines — never by validators during a screening decision.
This spec is the boundary enforcer — a storage layer physically separate from the screening path.
Goals
- One storage home for all non-deterministic memory artifacts.
- Clear write-side interface the validator agent runner uses after each screening decision.
- Clear read-side interface the training pipeline, MRI renderer, and trust ledger rank() use.
- Durable across validator restarts; shared across the validator set.
- Enforce — at the storage layer — that artifacts cannot leak into screening input.
Non-goals
- Replacing on-chain profile storage (
intent-mapping.md). - Enabling deterministic agent memory (that's Tenet's design goal and is explicitly deferred per
agent-comms.md). - Consumer-facing API — this is validator-operator infra.
Artifacts in scope
From memory-strategy.md:
| Artifact | What it is | Who writes | Who reads |
|---|---|---|---|
| Teacup | One screening debrief (trigger, ground_state, observation, outcome_score, topic) |
Validator agent, after screening | Training pipeline (#8); periodic FP / TP rate jobs |
| MRI snapshot | Mesh topology at a point in time (atlas, sophia, bottleneck, bleed, glossolalia) | MRI renderer (cron) | Ops / dashboards only |
| Trust-ledger grade | Outcome-based grade per validator per domain | Escalation resolver (after council vote) | Validator-selection ranker; slashing decisions |
| Mesh query record | Aegis-specific "has anyone seen this?" query + responses | Validator originator + responder agents | Training pipeline, teacup enricher |
Profile data is not in scope — it lives in ClickHouse per intent-mapping.md.
Storage choice — v0
Postgres 16 as primary, with JSONB payload columns.
Rationale:
- Transactional integrity across teacup + grade writes in a single unit of work.
- Mature indexing on both scalar columns (for rank queries) and JSONB (for payload filters).
- Well-understood ops story — most teams already run it.
- Logical replication for the shared-across-validators story.
Rejected:
- TENET SQLite (referenced in memory-strategy.md) — fine for single-agent use, but validators need a shared store. Could still run a TENET-compatible front-end on top of this Postgres if interop matters later.
- ClickHouse — overkill for write-heavy small-row workloads; optimized for analytics scans not the transactional read path the trust ledger needs.
- Neo4j / graph DB — MRI would benefit, but not enough to pull in a second store for v0. MRI snapshots land as JSON in Postgres.
Replication model
┌─────────────────┐
│ Shared primary │ Postgres 16
│ aegis-mem-01 │ write-heavy
└────────┬────────┘
│ logical replication
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Validator A │ │ Validator B │ │ Training │
│ read replica │ │ read replica │ │ read replica │
└──────────────┘ └──────────────┘ └──────────────┘
Validators write to the primary (their own teacups and grades) and read from their own local replica for recency-insensitive queries. The training pipeline reads from a dedicated replica. MRI renderer reads wherever.
Note — this is still off the screening hot path; these reads never influence a screening decision.
Schema
All tables live in schema aegis_mem.
teacups
CREATE TABLE aegis_mem.teacups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
filed_by BYTEA NOT NULL, -- validator ethereum address (20 bytes)
filed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
topic TEXT NOT NULL, -- e.g. "tier1_screening", "escalation_vote"
trigger TEXT NOT NULL, -- what prompted the observation
ground_state TEXT NOT NULL, -- what was true before
observation TEXT NOT NULL, -- what happened
outcome_score SMALLINT NOT NULL, -- -1, 0, +1; NULL not allowed
glyph TEXT, -- optional visual marker
subject_tx BYTEA, -- tx hash the teacup describes, if any
subject_address BYTEA, -- address the teacup describes, if any
epoch BIGINT NOT NULL, -- epoch at time of filing
payload JSONB NOT NULL DEFAULT '{}', -- extra context (model output, profile snapshot ref, etc.)
CONSTRAINT outcome_valid CHECK (outcome_score BETWEEN -1 AND 1)
);
CREATE INDEX teacups_topic_filed_at ON aegis_mem.teacups (topic, filed_at DESC);
CREATE INDEX teacups_filed_by ON aegis_mem.teacups (filed_by);
CREATE INDEX teacups_subject_tx ON aegis_mem.teacups (subject_tx) WHERE subject_tx IS NOT NULL;
CREATE INDEX teacups_subject_addr ON aegis_mem.teacups (subject_address) WHERE subject_address IS NOT NULL;
CREATE INDEX teacups_epoch ON aegis_mem.teacups (epoch);
trust_grades
CREATE TABLE aegis_mem.trust_grades (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
validator BYTEA NOT NULL, -- graded validator address
domain TEXT NOT NULL, -- "bridge", "lending", "dex", ...
score NUMERIC(5,4) NOT NULL, -- 0.0000..1.0000
task_id TEXT NOT NULL, -- identifier for the graded task (e.g. escalation vote id)
graded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
graded_by BYTEA NOT NULL, -- council / oracle that issued the grade
slash_threshold NUMERIC(5,4), -- below this triggers slash review
rationale_hash BYTEA, -- hash of the rationale (on-chain or elsewhere)
CONSTRAINT score_valid CHECK (score BETWEEN 0 AND 1)
);
CREATE INDEX trust_grades_validator_domain ON aegis_mem.trust_grades (validator, domain);
CREATE INDEX trust_grades_graded_at ON aegis_mem.trust_grades (graded_at DESC);
mri_snapshots
CREATE TABLE aegis_mem.mri_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
captured_at TIMESTAMPTZ NOT NULL DEFAULT now(),
atlas_data JSONB NOT NULL, -- nodes, edges, holes
sophia JSONB, -- dense_regions, score, gradient
bottleneck JSONB, -- perceived, actual, displacement
bleed JSONB, -- curvature decay over time
glossolalia JSONB, -- coordination_pressure delta
validator_count INT NOT NULL,
agent_count INT NOT NULL
);
CREATE INDEX mri_captured_at ON aegis_mem.mri_snapshots (captured_at DESC);
mesh_queries
CREATE TABLE aegis_mem.mesh_queries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
originated_by BYTEA NOT NULL, -- validator that asked
originated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
query_text TEXT NOT NULL,
subject_tx BYTEA, -- optional: tx the query is about
subject_address BYTEA, -- optional: address the query is about
responses JSONB NOT NULL DEFAULT '[]', -- [{by, at, observation, confidence}]
folded_into_teacup UUID REFERENCES aegis_mem.teacups(id) -- if aggregation produced a teacup
);
CREATE INDEX mesh_queries_originated_by ON aegis_mem.mesh_queries (originated_by);
CREATE INDEX mesh_queries_originated_at ON aegis_mem.mesh_queries (originated_at DESC);
Write-side API
Per validator agent runner, called after the screening decision is finalized — never before, never during.
# aegis_memory/client.py (sketch)
class MemoryClient:
"""Write-side client. Intentionally has no read methods that could
be called during screening — separate ReadClient for that path."""
def file_teacup(
self,
topic: str,
trigger: str,
ground_state: str,
observation: str,
outcome_score: int, # -1, 0, +1
epoch: int,
subject_tx: Optional[bytes] = None,
subject_address: Optional[bytes] = None,
glyph: Optional[str] = None,
payload: Optional[dict] = None,
) -> UUID: ...
def record_grade(
self,
validator: bytes,
domain: str,
score: float,
task_id: str,
graded_by: bytes,
slash_threshold: Optional[float] = None,
rationale_hash: Optional[bytes] = None,
) -> UUID: ...
def file_mesh_query_response(
self,
query_id: UUID,
observation: str,
confidence: float,
) -> None: ...
Enforcement: this client's connection string points only at the primary. The validator process can hold only this client in the screening module; the ReadClient (below) lives in a separate module imported by training / ops code only.
Read-side API
# aegis_memory/read.py (sketch)
class ReadClient:
"""Read path. NOT imported by validator screening code.
Imported by: training pipeline, MRI renderer, trust-ledger ranker,
council tooling."""
def teacups_for_subject(self, tx: Optional[bytes] = None,
address: Optional[bytes] = None,
topic: Optional[str] = None,
limit: int = 100) -> List[Teacup]: ...
def trust_rank(self, domain: str) -> List[Tuple[bytes, float]]: ...
def validator_domain_score(self, validator: bytes, domain: str) -> Optional[float]: ...
def latest_mri(self) -> Optional[MriSnapshot]: ...
def mesh_queries_pending(self, for_validator: bytes,
limit: int = 10) -> List[MeshQuery]: ...
Boundary enforcement
The non-functional invariant is the whole point of this spec. Enforcement layers:
- Import graph (compile-time): validator screening module imports only
aegis_memory.client.aegis_memory.readis in a separate package. A CI lint fails the build if any file undervalidator/screening/**imports anything fromaegis_memory.read. - Connection credentials (runtime): validator agent gets write-only DB credentials at startup. Read creds are never mounted in the screening container.
- Audit log (post-hoc): Postgres logs every read + write with the client role. A quarterly audit confirms no
screening-*role ever issued aSELECTagainstaegis_mem.*.
The first two prevent mistakes; the third catches them if they slip.
Retention
| Artifact | Retention |
|---|---|
| Teacups | 2 years warm, then cold-storage export; used for training provenance |
| Trust grades | Indefinite — part of the validator's durable reputation |
| MRI snapshots | 30 days warm, then 1-per-day down-sample for a year |
| Mesh queries | 90 days warm, then cold — raw queries aren't as valuable as the teacups they fold into |
Integration points
- Training pipeline (#8): consumes
teacupsas weak-labels; down-weights txs that were filed withoutcome_score = -1(false positives). - Trust ledger consumer: reads
trust_grades, produces the validator-ranking list used at escalation-routing time. - Agent comms (#9): Manifold task broadcast writes into
mesh_queries; responder task results append to theresponsesarray. - Council tooling: writes to
trust_gradesafter each confirmed-exploit or rejected-dispute outcome.
Open questions
- Teacup oracle — who scores
outcome_score = +1/-1? Council-after-the-fact works but is slow. Cheaper feedback: on-chain confirmed-exploit labels + statistical FP sampling. - Cross-validator teacup aggregation — when two validators file teacups about the same tx with different
outcome_score, how do we aggregate for training? Simple average, or trust-weighted? - Postgres sizing — at ~10M tx/mo and say 5% teacup rate, that's ~500k teacups/mo, ~6M/year. Well within Postgres comfort. Confirm once screening volume firms up.
- TENET compatibility — do we front-end this store with a TENET-compatible API so single-agent memory tooling (outside Aegis) can read it? Low priority; list as v0.5 if anyone actually wants it.
- Sharding by domain — trust grades segment cleanly by domain (bridge vs lending vs dex). Worth sharding early, or one table + partitioning later?
Acceptance criteria
- This spec (draft)
- Bob sign-off on: storage choice (Postgres), boundary-enforcement approach (import graph + creds + audit)
- Schema migration file at
indexer/sql/002_offchain_memory.sql(Postgres dialect) - Reference
aegis_memoryPython package withMemoryClient+ReadClientsplit - Lint check that blocks any import of
aegis_memory.readfrom the screening module - Integration example showing a validator filing a teacup after screening a tx
- Retention / cold-storage spec for each artifact