Memory Compression
Compress high-volume memory stores by clustering related memories, summarizing each cluster with an LLM, and replacing the verbose originals with compact canonical representations. Maintain recall quality while the agent keeps far fewer memories.
Start Free →- Running Dakera server (Quickstart)
- An agent ID with 500+ accumulated memories
- LLM client (OpenAI, Anthropic) for cluster summarization
- Familiarity with batch operations and scheduled jobs
The Problem: Memory Stores That Grow Faster Than They Age
An analytics agent processing daily reports stores 50–200 new memories per day. Over three months, that is 4,500–18,000 memories. Decay lowers the importance of memories nobody uses, but it never deletes anything, and many of these are legitimately important in the short term. The result: a bloated memory store where recall latency climbs, context windows overflow, and storage costs grow linearly with agent runtime.
Deduplication removes near-exact copies. Decay pushes unused entries down the ranking. But neither handles the class of memories that are individually distinct and important, but collectively redundant at a higher level of abstraction. A week of daily "DAU increased 3.2%" reports is seven distinct facts, but they can be compressed into one: "DAU grew 3.1% average over the week of May 13." Memory compression operates at this higher level.
Decay lowers the importance of unused memories over time (it never deletes them). Deduplication merges near-identical memories. Compression clusters semantically related memories (even if distinct) and replaces them with a denser, higher-level summary. Use all three together for maximum memory hygiene: decay → dedup → compress, in that order.
Architecture: Cluster, Summarize, Replace
The compression pipeline runs in three stages. First, your job lists the raw memories of one past time window (below importance 0.8, so earlier summaries are left alone) and groups them into clusters: by a shared tag or metric, or by semantic similarity on embeddings it computes (recall results carry text, not vectors). Second, each cluster is passed to an LLM for summarization; the output is a single, dense canonical memory that preserves the key facts from all cluster members. Third, the canonical summary is stored at high importance and the originals in the cluster are deleted, or demoted to importance 0.08 when you need an audit trail. Dakera stores, lists and deletes the memories; clustering, the LLM call and validation run in your job.
- Group related memories within one time window (5–30 memories per cluster, at least 3)
- Summarize each cluster with an LLM — preserve quantitative facts, dates, and conclusions
- Store the canonical summary at importance 0.85–0.92, tagged
["compressed", "cluster-{id}"] - Delete the originals, or demote them to importance 0.08 when an audit trail is required
- Run compression monthly or when the store exceeds a size threshold
POST /v1/agents/{id}/compress (compress_agent() in Python, compressAgent() in TypeScript) runs a compression pass for an agent; from v0.12.2 each summary is validated before it is written, and a refused one is listed in summaries_skipped with its cluster's originals kept. POST /v1/knowledge/summarize turns memories you choose into one summary memory. AutoPilot, on by default, also merges groups of three or more memories that share at least two tags and are all below importance 0.3, every 12 hours. Build the pipeline on this page when you need control over clustering, the prompt and validation.
Diagram: Before and After Compression
Diagram: Compression Pipeline
Real-World Scenario: Analytics Agent Compressing Weeks of Data
Scenario: MetricsMind AI builds an analytics agent that monitors SaaS KPIs. The agent stores one memory per metric per day: DAU, revenue, churn, NPS, support tickets. With 8 metrics tracked daily (those five and three more), it accumulates 240 memories per month. After 3 months: 720 raw data-point memories. Recall for "revenue trend Q1" returns dozens of individual day-level facts, overwhelming the context window.
MetricsMind runs monthly compression. The pipeline clusters memories by metric type (cluster A: all DAU memories, cluster B: all revenue memories, etc.), then calls GPT-4o to summarize each cluster: "DAU grew from 12,400 to 15,200 over Q1 (+22.6%), with a 3-day dip in week 8 due to a login outage." Each month's 240 day-level memories compress to 8 cluster summaries, one per metric; with the originals deleted, that is 97% fewer memories for the month. Recall for any metric now returns one clean, human-readable trend summary.
Step-by-Step Implementation
-
Set up the compression triggerDefine when compression runs: on a schedule (weekly/monthly), when memory count exceeds a threshold (e.g., 500 agent memories), or after batch ingestion. For analytics agents, monthly compression after the end-of-month data export is the natural trigger. Do not run compression more than weekly — it is computationally expensive.
-
Retrieve all candidate memoriesList the window with filter-based batch recall (
POST /v1/memories/recall/batch,batch_recall()): no query and no embedding, filtered by tags, importance and creation time (created_after,created_before). Take memories created more than 30 days ago and below importance 0.8: summaries from previous runs (0.85–0.92) are excluded because they are already compressed. -
Cluster by semantic similarityGroup the memories into clusters. Structured data points group by a shared tag or metric, as in the example below. Free-text memories need semantic similarity: compute embeddings with your own model (recall results carry text, not vectors), then for small stores (<500) use greedy clustering (sim > 0.78) and for large ones k-means. Target cluster sizes of 5–30 memories.
-
Summarize each cluster with an LLMFor each cluster, pass all member contents to an LLM with a structured summarization prompt. The prompt should instruct the LLM to: preserve all quantitative values (numbers, percentages, dates), identify the time range covered, and produce a single dense paragraph. Validate that the output is non-empty and that every number in it appears in the inputs before storing.
-
Store compressed summaries at high importanceStore each cluster summary at importance 0.85–0.92 with tags
["compressed", "cluster-{uuid}", "range-{start}-{end}"]. The range tags enable targeted retrieval (e.g., recall only Q1 summaries). Set no TTL — compressed summaries are the permanent record. -
Demote originals to trigger decayUse
forget(POST /v1/memory/forgettakes a list of IDs) for hard delete, orupdate_importanceto set each original to importance 0.08. Soft demotion is preferred when audit requirements exist. Hard delete is appropriate for analytics data where raw day-level points are no longer needed after compression.
Before & After: Analytics Memory Store
[
{ "id": "m-001", "content": "DAU: 12,421 on May 13",
"importance": 0.55, "tags": ["dau", "daily"] },
{ "id": "m-002", "content": "DAU: 12,509 on May 14",
"importance": 0.55, "tags": ["dau", "daily"] },
{ "id": "m-003", "content": "DAU: 12,388 on May 15 (login outage)",
"importance": 0.65, "tags": ["dau", "daily", "incident"] },
{ "id": "m-004", "content": "Revenue: $48,220 on May 13",
"importance": 0.60, "tags": ["revenue", "daily"] },
{ "id": "m-005", "content": "Revenue: $49,100 on May 14",
"importance": 0.60, "tags": ["revenue", "daily"] },
{ "id": "m-006", "content": "Churn: 2.08% week of May 13",
"importance": 0.58, "tags": ["churn", "weekly"] },
// ... more similar memories
]
// recall("DAU trend") returns individual day records
// (19 DAU points from May 13 to 31)
// context window filled with raw data points
[
{
"id": "m-comp-dau-001",
"content": "DAU SUMMARY May 13-31: avg 12,890/day, peak 13,420 (May 29), low 12,388 (May 15, login outage). Growth +3.8% vs prior period.",
"importance": 0.90,
"tags": ["compressed", "cluster-dau", "range-may13-may31", "dau"]
},
{
"id": "m-comp-rev-001",
"content": "REVENUE SUMMARY May 13-31: avg $48,980/day, total $930,620. Steady +0.95%/day trend. No anomalies.",
"importance": 0.88,
"tags": ["compressed", "cluster-revenue", "range-may13-may31", "revenue"]
},
{
"id": "m-comp-churn-001",
"content": "CHURN SUMMARY May wk2-wk4: stable at 2.1% avg. Slight uptick 2.3% week of May 20 (investigated, no root cause). Within target.",
"importance": 0.87,
"tags": ["compressed", "cluster-churn", "range-may13-may31", "churn"]
}
]
// recall("DAU trend") returns m-comp-dau-001 only
// Full month's insight in one clean memory
Implementation
# 1. List one past window of raw analytics memories (filter-based, no query)
# created_after / created_before are Unix seconds
curl -X POST http://localhost:3000/v1/memories/recall/batch \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "analytics-bot",
"filter": {
"tags": ["daily", "weekly"],
"max_importance": 0.8,
"created_after": 1747094400,
"created_before": 1748736000
},
"limit": 1000
}'
# 2. After LLM summarization, store compressed cluster summary
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "analytics-bot",
"content": "DAU SUMMARY May 13-31: avg 12,890/day, peak 13,420 (May 29), low 12,388 (May 15 login outage). Growth +3.8% vs prior.",
"importance": 0.90,
"memory_type": "semantic",
"tags": ["compressed", "cluster-dau", "range-may13-may31", "dau"]
}'
# 3a. Hard-delete the originals (analytics: no audit needed)
curl -X POST http://localhost:3000/v1/memory/forget \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{"agent_id": "analytics-bot", "memory_ids": ["m-001", "m-002", "m-003"]}'
# 3b. Or demote one original instead, when an audit trail is required
curl -X PATCH http://localhost:3000/v1/memories/m-004/importance \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{"agent_id": "analytics-bot", "importance": 0.08}'
# 4. Verify: recall now returns the compressed summary
curl -X POST http://localhost:3000/v1/memory/recall \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{"agent_id": "analytics-bot", "query": "DAU trend May", "min_importance": 0.7, "top_k": 5}'from dakera import DakeraClient, BatchRecallRequest, BatchMemoryFilter
import openai
import re
import uuid
from collections import defaultdict
from datetime import datetime, timedelta, timezone
client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")
oai = openai.OpenAI()
AGENT = "analytics-bot"
def cluster_by_metric(memories):
"""Group data points by metric: the content starts with "DAU:", "Revenue:", ..."""
clusters = defaultdict(list)
for m in memories:
metric = m.content.split(":", 1)[0].strip().lower()
clusters[metric].append(m)
return clusters
def summarize_cluster(cluster):
"""LLM summarization of a memory cluster."""
contents = "\n".join([f"- {m.content}" for m in cluster])
resp = oai.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Summarize these analytics data points into a single dense paragraph. Preserve all quantitative values, dates, and trends. Highlight anomalies."
}, {
"role": "user",
"content": f"Data points:\n{contents}"
}]
)
return resp.choices[0].message.content
def numbers_are_grounded(summary: str, cluster) -> bool:
"""Every number in the summary must appear in at least one input memory."""
source = " ".join(m.content for m in cluster)
return all(n in source for n in re.findall(r"\d[\d,.]*\d|\d", summary))
def compress_window(start: datetime, end: datetime, min_cluster_size: int = 3, hard_delete: bool = True):
"""Compress one time window of raw analytics memories."""
candidates = client.batch_recall(BatchRecallRequest(
agent_id=AGENT,
filter=BatchMemoryFilter(
tags=["daily", "weekly"], # raw data points; summaries are not tagged daily
max_importance=0.8, # summaries (0.85-0.92) are excluded
created_after=int(start.timestamp()),
created_before=int(end.timestamp()),
),
limit=1000,
))
clusters = cluster_by_metric(candidates.memories)
print(f"Found {len(clusters)} clusters from {len(candidates.memories)} memories")
for metric, cluster in clusters.items():
if len(cluster) < min_cluster_size:
continue # Skip tiny clusters: not worth compressing
summary = summarize_cluster(cluster)
if not summary or not numbers_are_grounded(summary, cluster):
continue # Skip empty summaries and summaries with invented numbers
cluster_id = str(uuid.uuid4())[:8]
client.store_memory(
agent_id=AGENT,
content=summary,
importance=0.90,
memory_type="semantic",
tags=["compressed", f"cluster-{cluster_id}", f"range-{start:%b%d}-{end:%b%d}".lower(), metric]
)
ids = [m.id for m in cluster]
if hard_delete:
for memory_id in ids: # analytics: no audit needed
client.forget(agent_id=AGENT, memory_id=memory_id)
else:
client.update_importance(agent_id=AGENT, memory_ids=ids, importance=0.08)
print(f"Cluster {cluster_id} ({metric}): {len(cluster)} memories -> 1 summary")
# Run monthly: compress the 4-week window that ended 30 days ago
now = datetime.now(timezone.utc)
compress_window(start=now - timedelta(days=58), end=now - timedelta(days=30))import { DakeraClient } from '@dakera-ai/dakera';
import OpenAI from 'openai';
import { randomUUID } from 'crypto';
const client = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: 'dk-...' });
const oai = new OpenAI();
const AGENT = 'analytics-bot';
// Group data points by metric: the content starts with "DAU:", "Revenue:", ...
function clusterByMetric<T extends { content: string }>(memories: T[]): Map<string, T[]> {
const clusters = new Map<string, T[]>();
for (const m of memories) {
const metric = m.content.split(':')[0].trim().toLowerCase();
clusters.set(metric, [...(clusters.get(metric) ?? []), m]);
}
return clusters;
}
async function summarizeCluster(cluster: { content: string }[]): Promise<string> {
const contents = cluster.map(m => `- ${m.content}`).join('\n');
const res = await oai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'Summarize these analytics data points into a single dense paragraph. Preserve all quantitative values, dates, and anomalies.' },
{ role: 'user', content: `Data:\n${contents}` },
],
});
return res.choices[0].message.content ?? '';
}
// Every number in the summary must appear in at least one input memory
function numbersAreGrounded(summary: string, cluster: { content: string }[]): boolean {
const source = cluster.map(m => m.content).join(' ');
return (summary.match(/\d[\d,.]*\d|\d/g) ?? []).every(n => source.includes(n));
}
async function compressWindow(start: Date, end: Date, minClusterSize = 3) {
const { memories } = await client.batchRecall({
agent_id: AGENT,
filter: {
tags: ['daily', 'weekly'],
max_importance: 0.8,
created_after: Math.floor(start.getTime() / 1000),
created_before: Math.floor(end.getTime() / 1000),
},
limit: 1000,
});
const clusters = clusterByMetric(memories);
console.log(`Found ${clusters.size} clusters from ${memories.length} memories`);
for (const [metric, cluster] of clusters) {
if (cluster.length < minClusterSize) continue;
const summary = await summarizeCluster(cluster);
if (!summary || !numbersAreGrounded(summary, cluster)) continue;
const clusterId = randomUUID().slice(0, 8);
await client.storeMemory(AGENT, {
content: summary,
importance: 0.90,
memory_type: 'semantic',
tags: ['compressed', `cluster-${clusterId}`, metric],
});
// Hard-delete the originals (analytics: no audit needed)
for (const m of cluster) await client.forget(AGENT, m.id);
console.log(`Cluster ${clusterId} (${metric}): ${cluster.length} memories -> 1 summary`);
}
}
// Run monthly: compress the 4-week window that ended 30 days ago
const day = 86_400_000;
await compressWindow(new Date(Date.now() - 58 * day), new Date(Date.now() - 30 * day));use dakera_client::{BatchMemoryFilter, BatchRecallRequest, DakeraClient, ForgetRequest,
MemoryType, StoreMemoryRequest};
use std::collections::HashMap;
let client = DakeraClient::builder("http://localhost:3000")
.api_key("dk-...")
.build()?;
let agent = "analytics-bot";
// 1. List raw data points (summaries are tagged "compressed", not "daily")
let candidates = client.batch_recall(
BatchRecallRequest::new(agent)
.with_filter(
BatchMemoryFilter::default()
.with_tags(vec!["daily".into(), "weekly".into()])
.with_max_importance(0.8),
)
.with_limit(1000),
).await?;
// 2. Group by metric: the content starts with "DAU:", "Revenue:", ...
let mut clusters: HashMap<String, Vec<_>> = HashMap::new();
for m in &candidates.memories {
let metric = m.content.split(':').next().unwrap_or("").trim().to_lowercase();
clusters.entry(metric).or_default().push(m);
}
// 3. For each cluster: summarize with your LLM, store, then delete the originals
for (metric, cluster) in clusters {
if cluster.len() < 3 { continue; }
let summary = summarize_with_llm(&cluster).await?; // your LLM call
let cluster_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
client.store_memory(
StoreMemoryRequest::new(agent, summary)
.with_type(MemoryType::Semantic)
.with_importance(0.90)
.with_tags(vec!["compressed".into(), format!("cluster-{}", cluster_id), metric.clone()]),
).await?;
let ids: Vec<String> = cluster.iter().map(|m| m.id.clone()).collect();
client.forget(ForgetRequest::by_ids(agent, ids)).await?;
println!("Cluster {} ({}): {} -> 1 summary", cluster_id, metric, cluster.len());
}package main
import (
"context"
"fmt"
"strings"
dakera "github.com/dakera-ai/dakera-go"
)
func compressAgentMemories(ctx context.Context, client *dakera.Client, agent string) {
// 1. List raw data points (summaries are tagged "compressed", not "daily")
maxImp := float32(0.8)
candidates, _ := client.BatchRecall(ctx, dakera.BatchRecallRequest{
AgentID: agent,
Filter: dakera.BatchMemoryFilter{Tags: []string{"daily", "weekly"}, MaxImportance: &maxImp},
Limit: 1000,
})
// 2. Group by metric: the content starts with "DAU:", "Revenue:", ...
clusters := map[string][]dakera.RecalledMemory{}
for _, m := range candidates.Memories {
metric := strings.ToLower(strings.TrimSpace(strings.SplitN(m.Content, ":", 2)[0]))
clusters[metric] = append(clusters[metric], m)
}
fmt.Printf("Found %d clusters from %d memories\n", len(clusters), len(candidates.Memories))
for metric, cluster := range clusters {
if len(cluster) < 3 { continue }
summary := summarizeWithLLM(cluster) // your LLM call
clusterID := randomID(8)
importance := float32(0.90)
// Store compressed summary
client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{
Content: summary,
Importance: &importance,
MemoryType: "semantic",
Tags: []string{"compressed", "cluster-" + clusterID, metric},
})
// Delete the originals
for _, m := range cluster {
client.Forget(ctx, agent, m.ID)
}
fmt.Printf("Cluster %s (%s): %d memories -> 1 summary\n", clusterID, metric, len(cluster))
}
}Fewer, denser memories for analytics agents
Dakera's batch recall, store and forget primitives make compression a short pipeline.
SDK Reference
| Method | SDK | Purpose |
|---|---|---|
batch_recall(BatchRecallRequest(agent_id, filter, limit)) | Python | List one window of candidate memories, no query |
batchRecall({agent_id, filter, limit}) | TypeScript | List one window of candidate memories, no query |
recall(agent_id, query, min_importance, top_k) | Python | Check what a query returns after compression |
store_memory(agent_id, content, importance, memory_type, tags) | Python | Store the compressed cluster summary |
storeMemory(agentId, {content, importance, memory_type, tags}) | TypeScript | Store the compressed cluster summary |
forget(agent_id, memory_id) | Python | Hard-delete an original after compression |
forget(agentId, memoryId) | TypeScript | Hard-delete an original after compression |
update_importance(agent_id, memory_ids, importance) | Python | Soft-demote originals (audit-friendly alternative to delete) |
updateImportance(agentId, {memory_ids, importance}) | TypeScript | Soft-demote originals (audit-friendly alternative to delete) |
Performance Considerations
- LLM calls are the dominant cost. The job makes one LLM call per cluster, so cost grows with the number of clusters, not the number of memories. A smaller model is often enough for clusters with few quantitative facts.
- Clustering is O(N²) for greedy clustering. Grouping by tag or metric is linear. For large free-text windows, use k-means on your embeddings (scikit-learn or faiss) instead; switch when your memory count regularly exceeds 1,000.
- Delete a cluster in one request.
POST /v1/memory/forgettakes a list ofmemory_ids, so one call per cluster removes all its originals; the Python and TypeScriptforget()helpers take one ID per call. - Recall diversity improves dramatically. Before compression, a query for "May analytics" returns DAU daily records, up to
top_kof them: effectively one answer repeated. After compression, the same query returns 3 summaries covering DAU, revenue, and churn: genuine diversity. Measure recall diversity before and after to validate compression quality.
Edge Cases
LLMs can hallucinate numerical values in summarization, especially when the input data has irregular patterns. Always validate compressed summaries by checking that all numerical values in the output appear in at least one input memory. Use a regex extraction pass to compare input values vs. output values, and re-run the LLM call with a stricter prompt if values diverge by more than 5%.
Clustering by semantic similarity may group "DAU on May 15" with "DAU on July 22" — same metric, very different time periods. Before clustering, filter memories by a time window first: only cluster memories from the same 4-week period together. Never compress across more than 6 weeks into a single summary or you lose temporal precision.
With a high similarity threshold (0.90+), many memories end up as singleton clusters — no similar memories found. Compressing singletons produces a "summary" identical to the original. Add a minimum cluster size check (at least 3 members) before calling the LLM. Singletons can be left as-is or demoted with importance decay instead.
High-signal anomalies (a login outage on May 15 causing DAU to drop) can be diluted in a summary that averages the normal days around it. Before compressing a cluster, identify anomalous members (importance > 0.75, or tagged "incident") and store them separately as high-importance standalone memories before the cluster is compressed. The summary captures the trend; the anomaly memory captures the exception.
After 3 months, you may have 12 weekly summaries for the same metric. These can themselves be compressed into a quarterly summary. Design a multi-tier compression hierarchy: daily → weekly summary → monthly summary → quarterly summary. Tag each tier: "tier-1", "tier-2", "tier-3". Always exclude higher tiers from lower-tier compression runs to prevent recursive summarization.
AutoPilot consolidation (every 12 hours, on by default) merges groups of three or more memories that share at least two tags and all sit below importance 0.3. Originals demoted to 0.08 that keep their shared tags qualify, and so do raw data points that decay has pushed below 0.3 before your job runs. If the raw points must stay as they are for audit, give them tag sets that do not overlap, run compression before they decay that far, or turn AutoPilot off by setting DAKERA_AUTOPILOT_ENABLED to false.
Advanced Configuration: Clustering Strategies & LLM Prompt Templates
Clustering Strategy by Store Size
| Store Size | Clustering Method (embeddings from your model) | Threshold |
|---|---|---|
| <200 memories | Greedy pairwise | 0.78 |
| 200–1,000 memories | Greedy pairwise | 0.80 |
| 1,000–5,000 memories | k-means on embeddings (k=50) | 0.78 |
| >5,000 memories | Approximate nearest neighbors + label propagation | 0.82 |
Analytics Summarization Prompt
SYSTEM: You are an analytics data summarizer.
Summarize the following data points into a single paragraph.
Rules:
1. Preserve ALL numerical values exactly as given (no rounding beyond source precision)
2. State the date range covered
3. Identify the central metric (DAU, revenue, churn, etc.)
4. Highlight any anomalies or outliers (not just the average trend)
5. End with: overall direction (growing/stable/declining)
6. Keep the summary under 120 words
Do not invent values not present in the source data.
USER: Summarize:
{CLUSTER_CONTENTS}
Compression Schedule by Domain
| Agent Type | Compression Frequency | Min Age Before Compress |
|---|---|---|
| Analytics / BI agent | Weekly (after report cycle) | 7 days |
| Customer support agent | Monthly | 30 days |
| Personal assistant | Monthly | 60 days |
| Legal / compliance agent | Quarterly | 90 days |
Run this pattern against a real Dakera.
The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.