Cross-Session Context
Bridge the gap between disconnected conversations. Let your agent pick up exactly where it left off — days or weeks later — without asking users to repeat themselves.
- Running Dakera server (Quickstart guide)
- Understanding of session start/end lifecycle
- Familiarity with episodic vs. semantic memory types
The Problem
Modern AI assistants are amnesiac by default. A customer opens a support ticket on Monday, describes their entire infrastructure setup, gets partial help, and closes the chat. On Wednesday they return with a follow-up. From the AI's perspective, it is a complete stranger. The customer must re-explain their stack, their constraints, and the context of the problem — a deeply frustrating experience that destroys trust.
The underlying cause is that LLM context windows only span a single session. Appending entire conversation histories to every request is token-expensive, hits limits fast, and fills the context with noise rather than signal. What you need is a curated context bridge: a compact, high-signal summary of prior sessions that can be recalled in milliseconds.
Dakera sessions track when interactions happened — they are time-bounded containers. Memories are the durable, queryable facts extracted from those sessions. This pattern uses both: a session per conversation (start_session / end_session), and memories stored with that session_id to carry the context into later sessions. Memories are never deleted by decay; they stay until you forget them or a TTL expires.
Architecture: Session Timeline with Context Bridging
This pattern builds a session bridge: at the end of each session, key decisions, open questions, and state changes are distilled and stored as memories. At the start of any new session, a targeted recall surfaces the most relevant prior context and rehydrates the agent's working memory.
Step-by-Step Implementation
-
Design your agent ID schemeUse a stable, per-user agent ID like
support-customer-{customerId}. This is the persistent identity that all session memories accumulate under. Do not use session-scoped IDs for the agent — those would prevent cross-session retrieval entirely. -
Recall prior context at session startBefore the user's first message hits your LLM, fire a recall with a broad query ("recent project progress, open tasks, constraints") and inject the results into your system prompt. Use
min_importance=0.72to capture medium-confidence context without pulling in low-signal memories. -
Select what to store during the sessionNot every exchange warrants storage. Focus on: decisions made, open problems not yet resolved, technical constraints stated, and state transitions (completed X, started Y). Skip: pleasantries, clarifying back-and-forth, and content the user will re-state naturally next time.
-
Store granular facts at session endStore 3–6 separate memories rather than one large summary blob. Each memory retrieves independently, so granular facts score more precisely against specific queries. Assign importance 0.75–0.90 based on how critical the fact is for future sessions.
-
Decay old sessions with summarizationAfter 50 sessions, older memories become noisy. Pair this pattern with Summarization & Decay to consolidate old sessions into a compact historical summary and reduce the importance of archived memories so fresh context always wins.
Implementation
# --- START OF SESSION 1 ---
curl -X POST http://localhost:3000/v1/sessions/start \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{"agent_id": "support-customer-9182"}'
# -> {"id": "sess_mon_0930", ...}
# --- END OF SESSION 1 ---
# Store distilled context: one memory per key fact
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "support-customer-9182",
"session_id": "sess_mon_0930",
"content": "Customer runs FastAPI backend with PostgreSQL 14 on AWS ECS.",
"importance": 0.85,
"memory_type": "episodic",
"tags": ["session-context", "infrastructure"]
}'
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "support-customer-9182",
"session_id": "sess_mon_0930",
"content": "Auth module complete (JWT HS256 + refresh tokens). Next open task: rate limiting middleware.",
"importance": 0.90,
"memory_type": "episodic",
"tags": ["session-context", "progress", "open-task"]
}'
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{
"agent_id": "support-customer-9182",
"session_id": "sess_mon_0930",
"content": "Constraint: must stay within AWS free tier. No external paid rate-limiting services.",
"importance": 0.82,
"memory_type": "semantic",
"tags": ["session-context", "constraint", "budget"]
}'
curl -X POST http://localhost:3000/v1/sessions/sess_mon_0930/end \
-H "Authorization: Bearer dk-..."
# --- START OF SESSION 2 (2 days later) ---
curl -X POST http://localhost:3000/v1/memory/recall \
-H "Authorization: Bearer dk-..." \
-H "Content-Type: application/json" \
-d '{"agent_id": "support-customer-9182", "query": "project progress open tasks constraints", "top_k": 8, "min_importance": 0.72}'from dakera import DakeraClient
client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")
class CrossSessionAgent:
def __init__(self, customer_id: str):
self.agent_id = f"support-customer-{customer_id}"
self.session_id = None
def start_session(self, user_message: str) -> str:
"""Open a Dakera session, recall prior context and build the system prompt."""
self.session_id = client.start_session(agent_id=self.agent_id)["id"]
prior = client.recall(
agent_id=self.agent_id,
query=f"project progress, open tasks, decisions, constraints: {user_message}",
top_k=8,
min_importance=0.72
)
if not prior.memories:
return "You are a helpful support agent. First conversation with this customer."
ctx_lines = "\n".join(f"- {m.content}" for m in prior.memories)
return (
"You are a support agent with memory of prior sessions.\n\n"
f"PRIOR SESSION CONTEXT:\n{ctx_lines}\n\n"
"Continue from where we left off. Do not ask the user to repeat themselves."
)
def end_session(self, outcomes: list) -> None:
"""Store distilled session outcomes, then end the session."""
for o in outcomes:
client.store_memory(
agent_id=self.agent_id,
content=o["content"],
memory_type=o.get("memory_type", "episodic"),
importance=o.get("importance", 0.80),
tags=o.get("tags", ["session-context"]),
session_id=self.session_id
)
client.end_session(self.session_id)
def get_session_list(self) -> list:
"""All sessions of this agent, newest first."""
return client.list_sessions(agent_id=self.agent_id)
# --- SESSION 1 (Monday) ---
agent = CrossSessionAgent("9182")
print(agent.start_session("help with FastAPI"))
# Output: first-conversation message
# ... LLM conversation happens ...
agent.end_session([
{"content": "Customer runs FastAPI + PostgreSQL 14 on AWS ECS",
"importance": 0.85, "tags": ["session-context", "infrastructure"]},
{"content": "Auth module complete (JWT HS256). Rate limiting is next.",
"importance": 0.90, "tags": ["session-context", "progress", "open-task"]},
{"content": "Constraint: AWS free tier only, no paid services",
"importance": 0.82, "memory_type": "semantic", "tags": ["session-context", "constraint"]},
])
# --- SESSION 2 (Wednesday, new process) ---
agent2 = CrossSessionAgent("9182")
system2 = agent2.start_session("I am back to add rate limiting")
print(system2)
# Output includes all 3 prior facts, agent picks up seamlesslyimport { DakeraClient } from '@dakera-ai/dakera';
const client = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: 'dk-...' });
interface SessionOutcome {
content: string;
importance?: number;
memory_type?: 'episodic' | 'semantic';
tags?: string[];
}
class CrossSessionAgent {
private agentId: string;
private sessionId?: string;
constructor(customerId: string) {
this.agentId = `support-customer-${customerId}`;
}
async startSession(userMessage: string): Promise<string> {
this.sessionId = (await client.startSession(this.agentId)).id;
const prior = await client.recall(
this.agentId,
`project progress, open tasks, decisions, constraints: ${userMessage}`,
{ top_k: 8, min_importance: 0.72 }
);
if (!prior.memories.length) {
return 'You are a helpful support agent. First conversation with this customer.';
}
const ctxLines = prior.memories.map(m => `- ${m.content}`).join('\n');
return `You are a support agent with memory of prior sessions.\n\nPRIOR CONTEXT:\n${ctxLines}\n\nContinue seamlessly without asking the user to repeat information.`;
}
async endSession(outcomes: SessionOutcome[]): Promise<void> {
await Promise.all(outcomes.map(o =>
client.storeMemory(this.agentId, {
content: o.content,
memory_type: o.memory_type ?? 'episodic',
importance: o.importance ?? 0.80,
tags: o.tags ?? ['session-context'],
session_id: this.sessionId,
})
));
if (this.sessionId) await client.endSession(this.sessionId);
}
async getSessionHistory() {
return client.listSessions({ agent_id: this.agentId });
}
}
// SESSION 1
const agent = new CrossSessionAgent('9182');
const sys1 = await agent.startSession('help with FastAPI project');
// ... conversation ...
await agent.endSession([
{ content: 'Customer runs FastAPI + PostgreSQL 14 on AWS ECS', importance: 0.85, tags: ['session-context', 'infrastructure'] },
{ content: 'Auth complete (JWT HS256). Rate limiting is next open task.', importance: 0.90, tags: ['session-context', 'open-task'] },
{ content: 'Constraint: AWS free tier only, no paid external services', importance: 0.82, memory_type: 'semantic', tags: ['session-context', 'constraint'] },
]);
// SESSION 2 (Wednesday)
const agent2 = new CrossSessionAgent('9182');
const sys2 = await agent2.startSession('back to add rate limiting');
// sys2 contains the prior context; the user never has to repeat themselvesuse dakera_client::{DakeraClient, MemoryType, RecallRequest, StoreMemoryRequest};
let client = DakeraClient::builder("http://localhost:3000").api_key("dk-...").build()?;
let agent_id = "support-customer-9182";
// --- SESSION 1: open a session; at its end, store distilled facts ---
let session = client.start_session(agent_id).await?;
let outcomes = vec![
("Customer runs FastAPI + PostgreSQL 14 on AWS ECS", 0.85f32, MemoryType::Episodic, "infrastructure"),
("Auth module complete (JWT HS256). Rate limiting is next open task.", 0.90, MemoryType::Episodic, "open-task"),
("Constraint: AWS free tier only, no paid external services.", 0.82, MemoryType::Semantic, "constraint"),
];
for (content, importance, memory_type, tag) in outcomes {
client.store_memory(
StoreMemoryRequest::new(agent_id, content)
.with_type(memory_type)
.with_importance(importance)
.with_tags(vec!["session-context".into(), tag.into()])
.with_session(session.id.clone()),
).await?;
}
client.end_session(&session.id, None).await?;
// --- SESSION 2: recall and rehydrate ---
let prior = client.recall(
RecallRequest::new(agent_id, "project progress open tasks constraints")
.with_top_k(8)
.with_min_importance(0.72),
).await?;
let ctx_lines: Vec<String> = prior.memories
.iter()
.map(|m| format!("- {}", m.content))
.collect();
let system_prompt = format!(
"You are a support agent with prior session memory.\n\nPRIOR CONTEXT:\n{}\n\nContinue seamlessly.",
ctx_lines.join("\n")
);package main
import (
"context"
"fmt"
"log"
"strings"
dakera "github.com/dakera-ai/dakera-go"
)
type outcome struct {
content string
importance float32
memoryType string
tags []string
}
func endSession(ctx context.Context, client *dakera.Client, agentID, sessionID string, outcomes []outcome) error {
for _, o := range outcomes {
importance := o.importance
if _, err := client.StoreMemory(ctx, agentID, dakera.StoreMemoryRequest{
Content: o.content,
MemoryType: o.memoryType,
Importance: &importance,
Tags: o.tags,
SessionID: sessionID,
}); err != nil {
return err
}
}
_, err := client.EndSession(ctx, sessionID)
return err
}
func startSession(ctx context.Context, client *dakera.Client, agentID, userMsg string) (string, string, error) {
session, err := client.StartSession(ctx, dakera.StartSessionRequest{AgentID: agentID})
if err != nil {
return "", "", err
}
minImp := float32(0.72)
prior, err := client.Recall(ctx, agentID, dakera.RecallRequest{
Query: fmt.Sprintf("project progress open tasks constraints: %s", userMsg),
TopK: 8,
MinImportance: &minImp,
})
if err != nil {
return "", "", err
}
if len(prior.Memories) == 0 {
return session.ID, "You are a helpful support agent. First conversation with this customer.", nil
}
lines := make([]string, 0, len(prior.Memories))
for _, m := range prior.Memories {
lines = append(lines, "- "+m.Content)
}
return session.ID, fmt.Sprintf(
"You are a support agent with memory of prior sessions.\n\nPRIOR CONTEXT:\n%s\n\nContinue without asking the user to repeat themselves.",
strings.Join(lines, "\n"),
), nil
}
func main() {
client := dakera.NewClientWithOptions(dakera.ClientOptions{
BaseURL: "http://localhost:3000",
APIKey: "dk-...",
})
ctx := context.Background()
agentID := "support-customer-9182"
// SESSION 1: open, converse, store distilled facts, end
sessionID, _, err := startSession(ctx, client, agentID, "help with FastAPI")
if err != nil {
log.Fatal(err)
}
// ... conversation ...
err = endSession(ctx, client, agentID, sessionID, []outcome{
{"Customer runs FastAPI + PostgreSQL 14 on AWS ECS", 0.85, "episodic", []string{"session-context", "infrastructure"}},
{"Auth module complete (JWT HS256). Rate limiting is next.", 0.90, "episodic", []string{"session-context", "open-task"}},
{"Constraint: AWS free tier only, no paid services", 0.82, "semantic", []string{"session-context", "constraint"}},
})
if err != nil {
log.Fatal(err)
}
// SESSION 2: recall prior context
_, system, err := startSession(ctx, client, agentID, "I am back to add rate limiting")
if err != nil {
log.Fatal(err)
}
_ = system // inject into the LLM call
}Stop making your users repeat themselves.
Ship cross-session memory in one afternoon with Dakera's session and recall APIs.
Before & After: Memory State
Contrast the agent's context at the start of Session 2 with and without this pattern.
{
// Agent has NO memory of Session 1.
// System prompt is generic.
// Agent asks user to re-explain stack.
system_prompt: "You are a helpful
support agent.",
// User is forced to repeat:
// "I told you on Monday I use
// FastAPI and PostgreSQL..."
memories_recalled: 0,
session_bridged: false
}
{
// Agent recalls 3 memories from
// Session 1 instantly.
system_prompt: "You are a support agent
with prior session memory.
PRIOR CONTEXT:
- FastAPI + PostgreSQL 14, AWS ECS
- Auth done. Rate limiting is next.
- Constraint: AWS free tier only",
memories_recalled: 3,
recall_latency_ms: 14,
session_bridged: true
// User says "I'm back for rate
// limiting" — agent starts coding.
}
Real-World Example: Long-Running Customer Support Relationship
Scenario: Helios Support AI powers enterprise customer support for a developer tooling company. Customers frequently open multiple tickets across weeks or months as they work through large integrations. Without cross-session context, every ticket starts from scratch. A customer who has been using the product for 6 months must still explain their AWS architecture on every call.
The 6-Month Customer Journey
Month 1, Session 1: Customer (enterprise account, 50-seat team) explains they run a microservices architecture on Kubernetes with 12 services. The agent stores: Kubernetes microservices, 12 services, GKE cluster.
Month 2, Session 4: Customer reports a performance issue with the search service. The agent recalls their full stack context instantly and asks targeted questions about search service load rather than generic "what is your setup?" questions. Session concludes with root cause: N+1 query pattern. Stored: Search service had N+1 query bug, resolved by dataloader in service #7.
Month 4, Session 11: Customer asks about upgrading from v2 to v3 API. The agent recalls all prior context — their 12-service architecture, the dataloader fix in service #7, their Kubernetes setup — and proactively flags that the v3 migration has a breaking change that will affect their search service implementation. The customer calls this the most impressive AI support experience they have had. They renew their annual contract that week.
The biggest mistake with cross-session context is storing raw conversation turns. A 40-turn conversation produces 40 noisy memories that compete with each other during recall. Instead, distill each session into 3–6 high-signal facts: what was decided, what was completed, what is still open. The distillation step — whether done by LLM or rule-based logic — is what makes cross-session recall precise rather than overwhelming.
Diagram: Life of a Session Fact
This diagram follows one distilled fact from the session that produced it into later sessions, and shows how it is kept current or retired.
Performance Characteristics
top_k for the session-start recallmin_importance for the session-start recallCross-session recall adds one recall to session startup. Run it in parallel with session initialization rather than sequentially after the first user message arrives. Past 50 sessions per user, consider applying the Summarization & Decay pattern to compress historical memories and keep retrieval fast.
Edge Cases & Developer Gotchas
Users frequently close browser tabs or apps without a formal session end event. If your end_session() logic only fires on clean disconnects, you will lose context from abruptly terminated sessions. Solution: Store context incrementally during the session (after each significant exchange), not just at the end. Dakera ends an idle session by itself after DAKERA_SESSION_IDLE_TIMEOUT_SECS (4 hours by default), but it does not extract your facts, so run a background job for sessions that ended without them.
Injecting 8 memories averaging 80 tokens each adds 640 tokens to every session. At scale this adds meaningful cost. Solution: Use min_importance to gate which memories enter the system prompt. A threshold of 0.80 typically yields 3–5 memories, keeping injection under 400 tokens. Use top_k=3 for cost-sensitive deployments.
A customer migrated from PostgreSQL to MySQL last month, but the memory still says "PostgreSQL." The agent confidently gives wrong advice. Solution: When a state change is detected ("we just switched from X to Y"), explicitly call update_memory or forget on the outdated fact. Add state-change detection to your memory extraction logic as a first-class concern.
Using a generic query like "recent context" returns the top memories by recency-weighted score, which may not be the most relevant memories for the current conversation topic. Solution: Incorporate the user's opening message into the recall query. If the user asks about billing, query for "billing, payment, subscription context." This dramatically improves precision.
If you build agent IDs dynamically and make a bug like "support-customer-" + req.customerId with an unvalidated input, you risk recalling the wrong customer's context. Solution: Always validate and sanitize the customer ID before constructing the agent ID. Combine with Namespace Isolation for hard tenant boundaries at the infrastructure level.
SDK Reference
| Operation | Python | TypeScript | Purpose |
|---|---|---|---|
| Store session fact | client.store_memory(agent_id, content, importance, memory_type, tags) |
client.storeMemory(agentId, {content, importance, memory_type, tags}) |
Persist a session outcome |
| Recall prior context | client.recall(agent_id, query, top_k, min_importance) |
client.recall(agentId, query, {top_k, min_importance}) |
Rehydrate cross-session context |
| List sessions | client.list_sessions(agent_id) |
client.listSessions({agent_id}) |
Get session history for user |
| Get session memories | client.session_memories(session_id) |
client.sessionMemories(sessionId) |
Retrieve all memories from a session |
| Update stale fact | client.update_memory(agent_id, memory_id, ...) |
client.updateMemory(agentId, memoryId, request) |
Correct outdated session context |
| Delete outdated fact | client.forget(agent_id, memory_id) |
client.forget(agentId, memoryId) |
Remove a superseded memory |
| Start / end a session | client.start_session(agent_id) / client.end_session(session_id) |
client.startSession(agentId) / client.endSession(sessionId) |
Group a conversation's facts under one session_id |
Advanced Configuration
Parallel Multi-Topic Recall at Session Start
When you need context across multiple domains at once, run the recalls concurrently instead of one after another. (Dakera's batch recall is filter-based and takes no query, so it does not fit here.)
import asyncio
from dakera import AsyncDakeraClient
async def load_context(agent_id: str) -> str:
async with AsyncDakeraClient("http://localhost:3000", api_key="dk-...") as client:
results = await asyncio.gather(
client.recall(agent_id=agent_id, query="infrastructure stack architecture", top_k=4),
client.recall(agent_id=agent_id, query="open tasks and unresolved issues", top_k=4),
client.recall(agent_id=agent_id, query="technical constraints and budget", top_k=3),
)
# Merge all results into the system prompt, without duplicates
seen, lines = set(), []
for r in results:
for m in r.memories:
if m.id not in seen:
seen.add(m.id)
lines.append(f"- {m.content}")
return "\n".join(lines)
Automatic Session Summarization on High Volume
When a customer exceeds 50 sessions, summarize the facts of the oldest 30. This works because every fact was stored with its session_id:
sessions = client.list_sessions(agent_id=agent_id) # newest first
if len(sessions) > 50:
# Facts stored in the oldest 30 sessions
old_memories = [m for s in sessions[-30:] for m in client.session_memories(s["id"])]
# Store one summary, forget the originals
summary = llm_summarize([m["content"] for m in old_memories])
client.store_memory(agent_id=agent_id, content=summary, importance=0.6,
memory_type="semantic", tags=["historical-summary"])
for m in old_memories:
client.forget(agent_id=agent_id, memory_id=m["id"])
TTL on Transient Session Context
Some session context is only relevant for a short time window. Use TTL to auto-expire it:
client.store_memory(
agent_id=agent_id,
content="Customer is attending our live webinar this week",
importance=0.70,
memory_type="episodic",
tags=["transient", "event"],
ttl_seconds=604800 # auto-expires in 7 days
)
When to Use This Pattern
- Customer support AIs serving recurring customers over weeks or months
- Project management assistants tracking long-running work items
- Sales AIs that remember prospect context across multiple touchpoints
- Onboarding assistants that track setup progress across multiple sessions
- Coding assistants working on large, multi-session feature implementations
- Any product where "the AI remembers your situation" is a retention driver
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.