# Agent Memory Integration

How the `@coda/memory` package integrates with the Coda server to provide cross-session learning.

## Session Lifecycle

Every streaming chat request follows this sequence:

1. **Load conversation history** (existing) -- Redis cache, DB fallback.
2. **Load memory** -- `memoryService.loadMemory(identityId)` runs consolidation (observations to facts), formats the prompt section, and extracts tool affinities. 500ms timeout; falls back to empty on failure.
3. **Format memory section** -- prepend to Langfuse-resolved system prompt (`memory.promptSection + "\n\n" + systemPrompt`).
4. **Extract tool affinities** -- `Map<string, number>` available for `searchCatalog()` calls during the agent loop.
5. **Call converseWithTools** -- enriched system prompt and tool affinities flow into the LLM orchestration loop.
6. **Emit observations (fire-and-forget)** -- `onToolExecution` callback emits tool success/failure observations. Query pattern observation is emitted at the start of the request.
7. **On shutdown** -- `memoryService.drain()` awaits in-flight observation appends before process exit.

## Integration Points

| Point             | File                                       | What happens                                                                                                    |
| ----------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Memory loading    | `apps/server/src/routes/stream-handler.ts` | `memoryService.loadMemory(identityId)` returns facts, promptSection, toolAffinities                             |
| Prompt injection  | `apps/server/src/routes/stream-handler.ts` | `memory.promptSection` prepended to `systemPrompt`                                                              |
| Query observation | `apps/server/src/routes/stream-handler.ts` | `emitQueryPatternObservation(userQuery)` emitted via `memoryService.emitObservation()`                          |
| Tool observations | `apps/server/src/routes/stream-handler.ts` | `onToolExecution` callback calls `emitToolObservations()` then emits each via `memoryService.emitObservation()` |
| Initialization    | `apps/server/src/server.ts`                | Creates backing stores + `RedisObservationLog`/`RedisFactRepository`/`AsyncMap` checkpoint + `MemoryService`    |
| Shutdown          | `apps/server/src/server.ts`                | `appLocals.memoryService?.drain()` in shutdown handler                                                          |
| App locals        | `apps/server/src/app-locals.ts`            | `memoryService?: MemoryService` on `AppLocals` interface                                                        |
| Tool affinity     | `apps/server/src/ai/tools/catalog.ts`      | `searchCatalog()` accepts `toolAffinities` parameter, applies `affinity * 2` boost                              |

## MemoryService (server-side wrapper)

`MemoryService` (in `apps/server/src/ai/memory/memory-service.ts`) wraps the `@coda/memory` package with server concerns:

- **`loadMemory(identityId)`** -- Runs `consolidate()` with a 100ms `Promise.race` timeout. On success, returns `{ facts, promptSection, toolAffinities }`. On any error (timeout, store failure), returns empty results.
- **`emitObservation(identityId, observation)`** -- Fire-and-forget: calls `ObservationLog.append()`, catches and swallows errors, tracks the promise in an `inflight` set.
- **`drain()`** -- Awaits all in-flight observation writes. Called during graceful shutdown.

### Observation Emitters

Three factory functions in `apps/server/src/ai/memory/observation-emitters.ts`:

| Function                                              | Input                                               | Output                                                                                      |
| ----------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `emitToolObservations(toolUses, toolResults, timing)` | Tool execution data from `onToolExecution` callback | `tool_success` / `tool_failure` observations (skips meta-tools like `search_tools`)         |
| `emitEntityObservations(toolName, resultData)`        | Tool name + parsed result                           | `entity_access` observations for get_account, search_snowflake_schema, get_products_by_isrc |
| `emitQueryPatternObservation(query)`                  | User query string                                   | Single `query_pattern` observation if domain pattern matches, else `null`                   |

### Prompt Formatter

`formatMemorySection(facts, budgetTokens?)` in `apps/server/src/ai/memory/prompt-formatter.ts`:

- Ranks facts by `confidence * reinforcementCount` (descending).
- Groups into display categories: Tool preferences, Frequent entities, Domain focus, Known issues.
- Assembles markdown under "What you know about this user" header, respecting a 300-token budget.
- Returns empty string if no facts pass the budget filter.

### Tool Affinity

`extractToolAffinities(facts)` in `apps/server/src/ai/memory/affinity.ts`:

- `tool_affinity` category produces positive scores (confidence).
- `tool_avoidance` category produces negative scores (-confidence).
- Consumed by `searchCatalog()` as a tiebreaker boost (max +/-2 points).

## Configuration

The `MemoryService` is created in `server.ts` and stored on `AppLocals.memoryService`:

```ts
// Current: in-memory backing stores
const memorySortedStore = new MemorySortedStore();
const memoryHashStore = new MemoryHashStore();
locals.memoryService = new MemoryService(
  new RedisObservationLog(memorySortedStore, "coda:memory:obs:"),
  new RedisFactRepository(memoryHashStore, "coda:memory:facts:"),
  new AsyncMap<string, number>(),
  new ExponentialDecay(),
);
```

**Current state**: Uses `MemorySortedStore` and `MemoryHashStore` from `@coda/common` plus `AsyncMap` from `@coda/common` -- in-memory implementations that provide the correct architecture without requiring Redis sorted set commands (`ZADD`, `ZRANGEBYSCORE`) that the existing `RedisLike` interface does not yet expose.

**TODO**: Wire real Redis sorted sets when `RedisLike` is extended with `zadd`/`zrangebyscore`.

## Langfuse Ordering

The memory injection must happen downstream of Langfuse prompt resolution:

```
getSystemPrompt()           // Langfuse-resolved system prompt
    │
    ▼
memory.promptSection        // Prepended if non-empty
    + "\n\n"
    + systemPrompt
    │
    ▼
converseWithTools(...)      // Receives enriched prompt
```

This ordering is critical because:

1. `getSystemPrompt()` may fetch the prompt from Langfuse (async, cached).
2. Memory injection adds user-specific context that Langfuse does not manage.
3. The combined prompt is what the LLM actually receives -- memory first, then domain instructions.
