# Memory Architecture — Stress Tests

**Date:** 2026-04-25
**Companion to:** ADR-033 (layered memory), `cognee-vs-build-study.md`
**Purpose:** Prove the four-layer model survives contact with (a) extensions whose profiles look nothing like classroom, (b) federation at 10k–100k nodes, (c) classification + export-control regimes. Each section tries to break the architecture; surviving stress-tests is the precondition for committing to the design at a load-bearing level.

> Memory is what will set Axiom apart. The cost of getting it wrong shows up everywhere; the cost of nailing it compounds across every extension. This doc is the deliberate attempt to break the design before anyone implements it.

---

## Part I — Multi-extension consumption sketches

The ADR profiles three extensions at one paragraph each. This section walks each one's actual API consumption end-to-end, identifying where the protocol pinches.

### A. Chat working memory (Profile C, deeper)

Today the chat extension has ad-hoc working-memory in the agent's runtime state. Under ADR-033 it consumes the four layers like this:

```mermaid
flowchart LR
    USER[user message]:::user
    AGENT[chat agent]:::ext
    CS[CompositionService.write]:::core
    L1[(L1 event store<br/>conversation_turn fragment<br/>scope = conversation_id<br/>cognitive_type = episodic)]:::store
    EXT[ConceptExtractor for chat<br/>NER + topic + intent]:::core
    L2[(L2 graph<br/>scoped to conversation_id)]:::store
    PROJ[WorkingMemoryProjection<br/>task: respond_to_user<br/>params: turn_window=10]:::proj
    LLM[LLM call with projected context]:::ext

    USER --> AGENT
    AGENT --> CS
    CS --> L1
    L1 --> EXT
    EXT --> L2
    AGENT --> PROJ
    L1 --> PROJ
    L2 --> PROJ
    PROJ --> LLM
    LLM --> AGENT

    classDef user fill:#5a3d1e,color:#fae8d8,stroke:#b07a4a
    classDef ext fill:#3d2e5a,color:#f0e8fa,stroke:#7a5fb0
    classDef core fill:#1e3a5f,color:#e8f0fa,stroke:#4a7ab8
    classDef store fill:#0d2a1f,color:#e8f5ee,stroke:#5a8a72
    classDef proj fill:#2a1a0d,color:#fae8d8,stroke:#b07a4a
```

**Where the protocol pinches:**

- **Per-turn extraction is too expensive.** Chat agents do many turns per minute. Running an LLM extractor synchronously on every `conversation_turn` write blows the response budget.
  - **Resolution:** Extractor execution is async by default. `ConceptExtractor.extract` may return immediately and queue the work; the graph catches up out-of-band. Projections must tolerate "graph slightly behind log" — they already do this naturally because they read both layers as snapshots at projection time.
- **Working memory wants the most recent N turns *verbatim*, not just concepts.** The graph is for cross-conversation patterns; the prompt context is for in-conversation continuity.
  - **Resolution:** `WorkingMemoryProjection` composes `RecentActivityProjection(window=N)` (raw fragments) + `RelevantConceptsProjection(linked_to=current_topic)` (graph traversal). Both feed the prompt builder. The projection layer's job is composition.
- **Conversations are sometimes ephemeral.** A short-lived support chat shouldn't litter the graph forever.
  - **Resolution:** Per-scope retention policy at L1 (already in MIRIX retention tiers: `ACTIVE → COMPRESSED → ARCHIVED`). The chat extension declares `retention=session` for ephemeral conversations; tombstones cascade through L1 → L2 on retention expiration. This needs to be added to L1's existing retention mechanism, but the hook point exists.

**Verdict:** The protocol fits. Two additions needed: async extractor execution and explicit retention policies threaded through the layers. Both are extensions of mechanisms already in the codebase.

### B. Research-loop / CURIO extension

Research loops have a fundamentally different shape from classroom: few large structured events, gigabytes of blob payload per event, multi-month timelines, cross-lab federation with strict pre-publication boundaries.

```mermaid
flowchart TB
    H[hypothesis_proposed event<br/>blob_refs: rationale.md, prior_findings/]:::evt
    E[experiment_run event<br/>blob_refs: input_params.json,<br/>output_dataset.parquet,<br/>simulation.log]:::evt
    F[finding_recorded event<br/>blob_refs: paper_draft.tex, plots/]:::evt
    R[retraction event<br/>refs: F]:::evt

    H -.cited_by.-> E
    E -.produced.-> F
    R -.tombstones.-> F

    SEXT[schema-driven extractor<br/>for experiment_run]:::ext
    LEXT[LLM extractor<br/>for hypothesis + finding text]:::ext

    GP[L2 graph<br/>concepts: variable_names,<br/>methods, citations,<br/>findings]:::store

    H --> LEXT
    F --> LEXT
    E --> SEXT
    LEXT --> GP
    SEXT --> GP

    classDef evt fill:#1e3a5f,color:#e8f0fa,stroke:#4a7ab8
    classDef ext fill:#3d2e5a,color:#f0e8fa,stroke:#7a5fb0
    classDef store fill:#0d2a1f,color:#e8f5ee,stroke:#5a8a72
```

**Where the protocol pinches:**

- **Blob refs may point to gigabytes.** The L1 fragment is small (metadata + refs); the blob backend must handle large content efficiently.
  - **Resolution:** `BlobStore` protocol is content-addressed and pluggable. `FilesystemBlobStore` for v0; SeaweedFS for Server+ profiles. Extensions never load blobs into memory speculatively — they fetch on demand at projection time.
- **Extraction over experiment_run is schema-driven, not LLM.** Variable names, parameter ranges, output column headers are structured; running an LLM on them is wasteful and lossy.
  - **Resolution:** Multiple extractors per fragment type. `experiment_run` registers `SchemaExtractor` (deterministic, fast, exact) plus optionally `LLMExtractor` for the free-text rationale fields. Extractors run in declared order; their outputs merge with conflict resolution policy (schema wins on overlap by default).
- **Pre-publication tag is critical.** Findings before peer review must not federate, even to trusted peer labs.
  - **Resolution:** Per-fragment `VisibilityHorizon` set at write. Default `SCOPE_INTERNAL` for `finding_recorded` (the research extension's "pre-publication" alias); explicit operator action promotes to `PUBLIC` (the "published" alias). Federation gateway respects the horizon; the extension's vocabulary is just an alias on top.
- **Replay matters more than for chat.** "What did we know about this hypothesis as of last quarter's report?" is a real question.
  - **Resolution:** Time-travel projection. `TaskSpec.as_of` parameter routes to `EventStore.list(scope, until=timestamp)` and `ConceptGraph.snapshot_at(timestamp)`. Both are first-class operations in the protocols.

**Verdict:** Profile B fits the protocols cleanly. The schema-driven extractor case validates that the `ConceptExtractor` protocol is properly LLM-agnostic. The blob-ref pattern survives gigabyte-scale.

### C. NeutronOS-shape domain extension (consumer of Axiom + Vega)

This is the test that matters most for downstream sovereignty. NeutronOS will eventually have its own classroom-shape extensions plus operational extensions (reactor sim runs, ALARA records, equipment logs). Per `feedback_axiom_domain_agnostic`, axiom core never names this consumer; per `project_axiom_co_ownership`, the IP boundary matters.

The architectural question: **does the four-layer model hold up when the consumer has classification + export-control + foreign-national-access constraints we don't have in classroom?**

```mermaid
flowchart TB
    subgraph DOM[Domain extension layer NOT in axiom core]
        SR[sim_run_completed event<br/>classification: CUI<br/>export: EAR cat 0E982<br/>part_810: applicable]:::evt
        ER[equipment_log_entry event<br/>classification: unclass<br/>export: none]:::evt
        AL[alara_record event<br/>classification: unclass]:::evt
    end
    subgraph CORE[Axiom core protocol]
        L1Q[EventStore.write<br/>checks per-fragment<br/>classification stamp]:::core
        L4Q[FederationGateway<br/>checks classification<br/>+ visibility horizon<br/>+ peer clearance<br/>+ peer nationality]:::core
    end
    subgraph PEER[Peer node]
        ACC[Acceptance / rejection<br/>per peer's domain capacity]:::peer
    end

    SR --> L1Q
    ER --> L1Q
    AL --> L1Q
    L1Q --> L4Q
    L4Q --> ACC

    classDef evt fill:#5a1e1e,color:#fae8e8,stroke:#b04a4a
    classDef core fill:#1e3a5f,color:#e8f0fa,stroke:#4a7ab8
    classDef peer fill:#3d2e5a,color:#f0e8fa,stroke:#7a5fb0
    style DOM fill:#2a0d0d,color:#fae8e8,stroke:#b04a4a
    style CORE fill:#0d1a2a,color:#e8f0fa,stroke:#4a7ab8
    style PEER fill:#1a0d2a,color:#f0e8fa,stroke:#7a5fb0
```

**Where the protocol pinches:**

- **Classification stamp is per-fragment, not just per-horizon.** Earlier draft used a flat sharing-tag string; classified handling per `spec-classification-boundary.md` requires a richer stamp: level + compartments + export_control + proprietary + provenance. The visibility horizon is the writer's outflow intent; the classification stamp is the regulatory constraint.
  - **Resolution:** `VisibilityHorizon` enum and `ClassificationStamp` are *both* fragment-level fields, evaluated together in the federation gateway. Classification trumps horizon — a `PEERS_DECLARED` fragment marked CUI cannot leave the originating scope; the gateway's effective rule is `min(visibility.outflow_level, classification.allowed_outflow_level)`.
  - **ADR-033 update landed:** `MemoryFragment.visibility: VisibilityHorizon` and `MemoryFragment.classification: ClassificationStamp` are both core fields. Both immutable; both feed the federation gateway. Extensions specialize the abstract horizon in their own vocabulary (classroom's "cohort-private" is one extension-level alias for `SCOPE_INTERNAL`; research's "pre-publication" is another).
- **LLM extraction must respect classification.** Running an OpenAI-hosted extractor over CUI material is a non-starter. Per `spec-classification-boundary.md` "LLM operations are domain-scoped and must never cross."
  - **Resolution:** Extractor selection is classification-aware. `ConceptExtractor` declares its data-flow profile (which LLM provider, where it runs, what it logs); the registration layer matches extractor capability to fragment classification before invocation. CUI fragments only get extractors that run on the local Bonsai or in-enclave model. Unclass extractors never see CUI.
- **Validated classification (per `project_validated_classification_pattern`) wants a re-classifier loop.** "Declared unclass but contains export-controlled terms" is a real failure mode.
  - **Resolution:** `ClassificationValidator` is a periodic projection, not a synchronous gate. Runs on a cadence (daily for fresh content, weekly for older), proposes re-classification deltas, surfaces to operator. Deterministic gate stays untouched in the meantime; validator is advisory.
- **Air-gapped operating mode (Scenario S8) must be functional from day 1.** No silent failure when network isn't available.
  - **Resolution:** Federation gateway is a separate primitive from L1/L2/L3. An air-gapped node runs L1+L2+L3 fully; federation gateway is simply unavailable. No code paths assume reachability.

**Verdict:** The four-layer model holds, but the ADR needs explicit classification-stamp fields on `MemoryFragment` and a classification-aware extractor selection mechanism. Both are additive — they don't change the protocol shape, they enrich the data model.

---

## Part II — Federation scale: 10k–100k nodes

Per `project_federation_scale_target`, anything O(n²) at n=50k is an outage. The L4 federation gateway is the obvious surface; the less obvious surfaces are L2 graph sync and L3 cross-cohort projections.

### Where the architecture is naturally O(1) or O(log n)

- **Per-cohort isolation at L1.** `EventStore.list(scope=...)` is O(events_in_scope), independent of total node count. Cohorts are the natural shard.
- **Concept lookup by deterministic ID.** Hash-based concept identity means the same concept across nodes maps to the same ID. Cross-node concept join is O(1) hash equality, not O(n²) similarity scoring.
- **Trust profile evaluation.** Per-scope trust profile is loaded once, applied per-fragment locally; federation decisions are local to the gateway.

### Where the architecture risks blowing up

| Surface | Naive approach | Cost at n=50k | Mitigation |
|---|---|---|---|
| **Graph sync between peers** | Push entire concept set to each peer | O(concepts × peers) — 50k peers × 100k concepts = 5B push ops | Pull-based, hop-bounded: peers query specific concepts, gateway responds with concept + 1-hop neighborhood. Cost = O(query × hops). |
| **Federated concept search** | Broadcast to all peers, aggregate | O(peers) per query = 50k requests | Hash-based deterministic concept IDs let us first ask "do you have this concept?" via a Bloom-filter exchange (O(log peers) probabilistic), then query only the matches. |
| **Cross-cohort projection (e.g. peer_signal)** | Fetch all peer cohorts' concept stats | O(peers × concepts_in_response) | Restrict by topology depth: only direct trust-graph neighbors, not transitive. ADR-028 trust graph is the natural bound. |
| **Per-peer trust profile lookup at gateway** | Linear scan | O(peers) per request | Indexed by peer_id; O(1) hash lookup. Trivial. |
| **Concept extractor on join (large new peer)** | Re-extract everything | O(events_in_new_peer) | Concept extraction is per-fragment-write, not per-peer-join. New peer's events go through their own L1 → L2 pipeline locally; cross-cohort sharing is concept-level, not raw-event-level. |
| **Tombstone propagation across federation** | Push every tombstone to every peer | O(tombstones × peers) | Tombstones are local. Peers consuming a federated projection re-query at projection time; tombstones in source cohort just don't appear in the next projection. No active push needed. |

### Concrete invariants to enforce

1. **No federation operation iterates the full peer set.** Every federation call names the peers (or peer set) it talks to.
2. **Concept federation is hop-bounded.** Default 1-hop, max 2; deeper requires explicit policy.
3. **Sync is pull-based, not push-based.** Peers fetch what they need; nodes never proactively push to peers they don't know need it.
4. **Bloom filters or similar probabilistic structures for "does anyone in this set have X?" queries** when the set is large.
5. **Trust profile per peer-pair, indexed.** O(1) lookup at gateway.

### What the federation looks like at 50k nodes

- Each node maintains its own L1+L2+L3 fully.
- Each node knows its direct peers (typically 10s–100s, not 50k) and their trust profiles.
- Cross-cohort queries fan out to direct peers only; deeper traversal hops through peers' peers explicitly with policy gates at each hop.
- Concept identity is global (deterministic IDs), so concept-level joins across the federation don't require any global registry — they emerge from hash equality.
- **There is no "all-nodes" data structure anywhere in the protocol.** The closest is the trust graph (ADR-028) which is itself federated and topology-bounded.

This survives 100k. The choke point becomes operator overhead managing per-peer trust profiles, not protocol cost — and that's an interface problem, not an architecture problem.

---

## Part III — Classification + export control + proprietary

Per `spec-classification-boundary.md`, three regimes overlap independently: classification (SECRET/SCI), export control (ITAR/EAR/Part 810), proprietary (contract). Each has its own enforcement; access decisions check all applicable.

### How the layered model carries this

| Spec primitive | Where it lives |
|---|---|
| Per-content classification stamp | `MemoryFragment.classification` (immutable, set at write) |
| Principal clearance | `Principal.clearance` (signed, TTL'd, separate from identity) — already on the Axiom roadmap |
| Federation domain | `Scope.classification_floor` — scope cannot accept fragments above its floor |
| Cross-domain transfer | Federation gateway operation, explicit + audited per Invariant 4 |
| Air-gap | L1+L2+L3 work without L4; no implicit network calls |
| Compartmentalization | Compartments are tags on the classification stamp; gateway evaluates principal compartment intersection |

### The interaction this exposes that ADR-033 didn't address

`VisibilityHorizon` (one of `SCOPE_INTERNAL` / `REQUEST_GATED` / `PEERS_DECLARED` / `FEDERATION_BOUND` / `PUBLIC`) is the **outflow intent** the writer expresses. `ClassificationStamp` is the **regulatory constraint** the regime imposes. These can disagree, and when they do, classification wins:

```
effective_outflow = min(visibility.outflow_level, classification.allowed_outflow_level)
```

Examples:
- Writer marks fragment `PUBLIC`. Classification is `unclassified`. Outflow = `PUBLIC`. ✓ as written.
- Writer marks fragment `PUBLIC`. Classification is `CUI`. Outflow collapses to `SCOPE_INTERNAL` regardless. Federation gateway hard-rejects any peer projection request.
- Writer marks fragment `SCOPE_INTERNAL`. Classification is `unclassified`. Outflow = `SCOPE_INTERNAL`. ✓ as written.
- Writer marks fragment `PEERS_DECLARED`. Classification is `EAR cat 0E982, US-only`. Outflow = `PEERS_DECLARED but only US-clearance peers`. Federation gateway filters by peer principal nationality.

**The federation gateway must be expressed in two stages:** classification check first (regulatory), then sharing-tag check (operational). Order matters because classification can fail-close in ways the writer doesn't anticipate.

### Validated classification feeds the same primitive

The validator (per `project_validated_classification_pattern`) is a Layer 3 projection that runs on a cadence. It produces `ClassificationDelta` entries proposing re-classification — these are themselves L1 events with their own provenance, never silent overwrites of the original stamp. Operator approval (deterministic RACI gate) is required to apply the delta; the delta is written as a new `ReclassificationApplied` event, the original fragment's stamp is NOT mutated (immutability), and a tombstone covers any sharing that happened under the old stamp before the re-classification.

This pattern satisfies: deterministic gate, audit trail, no silent escalation, replayable history of what was classified what at what time.

### Air-gap is structural, not configurational

The federation gateway is a separate primitive (L4) with no inline calls from L1/L2/L3. An air-gapped node runs L1+L2+L3 normally; the gateway either isn't started or operates in `local_only` mode where every peer call returns "no peers configured." There is no code path where retrieval, projection, or graph operation requires a network call. This is an architectural property, not a deployment toggle.

### Foreign-national access (Scenario S7)

Access decisions take principal nationality as input. The principal model already supports signed nationality attestations (per `project_federation_authority_scope`). The federation gateway evaluates `(fragment.classification.export_control.authorized_nationalities ∩ principal.nationality)` as part of every cross-domain projection. Inside a single domain, nationality enforcement is fragment-classification-driven; cross-domain it composes with the principal's clearance.

---

## Part IV — Where the design is still soft (open questions to close before commitment)

Honest list of things this stress-test surfaced that ADR-033 needs to incorporate or that need their own follow-up:

1. **`MemoryFragment.classification` field** — must be added to the data model. Immutable, set at write, fed to federation gateway.
2. **Async extractor execution + retention policy in the layers** — protocol additions for chat profile.
3. **Multiple extractors per fragment type with conflict policy** — for research/structured-payload profile.
4. **Pull-based concept federation with Bloom-filter probabilistic peer query** — needed for 10k+ scale. Not in current ADR.
5. **`ClassificationValidator` projection + `ReclassificationApplied` event type** — validated classification pattern made concrete.
6. **Hop-bounded federation traversal with policy gate per hop** — the architecture allows this but the protocol needs an explicit `max_hops` parameter on federated queries.
7. **Per-extractor data-flow declaration** — extractors declare what LLM provider, where it runs, what it logs; registration layer matches capability to classification before invocation. Critical for CUI handling.
8. **Compartment intersection logic** — compartments are sets; gateway logic must compute intersections over principal compartments × fragment compartments. Standard but needs a primitive.

Each of these is additive to ADR-033, not a redesign. The four layers hold; the protocols need enrichment at specific points.

---

## What we conclude

The architecture is **load-bearing for all three stress-test dimensions** but not yet **complete**. ADR-033 is correct in its layer decomposition; the open questions above are the next iteration's work. None of them require redesigning the layers — they require enriching the data model and the federation gateway protocol.

For Prague v0, the work order should be:

1. Stage 1 of ADR-033 (adapter dual-write through CompositionService) — proceed as planned.
2. Cheapest-first-move (`RecentActivityProjection`) — proceed as planned.
3. Add `MemoryFragment.classification` field as a write-time concern even if classification enforcement isn't load-bearing for Prague — Stage 2 will need it for Cognee-or-not extraction routing.
4. Stage 2 (graph + extractor) — ship Axiom-owned implementations per the Cognee study. Include extractor data-flow declarations from day 1.
5. Stage 5 federation gateway — keep the open questions in this doc as the design input.

The thing we should NOT do is commit to the federation + classification primitives in code without first updating ADR-033 with the open-question resolutions. This doc is the design input for that update.

## Recommended ADR-033 amendment

Add a new section "Federation + classification primitives" that incorporates open questions 1, 5, 6, 7, 8 from this doc, and update Stage 5 in the migration path to reference it. The four-layer figure is unchanged; the protocol surfaces are enriched.
