Skip to content

feat: add persistent context memory store with write-time dedup#37

Merged
Siddhant-K-code merged 6 commits intomainfrom
feat/context-memory-store
Feb 23, 2026
Merged

feat: add persistent context memory store with write-time dedup#37
Siddhant-K-code merged 6 commits intomainfrom
feat/context-memory-store

Conversation

@Siddhant-K-code
Copy link
Owner

Adds a persistent context memory store that accumulates knowledge across agent sessions. Memories are deduplicated on write, ranked by relevance + recency on recall, and compressed over time through hierarchical decay.

What it does

Store - Write context with automatic dedup. If a semantically similar memory already exists (cosine distance < threshold), the existing one is updated instead of creating a duplicate.

Recall - Retrieve memories ranked by (1-w)*similarity + w*recency. Supports tag filtering and token budgets.

Forget - Remove memories by ID, tag, or age.

Decay - Background worker compresses aging memories:

  • Full text -> summary (extractive, ~20% of original) after 24h
  • Summary -> keywords after 7 days
  • Keywords -> evicted after 30 days
  • Accessing a memory resets its decay clock

New files

File Purpose
pkg/memory/store.go Interface, types, config
pkg/memory/sqlite.go SQLite backend with write-time dedup
pkg/memory/decay.go Hierarchical decay worker
pkg/memory/helpers.go ID generation, embedding encode/decode, token estimation
pkg/memory/memory_test.go 11 tests covering store, recall, dedup, forget, decay, tags, token budget
cmd/memory.go CLI: distill memory store/recall/forget/stats
cmd/api_memory.go HTTP: POST /v1/memory/store, /recall, /forget, GET /stats
cmd/mcp_memory.go MCP tools: store_memory, recall_memory, forget_memory, memory_stats

Dependencies

  • modernc.org/sqlite - Pure Go SQLite (no CGO required)

Usage

# CLI
distill memory store --text "Auth uses JWT with RS256" --tags auth --source docs
distill memory recall --query "How does auth work?" --max-results 5
distill memory stats

# API
curl -X POST localhost:8080/v1/memory/store -d '{
  "session_id": "session-1",
  "entries": [{"text": "Auth uses JWT", "tags": ["auth"]}]
}'

# MCP (available as tools in Claude Desktop, etc.)
# store_memory, recall_memory, forget_memory, memory_stats

Closes #29

Siddhant-K-code and others added 3 commits February 22, 2026 18:30
Adds pkg/memory with SQLite-backed persistent storage for context
that accumulates across agent sessions.

Core features:
- Write-time dedup via cosine distance on embeddings
- Tag-based recall with relevance + recency ranking
- Token budget support for recall
- Hierarchical decay: full text -> summary -> keywords -> evicted
- Background decay worker with configurable age thresholds

Integration:
- CLI: distill memory store/recall/forget/stats
- API: POST /v1/memory/store, /recall, /forget, GET /stats
- MCP: store_memory, recall_memory, forget_memory, memory_stats tools

Uses modernc.org/sqlite (pure Go, no CGO) for zero-dependency
local storage.

Closes #29

Co-authored-by: Ona <no-reply@ona.com>
Co-authored-by: Ona <no-reply@ona.com>
Co-authored-by: Ona <no-reply@ona.com>
@Siddhant-K-code
Copy link
Owner Author

Code Review

CI is green (build + lint). 11 tests pass. Here's a deep review.

What's good

  • Clean interface design - Store interface in store.go is minimal and backend-agnostic. Adding Redis or Postgres backends later is straightforward.
  • Write-time dedup - Deduplicating on store (not recall) is the right call. It keeps the DB small and makes recall fast.
  • Decay model - The three-level decay (full -> summary -> keywords -> evict) with access-count-based clock reset is well thought out.
  • Pure Go SQLite - modernc.org/sqlite avoids CGO, making cross-compilation and Docker builds simpler.
  • Consistent patterns - Follows the existing codebase conventions (error types, config structs, CLI flag patterns).
  • Test coverage - 11 tests covering the main paths: store, dedup, recall, tags, token budget, forget, decay, edge cases.

Issues to address (in follow-ups)

P1 - Bug risk:

  • touchMemories goroutine uses the request context which may be cancelled after response. Should use context.Background().

P2 - Correctness:

  • MaxMemories config field is defined but never enforced in Store(). Either implement the eviction or remove the field.
  • Tag filtering uses LIKE '%"tag"%' on JSON arrays - substring matching can produce false positives (tag "au" matches "auth").

P3 - Performance (acceptable now, track for later):

  • findDuplicate does O(N) full table scan on every store. Fine for <1k memories, needs an index for larger stores.
  • Memory store is always created on distill api startup even when unused. Should be opt-in.

P4 - Code quality:

  • embeddingProvider interface in api_memory.go duplicates retriever.EmbeddingProvider.
  • extractSummary in decay.go is position-based only. The existing pkg/compress/extractive.go has a better scorer that could be reused.

Summary

Solid first implementation. The core store/recall/forget/decay loop works correctly and the integration across CLI, API, and MCP is complete. The P1 bug and P2 items should be addressed before merging to main; P3/P4 can be tracked as follow-up issues.

- P1: Make touchMemories synchronous (no goroutine leak risk)
- P2: Remove unused MaxMemories from config
- P2: Use junction table (memory_tags) for exact tag matching
- P3: Make memory store opt-in via --memory flag in api/mcp
- P4: Replace local embeddingProvider with retriever.EmbeddingProvider
- P4: Reuse pkg/compress extractive scorer in decay extractSummary
- Fix scan-then-process pattern to avoid SQLite single-conn deadlocks

Co-authored-by: Ona <no-reply@ona.com>
@Siddhant-K-code
Copy link
Owner Author

Updated Review - Post Refactor

CI passing (build + lint), all 11 tests pass, 0 lint issues. Previous P1-P4 findings addressed.

Verdict: Approve with minor suggestions

No blocking issues. Architecture is sound, single-connection SQLite pattern is correctly handled, test coverage is good.

Remaining Findings

Priority File Finding
P2 sqlite.go:143-157 findDuplicate does a full table scan of all embeddings. O(n) per insert. Fine for < 10K entries, add a TODO for scaling.
P3 sqlite.go:296-310 Stats has sequential defer rows.Close() - works but fragile with single-conn. Use scan-then-process for consistency.
P3 decay.go:120-131 extractSummary allocates a new compressor per call. Make it a package-level var.
P3 mcp.go:159 Memory DB path hardcoded in MCP. API server reads from viper. Add --memory-db flag.
Nit sqlite.go:39-42 WAL mode has no benefit with MaxOpenConns(1). Remove or comment.
Nit sqlite.go:82-84 PRAGMA foreign_keys should be in NewSQLiteStore alongside other PRAGMAs, not in migrate().
Nit decay.go:148-161 isStopWord rebuilds map on every call. Move to package-level var.
Nit memory.go:260 Trailing blank line.

What's Good

  • Junction table for tags - correct approach
  • Scan-then-process in Recall - avoids single-connection deadlock
  • --memory opt-in flag - clean separation
  • retriever.EmbeddingProvider reuse - no duplicate interface
  • Extractive compressor reuse in decay
  • 11 tests covering all key paths
  • Clean type separation (StoreEntry / Entry / RecalledMemory)

Siddhant-K-code and others added 2 commits February 22, 2026 19:02
- Add TODO to findDuplicate noting O(n) full table scan scaling limit
- Refactor Stats to scan-then-close each query (consistent with Recall)
- Move extractSummary compressor to package-level var (avoid per-call alloc)
- Add --memory-db flag to MCP command (was hardcoded)
- Move PRAGMA foreign_keys to NewSQLiteStore alongside other PRAGMAs
- Move isStopWord map to package-level var (avoid per-call alloc)
- Remove trailing blank line in memory.go

Co-authored-by: Ona <no-reply@ona.com>
- Add Context Memory section with CLI, API, and MCP usage examples
- Add memory endpoints to API Endpoints table
- Add memory command to CLI Commands list
- Update architecture diagram: Memory Store is shipped, not planned
- Update roadmap: mark Context Memory Store as shipped
- Update intro blurb to reflect memory is available

Co-authored-by: Ona <no-reply@ona.com>
@Siddhant-K-code Siddhant-K-code merged commit b978636 into main Feb 23, 2026
2 checks passed
@Siddhant-K-code Siddhant-K-code deleted the feat/context-memory-store branch February 23, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Product] Context Memory Store - persistent deduplicated memory across sessions

1 participant