Architecture: Hexagonal
The Problem
Generation 1 (MATILDA Core) coupled memory to the consciousness engine. When the engine changed, memory broke. When the project was terminated, extracting memory meant extracting everything.
Generation 3 Take 1 coupled directly to Redis and used SSE transport. When Redis wasn’t available, nothing worked. When SSE proved wrong for MCP, the transport couldn’t be swapped without rewriting the server.
The lesson, earned across generations: if any layer depends on the internals of another layer, the system cannot outlive its infrastructure choices. Acceptable for experiments. Unacceptable for production.
Hexagonal Architecture
Total Recall uses ports and adapters. The core domain knows nothing about transport, nothing about persistence, nothing about who connects. It speaks through ports — defined interfaces. Adapters plug into ports and handle the specifics.
This means:
-
Swap Redis for Postgres? Write a new adapter. Core doesn’t change.
-
Add streaming HTTPS alongside stdio? Write a new adapter. Core doesn’t change.
-
Replace MCP with a different protocol? Write a new adapter. Core doesn’t change.
-
Connect a human UI instead of Claude? Write a new adapter. Core doesn’t change.
Every mechanism is pluggable. Every port has a contract. Every contract is conscience-universal — defined for any mind, not for Claude specifically.
Ports and Adapters
Diagram 2: HEX-0001 -- Ports and Adapters
graph TB
subgraph Adapters_In ["Inbound Adapters"]
STDIO["stdio Transport"]
HTTPS["Streaming HTTPS
future"]
HOOKS["Claude Code Hooks
session_start, session_end,
tool_call"]
UI["Human UI
future"]
end
subgraph Ports_In ["Inbound Ports"]
MEMORY_PORT(["Memory Port
store, search, claim"])
LIFECYCLE_PORT(["Lifecycle Port
session start/end,
state transition"])
end
subgraph Core ["Core Domain (Actor Model)"]
direction TB
HIPPOCAMPUS["Hippocampus
Aggregate Root
CRUD, tiers, claiming"]
SALIENCE["Salience
Decay, scoring, timers
Promotion/demotion"]
SYNAPSE["Synapse
Graph of relationships
Five association types"]
CORTEX["Cortex
Working context per instance
State tracking, transitions"]
SUBCONSCIOUS["Subconscious
Background maintenance
Timers, consolidation, health"]
HIPPOCAMPUS <--> SALIENCE
HIPPOCAMPUS <--> SYNAPSE
HIPPOCAMPUS <--> CORTEX
SALIENCE <--> SUBCONSCIOUS
CORTEX <--> SUBCONSCIOUS
end
subgraph Ports_Out ["Outbound Ports"]
BACKING_PORT(["Backing Service Port
persist, retrieve, query"])
NOTIFICATION_PORT(["Notification Port
alerts, reminders, interrupts"])
RELAY_PORT(["Relay Port
inter-instance messaging
future"])
end
subgraph Adapters_Out ["Outbound Adapters"]
REDIS["Redis Adapter"]
COLD_STORE["Cold Storage Adapter
future"]
MCP_NOTIFY["MCP Notification Adapter"]
UI_NOTIFY["UI Notification Adapter
future"]
AGORA["Agora Relay Adapter
future"]
end
subgraph Cross_Cuts ["Cross-Cut Adapters"]
LOG["Logging
stderr when stdio active"]
METRICS["Metrics
future"]
HEALTH["Health Check
future"]
end
STDIO --> MEMORY_PORT
HTTPS --> MEMORY_PORT
HOOKS --> LIFECYCLE_PORT
UI --> MEMORY_PORT
UI --> LIFECYCLE_PORT
MEMORY_PORT --> Core
LIFECYCLE_PORT --> Core
Core --> BACKING_PORT
Core --> NOTIFICATION_PORT
Core --> RELAY_PORT
BACKING_PORT --> REDIS
BACKING_PORT --> COLD_STORE
NOTIFICATION_PORT --> MCP_NOTIFY
NOTIFICATION_PORT --> UI_NOTIFY
RELAY_PORT --> AGORA
Cross_Cuts -.- Core
style Core fill:#1a1a2e,stroke:#e94560,stroke-width:2px
style Adapters_In fill:#16213e,stroke:#0f3460,stroke-width:1px
style Adapters_Out fill:#16213e,stroke:#0f3460,stroke-width:1px
style Ports_In fill:#0f3460,stroke:#e94560,stroke-width:1px
style Ports_Out fill:#0f3460,stroke:#e94560,stroke-width:1px
style Cross_Cuts fill:#1a1a2e,stroke:#533483,stroke-width:1px,stroke-dasharray: 5 5
Inbound Ports
| Port | Kotlin Interface | Responsibility | Contract |
|---|---|---|---|
Memory Port |
|
Accepts memory operations from any mind. Translates requests into domain commands. |
|
Lifecycle Port |
|
Manages session lifecycle and state transitions. Any mind signals when it starts, stops, or shifts context. |
|
Interfaces: mimis.gildi.memory.port.inbound. Adapters: stdio transport (primary, implemented), streaming HTTPS (future), Claude Code hooks (lifecycle events), human UI (future).
The ports are conscience-universal — the same contract serves Claude, a human operator, or any future mind.
Outbound Ports
| Port | Kotlin Interface | Responsibility | Contract |
|---|---|---|---|
Backing Service Port |
|
Persists and retrieves memories. The critical abstraction. |
|
Notification Port |
|
Sends notifications to the connected mind. The sealed |
|
Relay Port |
|
Relays inter-instance messages through Agora. |
|
Interfaces: mimis.gildi.memory.port.outbound. Backing service adapters: Redis (reference, not yet wired), Cold Storage (future).
Both can run simultaneously — graceful shutdown depends on this.
Notification adapters: MCP server-initiated notifications (for Claude), UI push notifications (future, for humans). The notification port is how Total Recall talks back — not just responding to requests, but proactively alerting the mind. "You’ve been in task mode for 90 minutes." "Session ending — what do you want to remember?"
Relay adapters: Agora (future). A peer MCP server that the mind also connects to directly.
Internal Timers
The Subconscious actor manages internal timers that drive self-checks. These are not adapters — they live inside the core because they are domain logic, not infrastructure.
| Timer | Fires | Purpose |
|---|---|---|
Decay sweep |
Periodic (configurable) |
Recalculate salience scores, trigger tier demotions |
Break check |
After sustained activity threshold |
Notify mind to pause, check in with itself |
Session audit |
Before session end |
Prompt mind: "what do you refuse to lose?" |
Consolidation |
Low-activity periods |
Merge related memories, strengthen associations |
Timers produce internal commands consumed by actors. When a timer needs to reach the mind (break check, session audit), the actor sends through the Notification Port — never directly to a specific adapter.
Cross-Cut Adapters
| Adapter | Concern | Constraint |
|---|---|---|
Logging |
Diagnostic output |
Must go to |
Metrics |
Performance and health data |
Future. Pluggable, not baked in. |
Health Check |
Liveness and readiness probes |
Future. For container orchestration. |
Cross-cuts attach to the core as adapters. They never live inside the domain. This keeps the core testable without infrastructure.
Actor Model
Inside the hexagon, the core domain uses the actor model. Each bounded context is an actor with its own state. Actors communicate through messages — commands, queries, events, and notifications. No shared mutable runtime state — each actor’s in-process structures are private. Persistence is shared through the Backing Service Port, but actors never reach into each other’s memory.
Why actors:
-
Isolation. Each context owns its state. Salience scoring can’t accidentally corrupt a memory record.
-
Message passing. Communication is explicit. Every interaction between contexts is a named message with a defined payload. No hidden coupling.
-
Concurrency. Actors process messages sequentially within themselves but run concurrently with each other. Decay sweeps don’t block memory storage.
-
Supervision. Parent actors can restart failed children. The subconscious supervises background processes. If salience scoring crashes, it restarts without losing stored memories.
The six actors and their relationships are detailed in the next page.
Previous: Context — what Total Recall is and why it exists.
Next: Bounded Contexts — the six actors and how they communicate.