How the SAM In-App Agent Uses Project Memory and Context in OpenSEO
The SAM in-app agent stores its long-term context in a project-scoped memory table (sam_project_memory), allowing persistent, cross-session knowledge that any SAM chat for the same project can access and update.
The SAM (Search-Assisted-Metadata) agent in OpenSEO functions as a Durable Object bound to a single project, giving it a persistent "brain" that survives individual chat sessions. Unlike typical conversational AI where context evaporates when the session ends, SAM accumulates research, notes, and intermediate findings in a shared project workspace.
Project-Scoped Memory Architecture
SAM's memory system centers on a single database table with a simple key-value interface defined in src/server/features/sam/SamProjectMemoryRepository.ts.
Core Operations: getBlock and setBlock
The repository exposes two primary methods:
getBlock(projectId, label)– Retrieves a named memory block for a project (lines 10-24)setBlock(projectId, label, content)– Upserts content, keyed by(projectId, label)composite (lines 27-38)
This design ensures that memory blocks like "memory" and "research_log" are shared across all chat sessions belonging to the same project, not isolated per conversation.
// src/server/features/sam/SamProjectMemoryRepository.ts
// Lines 10-24: Reading a persistent memory block
async function getBlock(projectId: string, label: string): Promise<string | null> {
const row = await db
.selectFrom("sam_project_memory")
.select("content")
.where("projectId", "=", projectId)
.where("label", "=", label)
.executeTakeFirst();
return row?.content ?? null;
}
// Lines 27-38: Writing/upserting a block
async function setBlock(projectId: string, label: string, content: string): Promise<void> {
await db
.insertInto("sam_project_memory")
.values({ projectId, label, content, updatedAt: new Date() })
.onConflict((oc) => oc.columns(["projectId", "label"]).doUpdateSet({ content, updatedAt: new Date() }))
.execute();
}
Durable Object Implementation in SamChatAgent.ts
The SamChatAgent.ts file implements the Durable Object that orchestrates memory access on every conversational turn.
Memory Lifecycle Per Turn
When processing a user message, SAM follows this sequence:
- Load the
"memory"block viaSamProjectMemoryRepository.getBlockto establish persisted context - Execute reasoning and search operations using the combined system prompt + memory
- Persist new findings by updating
"research_log"or"memory"viasetBlock
This write-back pattern ensures that research progress, discovered entities, and analytical conclusions accumulate project-wide.
// Example: Loading project memory at turn start
import { SamProjectMemoryRepository } from "@/server/features/sam/SamProjectMemoryRepository";
async function loadProjectMemory(projectId: string) {
const memory = await SamProjectMemoryRepository.getBlock(projectId, "memory");
return memory ?? "";
}
// Example: Appending to research log after completing analysis
async function appendResearchLog(projectId: string, note: string) {
const existing = await SamProjectMemoryRepository.getBlock(projectId, "research_log") ?? "";
const updated = existing + "\n---\n" + note;
await SamProjectMemoryRepository.setBlock(projectId, "research_log", updated);
}
System Prompt Integration
SAM's behavior is shaped by two concatenated components:
| Component | Source | Mutability |
|---|---|---|
| System Prompt | src/server/features/sam/samSystemPrompt.ts |
Immutable – defines SAM's core personality and capabilities |
| Memory Blocks | sam_project_memory table |
Mutable – accumulates project-specific knowledge |
On each LLM call, OpenSEO merges the static system prompt with the latest "memory" and "research_log" blocks, giving the model both its foundational instructions and current project context.
Database Schema
The underlying table structure is defined in src/db/sam.schema.ts:
| Column | Purpose |
|---|---|
projectId |
Foreign key to the OpenSEO project |
label |
Block identifier (e.g., "memory", "research_log") |
content |
Text payload (typically JSON or markdown) |
updatedAt |
Timestamp for cache invalidation and debugging |
The composite primary key on (projectId, label) enforces the 1:1 relationship between projects and their named memory blocks.
Key Design Decisions
Why project-scoped instead of session-scoped? SEO workflows span days or weeks. A researcher might open multiple SAM chats while investigating competitors, keywords, or technical issues. Project memory eliminates redundant research and enables progressive depth—each conversation builds on prior work.
Why separate memory and research_log blocks? This separation allows structured retrieval: "memory" typically holds compact, synthesized knowledge, while "research_log" preserves raw chronological findings that can be summarized or queried on demand.
Summary
- The SAM in-app agent persists context through
sam_project_memorytable operations inSamProjectMemoryRepository.ts - getBlock/setBlock provide simple read/write semantics keyed by
(projectId, label) SamChatAgent.tsloads memory at turn start and writes updates after reasoning completes- Memory is project-wide, enabling cross-session continuity for SEO research workflows
- The immutable system prompt from
samSystemPrompt.tscombines with mutable memory blocks to form complete LLM context
Frequently Asked Questions
How does SAM memory differ from typical chatbot memory?
Typical chatbots retain context only within a single conversation thread, often using limited token windows. SAM's memory is persistent and project-scoped—any chat opened for the same OpenSEO project accesses identical accumulated research, regardless of when previous sessions occurred.
Can multiple users access the same SAM memory?
Yes. Since memory keys on projectId rather than user or session identifiers, all team members with project access share the same "memory" and "research_log" blocks. This enables collaborative SEO workflows where findings compound across contributors.
What happens if two SAM chats write simultaneously?
The Durable Object architecture in SamChatAgent.ts serializes execution—only one turn processes at a time per project. The upsert logic in setBlock ensures last-write-wins consistency without complex conflict resolution.
How large can memory blocks grow?
According to the repository implementation, memory blocks are text columns without explicit size limits in the application layer. In practice, extremely large blocks would impact LLM context window usage. The code suggests future iterations may implement summarization or chunking strategies for long-running projects.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →