# How the SAM In-App Agent Uses Project Memory and Context in OpenSEO

> Discover how the SAM in-app agent leverages project memory and context in OpenSEO. Learn how persistent, cross-session knowledge enhances agent interactions.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-08-10

---

**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`](https://github.com/every-app/open-seo/blob/main/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.

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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:

1. **Load** the `"memory"` block via `SamProjectMemoryRepository.getBlock` to establish persisted context
2. **Execute** reasoning and search operations using the combined system prompt + memory
3. **Persist** new findings by updating `"research_log"` or `"memory"` via `setBlock`

This write-back pattern ensures that research progress, discovered entities, and analytical conclusions accumulate project-wide.

```typescript
// 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 ?? "";
}

```

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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_memory` table operations in [`SamProjectMemoryRepository.ts`](https://github.com/every-app/open-seo/blob/main/SamProjectMemoryRepository.ts)
- **getBlock/setBlock** provide simple read/write semantics keyed by `(projectId, label)`
- [`SamChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/SamChatAgent.ts) loads 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.ts`](https://github.com/every-app/open-seo/blob/main/samSystemPrompt.ts) combines 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`](https://github.com/every-app/open-seo/blob/main/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.