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

# AI Hub memory page walkthrough: namespaces, entries, and tenant caps

> Use the AI hub's memory surface — the tenant-wide Customer Memory browser at /agents/memory — to audit, curate, and teach the notes your AI agents have stored; understand how memory namespaces are scoped, capped, and pruned, and how your agents query them at turn-time.

# AI Hub memory page walkthrough

The memory workspace is the part of the AI hub where you see, shape, and delete what your AI agents have learned about individual contacts. It is backed by a real public API — every operation the browser performs is also available as `GET / POST / PATCH / DELETE` against `/api/v1/agents/memory` — so you can automate everything the console does.

For the underlying model (the four memory types, the consent flag, the importance/eviction ranking) see [Agent memory model](/concepts/agent-memory-model). This page is the operator walkthrough.

## What the AI Hub entry point shows

The canonical AI workspace lives at `/agents`, and the legacy `/ai` URL 308s to `/agents`; the dashboard groups Agents, Studio, Marketplace, and Conversations tabs together with cards for Squads, Knowledge Base, Memory, Flows, and Verify. Any old bookmark to `/ai/memory` redirects permanently to `/agents/memory`, the tenant-wide memory browser.

The page is tenant-wide and cross-agent on purpose: it answers "what has my AI learned, across every agent, about every contact." That breadth is also why access is restricted to owner and admin roles — a member with contacts read access can already see the same notes one contact at a time.

## Set up: how namespaces are scoped

Memory namespaces are implicit, not provisioned. Every entry is stamped with three legs at write time, and the browser plus the runtime always filter on them together:

* `tenantId` — set by your API key / session and asserted again at the vector store boundary so one tenant can never resolve another tenant's points by guessing a UUID.
* `contactId` — the contact the note is about, always scoped within the tenant.
* `agentId` (optional but always stamped by the write endpoints) — narrows retrieval to notes one agent wrote, so a specialist agent does not bleed into a sibling's corpus.

There is no "Create namespace" step. Saving the first entry against a new `(tenantId, contactId)` pair creates the namespace implicitly through the write endpoints. Retention and caps are resolved per-tenant from your organisation's `agent_memory_caps` settings — and defaulted when unset — so a workspace-level override applies to every namespace you use. The defaults are:

| Setting | Default | Description |
| - | - | - |
| `itemsPerContact` | 100 | Total entries (any type) per contact before budget eviction |
| `factsPerContact` | 20 | Factual entries per contact (enforced at POST time) |
| `contentChars` | 10,240 | Max characters per entry body |
| `ttlDays` | 30 | Time-to-live; an `expiresAt` stamp is written at create time and a sweeper prunes lapsed entries |

Validate a new entry write against the scope your app will actually use:

```bash theme={null}
curl -X POST 'https://api.orbit.devotel.io/api/v1/agents/memory' \
  -H 'Authorization: Bearer dv_live_sk_xxxx' \
  -H 'Content-Type: application/json' \
  -d '{
    "contactId": "cnt_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
    "agentId": "agt_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
    "memoryType": "fact",
    "content": "Customer prefers SMS over email for booking confirmations"
  }'
```

A `201` returns the created `item` with its stamped namespace legs; a `409` either means the contact has opted out of memory (`MEMORY_DISABLED_FOR_CONTACT`) or the per-contact fact cap is reached (`MEMORY_FACTS_CAP_EXCEEDED`).

## CRUD on entries via the UI, with field-level examples

Open **AI Agents → Memory** (owner or admin only). The header offers **Create memory** and **Refresh**; the Filter card narrows the grid by Contact ID, Agent ID, entry type (fact / preference / goal / summary), and a creation-date range, debounced so a paste triggers one fetch. An inverted From/To range shows a warning banner instead of a misleading "no entries" result.

The grid loads 50 entries at a time; **Load more** advances the cursor. Click a card to open the detail dialog with every field — content, type, importance, contact, agent, timestamp, entry id.

Create via **Create memory** — fields map 1:1 to the POST payload:

1. **Contact ID** — paste `cnt_…` from the contact or conversation page.
2. **Agent** — pick from the dropdown; no need to copy `agt_…` from a URL.
3. **Type** — fact, preference, goal, or summary.
4. **Content** — up to 10,240 characters; the counter shows `length/10240`.

Created entries are stamped explicitly as `memoryType: "fact"` on the per-agent POST and respect your namespace legs; manual adds land at importance 0.9 by default. Update with **Boost** on an entry card (raises importance in place under the same entry id). Delete a single entry from the card, or erase a whole contact through the GDPR bulk-erase panel that appears once you filter to a single Contact ID — it deletes every entry for that contact and flips their `memory_enabled` consent flag off.

## Query memory at runtime from an agent

At turn-time your agent calls `retrieveContext` (in `@devotel/agents`) with the user utterance embedded as the query. Scope the search to one conversation by passing the unified-conversation id — this is the per-conversation snapshot pattern that keeps a contact's two parallel agent sessions from leaking into each other:

```typescript theme={null}
import { retrieveContext } from '@devotel/agents';

const memory = await retrieveContext({
  tenantId: req.ctx.tenant.tenantId,
  contactId: conversation.contactId,
  query: latestUserUtterance,
  limit: 5,
  agentId: agent.id,
  unifiedConversationId: conversation.unifiedConversationId,
});
```

The retrieved items are prepended to the system prompt as one `MEMORY_CONTEXT` block; nothing else changes about your loop. Without the conversation-or-agent narrow, retrieval is org‑wide within the tenant+contact pair, which is the correct default for a generalist and wrong for a specialist — scope deliberately.

## Naming hygiene, expiration, and per-agent rules

* **Types decide what stays.** Facts, preferences, and goals rank higher than summaries, and manual (operator) entries rank higher still under eviction — use the right type.
* **Sentence-shaped facts.** `MEMORY_MAX_CONTENT_CHARS` (10,240 by default) clips long extracts; one statement per entry is retrievable, one paragraph usually is not.
* **Expiry is written at create time.** `expiresAt = createdAt + ttlDays` lets the TTL sweeper prune cleanly; raising your tenant later `ttlDays` applies forward, not back.
* **Per-agent scope is a filter, not a partition.** The store is one shared per-contact corpus; `agentId` narrows retrieval. When two of your agents should never share notes, give them different contacts — do not rely on scope.

## Screenshot walkthrough

The page reads filter-bar → grid → single-detail dialog.

1. **Filters** — Contact ID, Agent ID, Type, From, To. Invalid From > To shows an inline banner and the range is ignored.
2. **Grid** — cards carry content, type, importance, and the entry id; boost, inspect, or delete from a card.
3. **GDPR erase** — appears after you scope one Contact ID; single-confirmation, bulk erase plus consent flip, audit-logged.
4. **Detail dialog** — complete entry and the created timestamp.

## Troubleshooting

**Writes rejected with a 4xx**

| Code | Meaning | Action |
| - | - | - |
| `MEMORY_DISABLED_FOR_CONTACT` (409) | Contact opted out of memory | Respect the opt-out; re-enable from the contact record only with their consent |
| `MEMORY_FACTS_CAP_EXCEEDED` (409) | Per-contact fact cap reached | Forget one before teaching another — never silently evicted |
| `VALIDATION_ERROR` (422) | Body shape wrong | Check `contactId`, `agentId`, `memoryType`, `content` are present and typed |

**Agent cannot see an entry it "should" see**

An entry written with `embeddingPending: true` (vector stored as zeros when the embedding gateway was momentarily down) is visible in the browser but scores \~0 semantically until the re-embed backfill runs. Entries the contact's consent flag excludes are also intentionally invisible — check `GET /api/v1/agents/memory?contactId=cnt_…` for the `memory_enabled: false` response.

**Quota on a namespace**

Caps are per-`(tenant, contact)` and per-tenant overall: bump `organizations.settings.agent_memory_caps` for the workspace, or delete stale entries from the console. The 5-minute caps cache means an org-settings change takes up to five minutes to apply.

**No entries at all yet**

Expect this the moment after onboarding: memory is written when an AI agent actually chats with a contact, or when you teach it a fact manually. Filter-less empty list plus no `next_cursor` means the tenant genuinely has no entries.
