> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orcapods.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Memory Bank

> Per-profile long-lived memory: structured saves, multi-signal relevance ranking, and automatic prompt injection.

## What is the Memory Bank?

The **Memory Bank** is a profile-scoped store of long-lived knowledge that survives across every session of an agent. It captures preferences, facts, behaviors, and short-lived context the agent has decided to remember, and surfaces the most relevant entries back into the prompt on every run.

Two delivery channels:

* **Automatic injection** — every run for a profile that opts in to `@memory` gets a prefixed `--- CONTEXT FROM MEMORY ---` block built from the core tier plus query-relevant entries.
* **On-demand** — agents can call `memory_save`, `memory_recall`, `memory_list`, or `memory_delete` at any point during a run.

The bank is a port of the Brain Dump relevance model. No embeddings — keyword overlap plus a small set of domain-specific clusters bridges the lexical gap between query and memory.

***

## Storage Layout

Memories are persisted to the same S3-compatible bucket as artifacts, under a dedicated prefix:

```text theme={null}
memory/<profileName>/<memoryId>.json
```

`<memoryId>` is `mem-<8hex>`, matching Orca's other ID conventions (`run-`, `sess-`, `agent-`, `pool-`).

The in-memory index is the runtime source of truth; S3 is the persistence tier hydrated at startup via `LoadAll`. A successful save does not return until the object lands in S3, so a crash never loses an explicitly saved memory. When S3 is not configured the bank still operates — degraded to in-memory only.

***

## Memory Schema

```go theme={null}
type AgentMemory struct {
  ID               string    // "mem-<8hex>"
  ProfileName      string
  RawInput         string    // verbatim, audit trail
  ProcessedContent string    // LLM-normalized, retrieval-friendly
  Summary          string    // <50 chars, used in lists
  Category         string    // preference | fact | behavior | context | general
  Source           string    // explicit | inferred (v2)
  Confidence       float64   // [0, 1]
  CreatedAt        time.Time
  LastAccessedAt   time.Time
  AccessCount      int
  StalenessScore   float64
  IsActive         bool
  Version          int       // schema version
}
```

`RawInput` is capped at **4 KiB**. Larger inputs are rejected with a clear error.

### Categories

| Category     | Decay   | Retrieval role                             |
| ------------ | ------- | ------------------------------------------ |
| `preference` | Slowest | Eligible for the always-injected core tier |
| `fact`       | Slow    | Eligible for the core tier                 |
| `behavior`   | Normal  | Query-only                                 |
| `context`    | Fastest | Query-only — meant to age out quickly      |
| `general`    | Normal  | Query-only                                 |

The **core tier** is `preference` + `fact` entries with `confidence >= 0.8`, ordered by confidence then access count. Cached per profile for **5 minutes** to keep the run hot path off S3.

***

## Relevance Scoring

`QueryWithRelevance` ranks active memories by a composite score:

```text theme={null}
score = (wR * recency  +  wU * usage  +  wT * topic) * confidenceMul
```

| Signal     | Default weight | Behavior                                                                                                 |
| ---------- | -------------- | -------------------------------------------------------------------------------------------------------- |
| Recency    | `0.35`         | Exponential decay, **30-day half-life**, anchored at the more recent of `CreatedAt` and `LastAccessedAt` |
| Usage      | `0.30`         | Logarithmic ramp from a `0.6` baseline (never accessed) toward `1.0` (heavily accessed)                  |
| Topic      | `0.35`         | Tokenized word overlap (stopwords removed) plus semantic-cluster matching                                |
| Confidence | multiplier     | `0.5 + 0.5 * confidence` — low-confidence saves never dominate the ranking                               |

### Maturity

Until the bank has accumulated query history, recency and topic dominate. The **maturity factor** is `min(1, totalAccesses / 50)` — once a profile has crossed \~50 cumulative recalls, the usage weight reaches its full `0.30`, and the slack from the early-bank period is redistributed back to recency and topic.

### Semantic clusters

Eight built-in vocabulary groups bridge cases where a query and a memory share a topic but no literal words. A "diet" query still matches a "peanut allergy" memory through the `food` cluster. Clusters: `food`, `health`, `work`, `tech`, `travel`, `entertainment`, `communication`, `shopping`. A non-zero contribution requires the cluster to appear on both sides.

### Staleness

Each memory carries a `stalenessScore` in `[0, 1]` recomputed on every save and access:

```text theme={null}
staleness = (daysSinceAccess * 0.05  -  log10(accessCount+1) * 0.2  +  (1 - confidence) * 0.3) * categoryWeight
```

Category weights tilt the decay: `preference=0.5`, `fact=0.7`, `behavior=1.0`, `context=1.5`, `general=1.0`. The score is surfaced for inspection but does not currently gate retrieval (Brain Dump parity).

***

## Prompt Injection

Profiles that opt in to the `@memory` capability receive an automatic context block prepended to every run's prompt:

```text theme={null}
--- CONTEXT FROM MEMORY ---
Core knowledge (always apply):
* [preference] The agent prefers returning structured JSON whenever the caller supplies a schema.
* [fact] Plans are persisted as JSONL objects at plans/<planId>.jsonl in the artefact bucket.

Relevant to this task:
* [behavior] Before reading a file, the agent should call list_dir on the parent directory first.
---
<user prompt here>
```

Two soft caps shape the block:

| Cap              | Default                     | Notes                                                                     |
| ---------------- | --------------------------- | ------------------------------------------------------------------------- |
| Character budget | `2000` chars (\~500 tokens) | Query-relevant tier is trimmed first, core tier last                      |
| Query results    | `8` memories                | Pulled with `minScore = 0.05`, excluding any IDs already in the core tier |

After the block is rendered, the IDs that made it in are passed to `MarkAccessed` fire-and-forget — `accessCount` and `lastAccessedAt` update without blocking the run.

Profiles **without** `@memory` (or any of the four `memory_*` tools by name) never receive injection. Same opt-in shape as `@artifacts`.

***

## LLM Processor

When an agent saves a memory with only `rawInput` (no pre-structured `processedContent`), the bank consults a fast LLM to extract `processedContent`, `summary`, `category`, and `confidence`. The processor is a thin wrapper around Anthropic's Messages API with a **5-second deadline**.

Configuration:

| Variable                 | Default            | Notes                             |
| ------------------------ | ------------------ | --------------------------------- |
| `ANTHROPIC_API_KEY`      | —                  | Required to enable LLM extraction |
| `ANTHROPIC_MEMORY_MODEL` | `claude-haiku-4-5` | Override the extraction model     |

When the key is unset, or the call fails, or the response is malformed, the bank falls back to a deterministic preview: `processedContent = rawInput`, `summary` = first 50 chars, `category = general`, `confidence = 0.5`.

***

## Memory Tools

`@memory` is **opt-in** (not in `@default`). Every tool requires session context and operates against the session's profile.

| Tool            | Description                                                                                                                             |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `memory_save`   | Save a memory. Provide `rawInput` for free-text (LLM extracts structure) or `content`/`category`/`confidence` for pre-structured saves. |
| `memory_recall` | Search by topic. Returns ranked results with the relevance breakdown attached. Bumps `accessCount` for every returned memory.           |
| `memory_list`   | List newest-first with `limit` / `offset` paging. Read-only — does not affect access counts.                                            |
| `memory_delete` | Remove a memory by id.                                                                                                                  |

```json theme={null}
{
  "name": "researcher",
  "tools": ["@default", "@memory"]
}
```

Selecting `@memory` (or naming any `memory_*` tool individually) both registers the tools and enables prompt injection.

***

## REST API

All endpoints return `503 Service Unavailable` when the bank is not wired. Reads call a 2-second TTL `RefreshIfStale` so the dashboard sees runner-side saves promptly without paying for an S3 round-trip on every request.

### Per-profile

| Method   | Path                                   | Description                            |
| -------- | -------------------------------------- | -------------------------------------- |
| `GET`    | `/api/profiles/{name}/memories`        | List with `limit` / `offset`           |
| `POST`   | `/api/profiles/{name}/memories`        | Create — body is `MemoryCreateRequest` |
| `GET`    | `/api/profiles/{name}/memories/search` | Query with `q`, `limit`, `minScore`    |
| `GET`    | `/api/profiles/{name}/memories/{id}`   | Get one                                |
| `DELETE` | `/api/profiles/{name}/memories/{id}`   | Delete one                             |

### Global

| Method | Path                     | Description                                                                                       |
| ------ | ------------------------ | ------------------------------------------------------------------------------------------------- |
| `GET`  | `/api/memory-bank`       | All memories grouped by profile, or a flattened page with `limit` / `offset`, `q`, and `category` |
| `GET`  | `/api/memory-bank/stats` | Roll-up: total memories, profiles with memory, per-profile counts                                 |

See the [Conductor API reference](/api-reference/conductor#memory-bank) for request and response shapes.

***

## Configuration

The bank shares the artifact store's environment contract — see [Storage and Files](/concepts/storage). When `S3_BUCKET` is set it boots in fully persistent mode; otherwise it runs in-memory only and logs a warning at startup.

| Variable                 | Description                                |
| ------------------------ | ------------------------------------------ |
| `S3_BUCKET` and friends  | Persistence layer (shared with artifacts)  |
| `ANTHROPIC_API_KEY`      | Enables LLM extraction in `memory_save`    |
| `ANTHROPIC_MEMORY_MODEL` | Optional override for the extraction model |

Both the runner and the conductor build their own bank from these variables. The runner's bank is the source of truth on writes; the conductor's bank is read-through synced every 2 seconds for dashboard reads.

***

## Limits and Caveats

* **Per-profile scope only.** Memories are not shared across profiles or pools — by design. Use pools and the shared filesystem for cross-agent state.
* **Listing cap.** `LoadAll` walks at most 1000 entries per profile (the underlying `ListObjectsV2` page cap). Banks larger than that need pagination plumbed through `ListInput`.
* **No retention enforcement.** Staleness is computed but not acted on. Operators delete cold memories by hand or via the dashboard.
* **`MarkAccessed` is best-effort.** Access count and timestamp updates persist asynchronously; a runner crash within a few seconds of a recall may lose them.
* **Source `inferred` is reserved for v2.** All v1 saves carry `source: "explicit"`; future post-run extraction will populate `inferred`.
