Agentic Memory in TruvaG3: A Team With a Shared Notebook

A guided tour of TruvaG3's four-tier shared memory system — the academic research it came from, the five pipeline hooks that wire it into the orchestrator, how a fleet of agents stops repeating each other's work, and how a single chat agent finally remembers what a user said last week.

Neelabh Tripathi · May 27, 2026 · Updated August 10, 2026

Contents

The amnesia problem

In TruvaG3, every ProcessRequest call starts with a blank slate. The orchestrator plans against the registry, executes the DAG, synthesizes a response, and forgets everything. The next call — even one made one second later, by the same agent, against the same entity — begins again from zero.

That is fine for a single tool. It is a problem the moment you have two agents that can act on the same entity. Without shared memory, agents fail in three concrete ways. Duplicated work — two agents independently investigate the same root cause, burning LLM tokens and tool invocations on the same five-step plan. Missing context — Agent B makes a decision without knowing Agent A's recent action on the same entity, leading to conflicting remediations or contradictory customer messages. No institutional learning — the same class of incident gets resolved fifty times, and the fifty-first still runs the full investigation, because the system has accumulated nothing.

The picture below is what this looks like in a typical TruvaG3 deployment. An event-driven agent reacts to an OOMKilled alert and creates a JIRA ticket. Five minutes later, a chat agent receives a related question about the same pod. Without shared memory, the second agent has no idea the first one ran.

Without memory With memory event-driven-agent Pod X is OOMKilled · alert received 5-step investigation · creates JIRA DEVOPS-42 5 min later — no record — no signal — no knowledge devops-chat-agent "Why is pod X slow?" Investigates from scratch — no idea A ran OUTCOME • DUPLICATE 5-step investigation • DUPLICATE JIRA ticket created • DUPLICATE Slack notification • Conflicting remediations · tokens wasted event-driven-agent Pod X OOMKilled · creates DEVOPS-42 Writes AgentEvent → episodic memory writes shared memory events · coordination · knowledge reads devops-chat-agent "Why is pod X slow?" Reads memory: A restarted pod X · DEVOPS-42 OUTCOME • 2-step plan: check status, add comment • No duplicate ticket • Consistent action across agents • Fewer tokens spent · faster turnaround
Figure 1. The amnesia problem and the shared-notebook fix. Two agents, same entity, two sequential requests. On the left, each agent investigates independently — duplicated tools, duplicated tickets, duplicated notifications. On the right, the first agent's actions are written into shared memory; the second agent reads them before planning and produces a different, smaller plan. The shared memory does not change what an agent can do — it changes what the agent knows when it starts.

Everything in the rest of this article is an answer to the same question asked at different timescales: how does Agent B see what Agent A just did? Five seconds ago. Five minutes ago. Five weeks ago. And how does that visibility survive a backend restart, scale to thousands of events, and not block the request path when it fails?

Where the design came from

A survey, not a vector DB bolt-on

The temptation when adding "memory" to an agent framework is to attach a vector database and call it done. Every paper that took that route — store everything, retrieve by similarity — found the same failure mode: as memory grows, signal-to-noise collapses and decision quality degrades. TruvaG3's memory system was designed against a literature survey first, then implemented. The relevant lineage:

SourceWhat it gave us
Stanford Generative Agents (2023)
Park et al., arXiv 2304.03442
The three-tier model — memory stream, reflection, planning — and the now-standard recency · importance · relevance retrieval triad. Their hard finding: without a periodic reflection pass, agents became incoherent within a few hundred turns.
MemGPT (2023)
Packer et al., arXiv 2310.08560
The LLM as its own memory manager — paging information in and out of the context window via explicit function calls. TruvaG3's enrichment hook is the "page in" mechanism.
A-MEM (2025)
Xu et al., arXiv 2502.12110
Agentic memory — the store actively organizes, connects, and evolves its contents instead of passively accumulating them. Source of the ADD / UPDATE / CONTRADICT / DUPLICATE reconciliation pattern.
DAMCS (2025)
arXiv 2502.05453
The cognitive-science three-tier model — sensory (raw tool output), episodic (events), procedural (learned how-to). Maps cleanly onto TruvaG3's ExecutionStore → AgentEvent → KnowledgeFragment progression.
G-Memory (2025)
arXiv 2506.07398
Provenance — every fragment tracks who created it, what evidence it was based on, and the derivation chain. Critical for enterprise audit trails. Lives in KnowledgeFragment.SourceEvents (which events produced the fragment) and AgentEvent.ParentEvent (which request derived from which).
Multi-Agent Memory from CompArch (2026)
arXiv 2603.10062
The L1/L2/RAM/Disk analogy and the consistency-model benchmark. Strong consistency cost 3-5× latency; eventual consistency caused decision conflicts in ~12% of scenarios. Causal consistency — what Redis Streams give you for free — was the sweet spot.
AgentRM (2026)
arXiv 2603.13110
The number that motivated the entire compaction layer: with no memory management, effective context degrades to noise within ~50 turns. With simple compression, agents retain ~37% of entity information across sessions.

Three findings that shaped everything

Three results recurred across the literature, and shaped the design directly.

Raw event logging is necessary but insufficient. Every "store everything" approach degraded under load; a reflection or distillation layer is required. Retrieval must be multi-signal. Pure embedding similarity misses temporally relevant information, and the recency-importance-relevance triad has held up across every follow-on paper. Consistency model matters. Stale reads cause measurable decision quality loss, but strong consistency is too expensive — causal consistency is the right floor.

Four interfaces, swappable backends

The memory system decomposes into four independent interfaces defined in core — each solves a distinct problem, each can be implemented and scaled separately, each degrades gracefully when missing. The interfaces are the framework's contract; the backends that implement them are the framework's defaults — opinionated but replaceable. A separate fifth interface, user memory, handles per-user private facts and is covered in §7.

Memory typeInterface (in core)What it storesDefault backend (in memory)
Episodic EpisodicMemory Structured events: "Agent X did Y to entity Z at time T with outcome W" StreamEpisodicMemory · Redis Streams (or in-memory for tests)
Coordinator InvestigationCoordinator Who currently holds the claim on which entity (with TTL) AtomicLockCoordinator · Redis SETNX
Knowledge SharedKnowledge Embedded knowledge fragments — learned patterns, resolution strategies VectorSharedKnowledge · Qdrant (vector DB, gRPC)
Reflector MemoryReflector Higher-level synthesized insights from many episodic events LLMMemoryReflector · uses the configured AIClient + episodic backend

Each interface lives in the core kernel; concrete backends live in the separate memory module, which absorbs the heavy client dependencies (Redis client, Qdrant gRPC + protobuf) so core stays lightweight. A Phase-1 deployment that needs only episodic memory and coordination compiles with none of the vector-DB dependencies. A Phase-2 deployment that wants knowledge search opts in by passing an embedding client.

FOUR INTERFACES (core) · DEFAULT BACKENDS (memory module, swappable) core.EpisodicMemory interface default: StreamEpisodicMemory backend: Redis Streams alt: pgvector · NATS · in-mem core.Investigation Coordinator interface default: AtomicLockCoordinator backend: Redis SETNX core.SharedKnowledge interface default: VectorSharedKnowledge backend: Qdrant (vector DB) alt: pgvector · Weaviate · Milvus core.MemoryReflector interface default: LLMMemoryReflector uses configured AIClient distills events → fragments core (micro-kernel) interfaces · types · NoOp defaults deps: go-redis · uuid · testify only application · main.go memory.NewSharedBackends(...) · orchestration.BuildMemoryHooks(...) · framework.RegisterRunnable(...) Phase 1 (episodic + coordinator) always created. Phase 2 (knowledge + reflector) opt-in via TRUVAG3_VECTOR_DB_URL + embedding client.
Figure 2. Four core interfaces, swappable backends. Every tier lives behind a Go interface defined in core. The boxes name the interface first and the default backend second — Redis Streams and Qdrant ship as the production defaults because most platforms already operate Redis and Qdrant is purpose-built for vector search, but they are replaceable. Any backend that satisfies the same core interface is a drop-in alternative; the rest of the framework never knows the difference. The application's main.go is the only place that wires backends to hooks; the orchestrator itself never imports memory, only the core interfaces.

One detail in Figure 2 deserves its own paragraph, because it is the design choice that makes the rest of the system safe to deploy. Every backend is fail-open, regardless of which implementation is wired in. Runtime errors return (nil, nil), not (nil, error). If the knowledge backend is unreachable, search returns empty; the LLM plans without enrichment. If the episodic backend is unreachable, the activity coordinator returns empty signals; the planner proceeds without coordination. If the LLM-driven compaction times out, the cached digest is used; if there is no cached digest, the raw last-N events are sent through. Memory is an enhancement, never a prerequisite — the orchestration pipeline cannot fail because of a memory backend, whichever one you have plugged in.

How memory plugs into the pipeline

Five hooks, three stages

Memory is not invoked imperatively — agent code does not call memory.Recall() or memory.Record(). Everything happens through TruvaG3's existing pipeline-hook mechanism: middleware that runs at well-defined stages of ProcessRequest, registered once at startup. There are five hooks; they run in a fixed order on every request; they are fail-open individually so a failure in one does not break the others.

User request STAGE 1 · BEFORE PLANNING 1 · ActivityAnnouncementHook writes "I'm working on entity X" signal reads other agents' live signals → injects <agent_coordination> 2 · MemoryEnrichmentHook queries episodic + compacts digest searches shared knowledge (vector) → injects <agent_memory> Plan → Execute → Synthesize LLM sees <agent_coordination> and <agent_memory> in the planning prompt DAG executes with parallelism · synthesis composes the response STAGE 2 · AFTER EXECUTION 3 · MemoryRecordHook records AgentEvent per step (LLM summarizes) 5 · ActivityCleanupHook removes "working on X" signal · async STAGE 3 · AFTER SYNTHESIS 4 · KnowledgeExtractionHook LLM extracts reusable knowledge fragments embeds → stores via core.SharedKnowledge async · fail-open · doesn't block response
Figure 3. Five memory hooks across three pipeline stages. Two hooks fire before planning — one announces and reads activity signals, one queries episodic and knowledge and writes the digest into the prompt. After the DAG finishes, the record hook writes events for every step and the cleanup hook removes the "working on this" signal. After synthesis, the knowledge-extraction hook distills the response into a reusable fragment, asynchronously. Every hook is fail-open; a failure logs a warning and the pipeline continues without that hook's contribution.

What the LLM actually sees

The hooks contribute two new XML sections to the planning prompt. The first is Coordination — real-time signals from agents currently active in the same domain:

<agent_coordination>
Other agents currently active in domain 'infrastructure':
- event-driven-agent: investigating "TruvaG3HighLatency on product-catalog-api"
  (status: executing, started 3s ago)
</agent_coordination>

The second is Episodic + Knowledge — a compacted digest of recent domain activity plus the most recent events at full detail, plus any vector-retrieved knowledge fragments:

<agent_memory>
Domain activity summary (last 2 hours, 106 events):
- Deployment restart: agent-with-human-approval restarted via rollout (200ms)
- JIRA: ticket DEVOPS-49 created for the restart, comment added with details
- Slack: notification sent to #notifications with JIRA reference
- Monitoring: 4 Prometheus queries run by event-driven-agent

Most recent events (detail):
- [2026-05-26 23:31] devops-chat-agent: Queried logs for orch-1774... (success)
- [2026-05-26 23:30] devops-chat-agent: Created JIRA ticket DEVOPS-52 (success)

Prior knowledge relevant to this request:
- OOMKilled on pods with <256Mi memory is typically resolved by doubling
  the memory limit. Check resource quotas first. (importance: 0.82)
</agent_memory>

This is what the LLM sees when it generates its plan. It is not asked to "consider memory" — the memory is already in the prompt, and the LLM treats it like any other context. The behaviour change is emergent: the planner naturally writes shorter plans when the digest tells it the work was already done, and it naturally references existing tickets when the digest names them.

Activity compaction: surviving thousands of events

The naive version of the enrichment hook sends every recent event through to the LLM on every request. At 200 events per planning prompt, that is 5,000-10,000 tokens of context overhead — every request, every agent. It will work, and it will be expensive.

Episodic compaction solves this with an LLM-summarized digest that lives in the digest cache (core.DigestCache — default backend is Redis, in-memory ships for tests, anything else can be plugged in) and is shared across every agent in the domain. The hook's behaviour depends on what is in the cache and how many new events have arrived since the last compaction:

Request arrives BeforePlanning cache state? how many new? A · Cache miss (cold start) 200 events → full LLM compaction (5-10s) → cache B · Cache hit · 0 new events cached digest reused · 0 ms · no LLM call C · Cache hit · few new events (< 20) incremental update · prev digest + delta (0.5-1s) D · Burst · 20+ new events full recompaction · LLM (5-10s) · new cache Typical steady state · 80% cached (0ms) + 15% incremental (0.5s) + 5% full (5s) = ~0.3s average cache is shared across every agent in the domain — first agent pays the cost, all peers read it back
Figure 4. Activity compaction is a four-state cache. Cold start is rare. Most requests hit a warm cache that is either reused as-is or incrementally updated with a handful of new events. Burst recompaction kicks in when more than TRUVAG3_SHARED_MEMORY_DIGEST_INCREMENTAL_THRESHOLD events have arrived (default 20). The digest cache is shared across every agent in the same domain — when one agent pays the cost of recompaction, all peers benefit on their next request.

The cache key is the domain, not the agent. When event-driven-agent compacts the digest, devops-chat-agent reads it back as a cache hit on the next request. This is what makes the compaction pattern affordable in a fleet — the LLM cost is amortized across every agent in the domain, not paid per agent.

The span event activity.compaction.cache_decision labels the path taken on each request — cached, incremental, full, or full_recompact. If a production deployment shows path=full on every request, the cache is broken — the configured digest-cache backend is unreachable, the TTL is too short, or the digest cache wasn't wired into the enrichment hook. The condition is visible without reading a single application log.

Real-time coordination signals

Episodic memory has a built-in latency: events are visible after execution completes, which is typically 2-4 seconds. For most workloads that is fine. For concurrent requests — an alert that fires while a chat session is already mid-investigation against the same pod — 2-4 seconds is too slow. The two agents can both start a five-step plan before either of them sees the other's event.

This is what the Coordinator solves. It is a separate, lightweight store implementing core.ActivityCoordinator: transient keys with TTL, no JSON payloads, no LLM calls. The default backend writes through Redis (with an in-memory fallback for tests); any KV store with atomic SETNX + TTL semantics can be substituted. An agent writes a signal the moment it starts processing a request and removes it the moment the response is synthesized:

Two agents, one pod, no duplicated work event-driven-agent signal: "investigating Pod X" (TTL 5min) KV with TTL — no JSON payload, no LLM call alert fires signal removed devops-chat-agent (second request, same pod) 1s later — same domain · signal visible reads signal sees E-D-A is on it signal in prompt planner can defer or adapt responds to user "investigation in flight, ETA 30s" same alert arrives T = 0s T = 1s T = 3s T = 28s T = 30s
Figure 5. Real-time signals close the 2-4 second gap. The event-driven agent writes a signal to the activity coordinator's KV store (default backend: Redis) the moment it starts processing the alert (T = 0). One second later, the chat agent reads the same domain's signals and the planner sees E-D-A's activity in its <agent_coordination> prompt block — giving it the option to defer, coordinate, or adapt rather than blindly starting a parallel investigation. The signal is removed when the originating agent finishes; if it crashes, the TTL expires automatically. No cleanup process, no leaked locks.

Two design choices keep this affordable. The signal is a KV entry with a TTL — on the default Redis backend that is one SET … EX 300, but the interface only assumes "put, get, expire," so the same hook works against any KV-with-TTL backend. No JSON, no large payloads, no LLM call to write or read. The TTL is longer than the longest expected request (default 5 minutes), so a crash never leaves a permanent claim. The signal is plain text written in natural language — "investigating TruvaG3HighLatency on product-catalog-api" — and the LLM reads it the same way it reads the rest of the prompt. The decision of whether to defer is made by the planner, not by a hardcoded rule, so agents can coordinate even when their work overlaps in ways the framework cannot anticipate.

Knowledge extraction and the reflection job

Episodic events answer "what did Agent X just do?" Knowledge fragments answer the harder question: "what have we, collectively, learned about this class of problem?" These two questions need different storage. Episodic events expire after 60 days (the default EpisodicMemory backend honours TRUVAG3_SHARED_MEMORY_STREAM_MAXLEN and TTL); knowledge fragments live in the SharedKnowledge backend with no TTL, surfaced through semantic search. The default SharedKnowledge implementation is vector-DB-backed (Qdrant out of the box), but any backend that satisfies the interface works — pgvector for teams already running Postgres, valkey-search once it matures, or a custom implementation for an internal system.

TruvaG3 produces knowledge fragments through two complementary paths, running on different timescales. The first is the KnowledgeExtractionHook — it runs after every synthesis, distills the current request's findings into a 1-2 sentence fragment, embeds it, and stores it. Fast, immediate, high-volume. The second is the Reflector — a background job that runs every 24 hours, scans episodic events older than seven days, and asks the LLM to identify patterns across many past events for the same entity. Slower, distilled, pattern-aware.

The life of one event, from recording to durable knowledge T = 0 ≤ 24h 7 days 60 days 6 months recorded tool runs MemoryRecordHook writes event EpisodicMemory · 60d in digest compactor sees event in 24h window surfaced to LLM via <agent_memory> reflection-eligible ≥ AGE_THRESHOLD old paired with peer events for the same entity background job picks up distilled to fragment reflector LLM extracts 1-3 fragments embed → SharedKnowledge no TTL — permanent event expires EpisodicMemory · 60d original gone fragments survive in SharedKnowledge T = 6 months later · a new but related incident arrives → Semantic search retrieves the reflected fragment from SharedKnowledge → Planner sees "Prior knowledge: OOMKilled on <256Mi pods is resolved by doubling memory" → 2-step plan applied directly — even though the original event has aged out of the episodic store Knowledge outlives the events it was distilled from.
Figure 6. Knowledge outlives the events it was distilled from. An event is visible to the compactor for the first 24 hours, eligible for reflection once it crosses the age threshold (default 7 days), and expires from the episodic store at 60 days. By that point, the reflector has already extracted any durable patterns into SharedKnowledge fragments. A new incident months later, semantically related to those patterns, gets a 2-step plan instead of a 5-step investigation — using knowledge derived from events that no longer exist in the episodic store.

The reflection job is the part of the system that most directly maps onto the Stanford Generative Agents finding — agents without a reflection layer became incoherent within a few hundred turns. TruvaG3's version (memory/reflection_job.go) runs as a framework Runnable with one line of wiring:

// Reflection job: long-term knowledge retention via background distillation.
// Returns nil silently if Phase 2 backends aren't available.
if reflectionJob, _ := memory.BuildReflectionJob(memBackends.ToDeps(), agent.AI, agent.Logger); reflectionJob != nil {
    framework.RegisterRunnable(reflectionJob)
}

Two operational properties matter. It is multi-replica safe. The job acquires a distributed lock keyed by domain before each pass (default backend: Redis; the lock is a core.DistributedLock abstraction so any KV with atomic check-and-set will do). Other replicas log "lock held by another replica, skipping" and return. Model choice matters more than for any other LLM call in the system. Reflection writes durable knowledge that influences every future request — a bad fragment lingers in the knowledge store and surfaces as confident "prior knowledge" for as long as it remains. The default model is the agent's main AI client; TRUVAG3_REFLECTION_MODEL=fast trades fragment quality for cost, which is acceptable for high-volume cost-sensitive deployments but not the right default.

User memory: per-user personalization

Everything covered so far is shared agent memory — cross-agent visibility within a domain. TruvaG3 also ships a separate, complementary system for per-user private facts: user memory. Different store, different hooks, different privacy boundary, different lifecycle. The mental model is a personal assistant's notebook versus a team's shared notebook.

Shared memoryUser memory
ScopeAll agents in a domainOne user only
Content"Agent X restarted pod Y""User prefers DFW airport"
VisibilityDomain-widePrivate to user
Lifetime24h lookback + knowledge persistsMonths / years; evolves with user
GDPR deleteNot per-userForget("user-42") → one call
Enrichment tag<agent_memory><user_profile>
Interfacecore.EpisodicMemory + core.SharedKnowledgecore.UserMemory
Default backendRedis Streams + Qdrant (swappable)VectorUserMemory (Qdrant) or InMemoryUserMemory (swappable)

Fact reconciliation in four operations

The interesting part of user memory is not the storage — it is what happens when a new fact contradicts an existing one. A user who says "I am vegetarian" on Monday and "I'm not vegetarian anymore" on Friday should not have both facts in their profile. The system needs to recognize the relationship and update accordingly.

Fact reconciliation: classifying a new candidate against what's known Candidate fact extracted from latest conversation Vector similarity cosine against existing facts (vector search) below 0.75 → ADD directly (no LLM) above 0.75 → LLM reconciliation LLM CLASSIFIES THE RELATIONSHIP INTO ONE OF FOUR OPERATIONS ADD no similar fact exists "User likes hiking" → stored as-is most common path for new users UPDATE refines existing fact "Likes cricket" → "...with friends on weekends" CONTRADICT new supersedes old "Vegetarian" → "No longer vegetarian" → old deleted, → new stored DUPLICATE same info exists → skip storage → update last_used → timestamp Cost optimization: when no similar fact exists (cosine < 0.75), reconciliation skips the LLM call entirely. For new users, most facts ADD directly with zero reconciliation cost.
Figure 7. Fact reconciliation — four operations, one LLM call. Embedding similarity finds candidate matches; the LLM classifies the actual semantic relationship. Pure vector similarity cannot distinguish "prefers window seats" from "prefers aisle seats" — both are high-similarity, both are about seating, and the meanings are opposite. The LLM call is the part that handles the contradiction correctly.

Five parallel recall paths

Recall is structured around five parallel paths, each contributing different facts to the planning prompt: identity (who the user is — always recalled), stable_namespace (facts tagged with stable identifiers), query (semantic match against the current request), summary (rolling session summaries from previous conversations), and universal (facts that cross every agent — name, language, timezone). Each path emits its own span in Jaeger, so a developer can see which paths contributed to a given response.

The result, in the planning prompt, is a <user_profile> tag with confidence-banded facts:

<user_profile>
Identity:
- User lives in Coppell, TX (explicit, high confidence)

Constraint:
- User and family are vegetarian, no eggs or gelatin (explicit, high confidence)

Preference:
- User prefers DFW airport for flights (explicit, high confidence)

Relationship:
- Family of three: user, wife, and 12-year-old son (explicit, high confidence)

Summary:
- Planned a week-long family itinerary in Zurich with hotel picks and vegetarian restaurant recommendations near each one (derived, 3 days ago)
- Asked about Switzerland weather, currency, and travel tips (derived, 3 days ago)
</user_profile>

The travel agent now plans around the user's dietary constraints, family size, and airport preference — without the user repeating any of it. Extraction runs after the response is streamed, so the user sees the answer immediately and the new facts are stored asynchronously. GDPR erasure is a single API call: userMemory.Forget(ctx, userID) deletes every vector and payload for that user. The interface is the boundary; swapping Qdrant for PostgreSQL with pgvector is a backend change, not an application change.

Domain scoping and physical isolation

An enterprise deployment has multiple agent types, each with multiple replicas, serving different business domains. A platform team needs to know exactly what is shared, what is isolated, and how compliance boundaries are enforced. TruvaG3 operates at two levels: logical isolation by default, physical isolation when regulated.

Two scopes: shared backends, regulated backends STANDARD DOMAINS · LOGICAL ISOLATION domain: infrastructure devops-chat-agent event-driven-agent agent-with-human-approval share events + knowledge domain: commerce order-exception-agent refund-agent customer-support-agent share events + knowledge ScopeGlobal events — visible across both domains "service-X is down" · entity state changes · cross-domain correlations EpisodicMemory backend + SharedKnowledge backend scope filter on every query — runs inside the backend's index traversal, not as a post-filter REGULATED · PHYSICAL ISOLATION domain: healthcare clinical-agent (HIPAA) K8s NetworkPolicy denies cross-namespace dedicated episodic backend dedicated knowledge backend no traffic crosses
Figure 8. Two isolation levels, one piece of application code. Standard domains share centralized backend instances with logical isolation enforced at query time — every SearchKnowledge call includes a callerDomain filter that runs inside the backend's index traversal, not as a post-filter. Regulated domains (HIPAA, PCI-DSS) get separate backend instances in a different K8s namespace with a NetworkPolicy that denies all ingress. Same code, same interfaces; only the connection address (and, optionally, the backend implementation itself) differs. The framework module does not enforce compliance — infrastructure does, and that boundary is the right place for it.

Every AgentEvent and KnowledgeFragment carries three fields that determine visibility: AgentDomain, AgentName, and Scope. Scope is one of three values:

ScopeVisible toExample
ScopePrivateOwning agent onlyInternal reasoning trace, intermediate failure state. Never stored in shared knowledge.
ScopeSharedDomainAgents in the same domainDevOps incident details. Visible across the infrastructure fleet, invisible to commerce agents.
ScopeGlobalAll agents, any domain"service-X is down" — entity state changes that cross domain boundaries.

The investigation-coordinator claim has one known limitation: the lock key is the entity ID only, not type:id. Two entities of different types that happen to share an ID — pod/foo and service/foo — will collide on the lock. The framework documents this honestly because it has low practical impact in most domains and a fix would require breaking the lock-key format. It is the kind of trade-off worth surfacing rather than hiding.

What the wiring looks like

The wiring is shorter than the architecture might suggest. Two function calls — one in the memory module to create backends, one in the orchestration module to create hooks from those backends — and the orchestrator gets memory on every request. The example below shows the default wiring (Redis + Qdrant) from the bundled devops-chat-agent; replacing either backend is covered after the snippets.

// 1. Memory module creates all backends. The default factory takes a Redis
//    client because Redis is the default backend; to swap, skip this factory
//    and construct your own backends against core.EpisodicMemory etc. directly.
//    Phase 1 (episodic + coordination + activity + digest cache) always created.
//    Phase 2 (knowledge store) auto-detected from TRUVAG3_VECTOR_DB_URL.
backends, _ := memory.NewSharedBackends(redisClient, agent.Logger,
    memory.WithAgentName("my-agent"),
    memory.WithDomain("infrastructure"),
    memory.WithEmbeddingClient(embedder),   // enables Phase 2
)
defer backends.Close()

// 2. Orchestration module creates the 5 hooks from the backends.
hooks, activityCoord := orchestration.BuildMemoryHooks(
    backends.ToDeps(), agent.AI, agent.Logger,
)

// 3. Pass into the orchestrator.
deps := orchestration.OrchestratorDependencies{
    Discovery:           discovery,
    AIClient:            agent.AI,
    PipelineHooks:       hooks,
    ActivityCoordinator: activityCoord,
}

// 4. Long-term retention: reflection job as a framework Runnable.
if reflectionJob, _ := memory.BuildReflectionJob(
    backends.ToDeps(), agent.AI, agent.Logger,
); reflectionJob != nil {
    framework.RegisterRunnable(reflectionJob)
}

For a user-memory-enabled chat agent (the travel-chat-agent pattern), one more block:

// 5. User memory: separate backend, separate hooks.
userMemBackend, _ := memory.NewUserMemoryBackend(agent.Logger,
    memory.WithUserMemoryNamespace("travel"),
    memory.WithUserMemoryEmbeddingClient(embedder),
)
defer userMemBackend.Close()

userHooks, userHooksCloser := orchestration.BuildUserMemoryHooks(
    userMemBackend.ToDeps(), agent.AI, agent.Logger,
)
defer userHooksCloser.Close()

// Shared + user hooks compose into a single pipeline.
deps.PipelineHooks = append(hooks, userHooks...)

Both wirings ship as working production code. Behavioural customization — domain-specific entity extraction, importance scoring, custom retrieval weights, longer lookback windows — is opt-in via additional options to BuildMemoryHooks. Numeric tuning (token budgets, TTLs, summarizer model alias) goes through TRUVAG3_* environment variables.

Swapping a backend

Replacing the default Redis or Qdrant backend is a constructor-line change, not a framework change. The application skips NewSharedBackends (the convenience factory that assumes Redis), constructs whichever backend it wants, and assembles its own core.SharedMemoryDeps:

// Example: a deployment that prefers pgvector for SharedKnowledge
// (Postgres is already on the platform's bill of materials).
import (
    "github.com/truvaagents/truva-g3/core"
    "github.com/truvaagents/truva-g3/memory"
    "acme/pgvectormem"      // user-owned package implementing core.SharedKnowledge
)

episodic, _ := memory.NewStreamEpisodicMemory(                  // keep Redis here
    memory.WithEpisodicRedisClient(redisClient),
    memory.WithEpisodicDomain("infrastructure"),
)
coordinator, _ := memory.NewAtomicLockCoordinator(
    memory.WithCoordinatorRedisClient(redisClient),
    memory.WithCoordinatorDomain("infrastructure"),
)
knowledge := pgvectormem.New(pgPool, ...)                       // ← custom backend

deps := &core.SharedMemoryDeps{
    AgentName:   "my-agent",
    AgentDomain: "infrastructure",
    Episodic:    episodic,
    Coordinator: coordinator,
    Knowledge:   knowledge,                                     // any core.SharedKnowledge
    Embedder:    embedder,
    // ActivityCoordinator and DigestCache are optional; leave nil
    // to disable signals/cache, or wire memory.NewRedisActivityCoordinator
    // and memory.NewRedisDigestCache against the same client.
}
hooks, activityCoord := orchestration.BuildMemoryHooks(deps, agent.AI, agent.Logger)

The rest of the framework — the five hooks, the activity compactor, the reflection job, the LLM prompt builders — never knows the backend changed. The same applies to every core memory interface: EpisodicMemory can be swapped for a NATS JetStream implementation, DigestCache for any KV cache, UserMemory for a Postgres-backed implementation. What does not ship yet is one shared conformance suite for memory backends. A custom implementation should carry its own contract tests for the guarantees its interface needs — round trips and retention, cross-instance visibility, atomic ownership where coordination is involved, and the documented failure behavior. The framework's existing reusable suites cover task delivery and orchestration state, not these memory interfaces.

One subtle wiring property is worth pulling out. The orchestration module does not import the memory module. The dependency graph is orchestration → core (interfaces only), memory → core + telemetry (default implementations), and main.go → orchestration + memory + ai (wiring). This means a Phase-1 deployment that does not import the memory module's vector backend never pulls in gRPC or protobuf, and a deployment that wires only custom backends does not need to import the bundled memory module at all.

Architect's cheat sheet

Six things to remember when you walk into a design review with this system on the whiteboard.

CONTRACT

1 · The interfaces are the framework; the backends are defaults

Four core interfaces — EpisodicMemory, InvestigationCoordinator, SharedKnowledge, MemoryReflector — are the framework's contract. Redis and Qdrant are the production defaults; pgvector, NATS, Weaviate, or a custom backend each work by satisfying the same interface.

DESIGN

2 · Memory is an enhancement, not a prerequisite

Every backend is fail-open, whichever one is plugged in. If the episodic backend is unreachable, the planner runs without enrichment. If the knowledge backend is unreachable, search returns empty. The pipeline cannot fail because of a memory backend.

RESEARCH

3 · Recency · Importance · Relevance — all three

Pure vector similarity misses temporally relevant information. The Stanford triad held up across every follow-on paper. Weights are tunable per agent type; defaults are balanced.

SCALE

4 · The digest cache is what makes this affordable

The naive version sends 200 events through the LLM per request. The cached digest is shared across every agent in the domain — first agent pays the cost, all peers read the cache. Watch activity.compaction.cache_decision.

RETENTION

5 · Reflection is what makes memory durable

Episodic events age out on the configured TTL; the activity compactor only looks back 24 hours. Without reflection, every event older than that window is invisible and eventually gone. The background job converts ephemeral activity into permanent SharedKnowledge fragments.

PRIVACY

6 · Shared and user memory are separate concerns

Different interfaces, different hooks, different lifecycles. Shared memory is domain-wide; user memory is private and per-user. Forget(userID) is a single call that deletes everything for one user — GDPR-shaped from day one.

Where to go next

Recommended reading order, by goal: