Skip to content
← How we build

adr

ADR-006: Memory Strategy — Two Tiers, Fallback-Only, User-Scoped

Status

Accepted

Context

Phase 2 adds conversational memory: the ability for the system to recall earlier turns in a session and durable facts/preferences across sessions. Two questions had to be answered before this could be designed in detail: where memory sits in the request flow (which components can reach it), and how many tiers it has and what each tier persists. This ADR decides both, mirroring how ADR-007-rag-strategy.md decided RAG's shape before rag-design.md detailed its internals. The specific storage technology (SQLite via SQLAlchemy) is decided separately in ADR-010-rag-memory-tech-stack.md — this ADR is about strategy, not implementation.

Decision

Memory has two tiers, and both are reachable only through the Context Builder, on the same low-confidence/no-route branch RAG already occupies. Memory is never available to high-confidence Skills, and the Router never depends on it.

Router (scores the RAW query)
   │
   ├── HIGH CONFIDENCE ──────→ Skill (no Context Builder, no memory, no RAG)
   │
   └── LOW CONFIDENCE / NO ROUTE ──→ Context Builder (RAG + Memory) ──→ Fallback Agent Skill
Tier Scope Persistence Isolation key
Short-term Current session's conversation buffer (recent turns) In-process, TTL-bound — not written to any store session_id (AgentContext.session_id)
Long-term Durable facts/preferences that outlive a session SQLite, SQLAlchemy models (ADR-010) user_id (AgentContext.user_id, from Milestone 3 — Auth Foundation, see phases/phase2-implementation-plan.md)

RequestContext (domain-model-design.md §3) gains a memory_items: list[MemoryItem] field alongside the existing retrieved_context: list[RetrievedItem] (RAG) and conversation_history: list[ConversationTurn] (already typed, populated for the first time in Phase 2) — populated by the Context Builder the same way retrieval is, and empty on every high-confidence request.

Rationale: Why Fallback-Only, Not Always-On

The alternative — the Orchestrator attaching memory to every request, including high-confidence ones — was considered and rejected for Phase 2:

Consideration Always-on (rejected) Fallback-only, via Context Builder (adopted)
Invariant impact Requires amending project-context.md §2.2 (Context Builder is conditional-only) Zero invariant change — memory occupies the exact call site RAG already uses
Does it actually fix anything? No, not by itself — ordinary Skills (FinancialSkill, etc.) have a fixed, hand-written Tool-call sequence (skills-design.md §3a); seeing conversation_history doesn't make a Skill decide to fetch data for an entity mentioned only in that history. Only FallbackAgentSkill's internal LangChain agent can act on memory it's given. Routes exactly the queries that need memory-driven reasoning (referring expressions, missing entities) to the one Skill built to reason over it
Consistency with why a query was low-confidence in the first place N/A Low confidence already means "doesn't map cleanly to a deterministic route" — the same condition that makes memory-assisted resolution valuable, not incidental (identical reasoning to ADR-007)
Cost Memory lookups are cheap (TTL buffer read, indexed SQLite point query by user_id) — unlike RAG, cost isn't the limiting argument N/A — cost was not the deciding factor here, invariant stability and actual usefulness were

The residual gap this creates: a query with an explicit entity and an implicit reference to something else — e.g. "Compare AAPL's P/E to the one I asked about earlier" — clears the entity gate and routes high-confidence to FinancialSkill, which never sees memory. This is closed by a Router-side fix, not a memory-scope change: routing-design.md §5b (Phase 2) adds a reference/comparison-cue gate that demotes such queries to low_confidence=true even when an entity matched, so they still reach the Context Builder + FallbackAgentSkill. Memory's own scope stays fallback-only; the Router gets better at recognizing which queries actually need that branch. As a backstop for whatever the gate still misses, every Skill's system prompt (skills-design.md §2, Phase 2 addition) is instructed to explicitly flag — not silently drop — any part of the query it lacks data or context for, so a missed case degrades visibly instead of quietly.

Rationale: Why Two Tiers, Not One

A single persisted tier (everything in SQLite) would pay a durable-write cost for data that's only ever useful within one session (e.g., "the company I just asked about" two turns ago) and would need explicit expiry logic to avoid unbounded growth. A single in-process tier (nothing persisted) can't answer "what did I tell you about my portfolio last week," which is squarely what "long-term memory" is supposed to mean. Splitting matches the actual access pattern: short-term is read far more often and needs to be fast and disposable; long-term is read less often but must survive a restart.

Rationale: Why user_id, Not session_id, for Long-Term Scoping

Long-term memory is durable preference/fact storage that should follow a person across sessions and devices — a new session_id is minted per conversation (AgentContext.__init__), so scoping long-term storage to it would make memory effectively session-scoped, defeating the point of a "long-term" tier. This requires Phase 2 to introduce real user identity, which is why Milestone 3 (Auth Foundation — FastAPI Users, JWT, per security-design.md §4) is sequenced immediately before the Memory milestone in phases/phase2-implementation-plan.md, rather than deferred to a later phase. Short-term memory keeps session_id scoping — it has no reason to survive past the session it buffers.

Alternatives Considered

A. Always-on memory (Orchestrator-level, every branch) — rejected; see rationale table above.

B. Single persisted tier only — rejected; see "Why Two Tiers" above.

C. session_id-scoped long-term memory, no auth in Phase 2 — considered and rejected during this session's planning; without real user identity, "long-term" memory can't outlive the session it was created in, which contradicts what the tier is for. Rejecting this option is what pulled Auth Foundation into Phase 2 rather than deferring it.

Consequences

Positive:

  • No invariant changes; memory and RAG share one seam, one mental model, one set of anti-patterns (context-builder-design.md §8).
  • Long-term memory is genuinely durable and personal from the moment it exists, not a session-scoped placeholder that needs a later migration once real accounts arrive.

Negative / accepted tradeoffs:

  • Phase 2 grows by one milestone (Auth Foundation) beyond the original ~4-milestone estimate, specifically to make user_id scoping meaningful now rather than later.
  • High-confidence Skills still don't get memory automatically; the Router reference-gate + Skill backstop-prompt combination is a deliberate mitigation, not a guarantee of catching every phrasing.

Revisit Trigger

If observability data shows the Router's reference/comparison-cue gate (routing-design.md §5b) is missing a specific, recurring phrasing pattern, extend the gate's cue list first. If a high-confidence Skill is later found to have a genuine, recurring need for memory (not just an edge case), revisit via the same small/local retrofit path already documented for RAG (architecture-overview.md §8, "What if a high-confidence Skill someday needs retrieved context?") — not by making the Context Builder unconditional.

Source: docs/adr/ADR-006-memory-strategy.md

Follow the work.

Occasional updates on SignalFoundry, MarketCompass, and what we are building at CompassFoundry Labs.

No spam. Unsubscribe anytime.