Skip to Content
FeaturesSession / Memory Management

Session / Memory Management

Purpose

Allow agents to maintain context across multiple turns and activities. Rather than starting every LLM call cold, agents can resume a prior conversation — preserving reasoning context, established brand tone, and intermediate results without re-sending all prior content.

Related: Agent Execution Engine — adapter implementations | Databasesessions table | Skills System — the client context file is always re-injected (skills), separate from session history


Why Sessions Matter

  • A Activity Planner that decomposes a brief into 6 subtasks can later resume to synthesise results with full context of the original brief.
  • A Copywriter writing a 10-part blog series can resume with brand voice already established in the session rather than re-injecting it on every call.
  • Session resumption dramatically reduces token spend on continuation tasks.

Claude Session Management

How it works

Claude Code CLI natively manages session state. On the first call, Claude creates a session and stores it as a .jsonl file:

~/.claude/projects/<cwd>/<session-id>.jsonl

Each line is a conversation turn. The session ID is returned in the system/init NDJSON event on stdout.

Resumption

Pass --resume <sessionId> on subsequent calls. Claude reloads the .jsonl file and continues the conversation from where it left off.

# First call — new session claude --output-format stream-json -p "Write a blog post outline about X" # → emits: { "type": "system", "session_id": "sess_01abc..." } # Resume call — continues context claude --output-format stream-json --resume sess_01abc -p "Now write section 2 in full"

Our responsibility

Claude manages the file storage. We wrap this with a DB record for:

  • Associating the session with a specific campaign_id and agent_type
  • Tracking token usage and cost accumulating across the session
  • Enforcing TTL expiry (session cleanup policy)
  • Enabling the Activity Planner to look up the right session when dispatching continuation tasks

Ollama Session Management

Ollama has no built-in session system. We maintain conversation state manually.

Approach

Store the full messages array in the sessions.message_history JSONB column. Before each call, load the history from DB; after each call, append the new assistant message and write back.

// Load history const session = await db.query.sessions.findFirst({ where: and(eq(sessions.campaignId, campaignId), eq(sessions.agentType, agentType)) }); const history = session?.messageHistory ?? []; // Build request const messages = [ { role: 'system', content: systemPrompt + injectedSkills }, ...history, { role: 'user', content: taskPrompt } ]; // Call Ollama const response = await ollamaChat(model, messages); // Persist updated history await db.update(sessions).set({ messageHistory: [...history, { role: 'user', content: taskPrompt }, response.message], tokenCount: (session?.tokenCount ?? 0) + response.eval_count, }).where(eq(sessions.id, session.id));

Truncation strategy

Ollama context windows are finite. When messageHistory exceeds a configured token threshold (e.g. 80% of model context), a summarisation pass runs before the next call:

  1. Send the history to the same Ollama model with a summarisation prompt.
  2. Replace the full history with a single system message containing the summary.
  3. Continue the conversation from the condensed context.

This keeps costs near-zero (Ollama = free) while preserving critical context.


Database Schema

sessions table

CREATE TABLE sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), external_session_id VARCHAR(255), -- Claude session ID; null for Ollama agent_type VARCHAR(100) NOT NULL, campaign_id UUID NOT NULL REFERENCES campaigns(id), model VARCHAR(100) NOT NULL, message_history JSONB, -- Ollama multi-turn; null for Claude token_count INTEGER DEFAULT 0, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX ON sessions(external_session_id); CREATE INDEX ON sessions(campaign_id, agent_type);

Lookup pattern

When a new task arrives for an agent on an existing campaign, the queue worker looks up the session before starting the executor:

async function resolveSession(campaignId: string, agentType: string): Promise<Session | null> { return db.query.sessions.findFirst({ where: and( eq(sessions.campaignId, campaignId), eq(sessions.agentType, agentType), gt(sessions.expiresAt, new Date()) // not expired ) }); }

Session Lifecycle

Task arrives for campaign X, agent type: copywriter Look up session (campaign_id=X, agent_type=copywriter, not expired) found? ┌─────┴──────┐ YES NO │ │ Resume Create new session record session (no external_session_id yet) │ │ └─────┬───────┘ Execute LLM call On completion: - Claude: upsert session record with external_session_id from init event - Ollama: append messages to message_history, update token_count Link session_id to task_run record

Session Expiry & Cleanup

Sessions have a configurable TTL. Defaults:

ProviderDefault TTL
Claude30 days (aligns with Claude’s own project session retention)
Ollama7 days

A cleanup cron job runs nightly:

// Runs at 02:00 UTC daily async function cleanupExpiredSessions() { const deleted = await db.delete(sessions) .where(lt(sessions.expiresAt, new Date())) .returning({ id: sessions.id }); // Claude sessions: the .jsonl file is managed by Claude CLI itself // Ollama sessions: DB record deletion is sufficient (no file to clean) logger.info(`Cleaned up ${deleted.length} expired sessions`); }

Optional: Long Session Summarisation (Claude)

For very long-running campaigns, Claude session .jsonl files can grow large, increasing the token cost of resumption. To mitigate:

  1. Monitor sessions.token_count — when it exceeds a threshold (configurable, e.g. 50k tokens), trigger summarisation.
  2. Start a new Claude session with a summarised brief instead of resuming the full history.
  3. Update the sessions record with the new external_session_id and reset token_count.

This is optional — only needed for campaigns with many continuation tasks.


Package Location

Session management logic lives in packages/agent-engine/src/sessions/:

sessions/ ├── resolver.ts # resolveSession() — find or create session for a task ├── claude-session.ts # extract session ID from NDJSON, upsert record ├── ollama-session.ts # load/save message_history, truncation logic ├── cleanup.ts # TTL expiry cron handler └── types.ts

© 2026 Leadmetrics — Internal use only