Context pipeline¶
The write path: what happens between POST /api/memory and a stored row.
flowchart TB
IN["POST /api/memory"] --> V1
V1["1 · Pydantic validation<br/>lengths, ranges, required fields"] -->|"fail → 422"| V2
V2["2 · type ∈ six allowed values"] -->|"fail → 422"| P
P["3 · resolve project key<br/><b>creates it if absent</b>"] --> E
E["4 · embed(content)"] --> W
W["5 · INSERT row + embedding_json"] --> T
T["6 · emit CONTEXT_STORED telemetry<br/>hashed ids, sizes — never content"] --> OUT["200 + row"]
Everything is synchronous. When the response arrives, the memory is durable and immediately retrievable.
The steps¶
1 — Validation¶
Pydantic, at the API boundary:
| Field | Rule |
|---|---|
project_id, agent_id |
required, non-empty |
content |
1–8000 characters |
confidence, importance |
0.0 ≤ x ≤ 1.0 |
metadata |
any JSON object |
2 — Type check¶
Checked against the six allowed values in the handler, after Pydantic:
422, and no row is written. Nothing is coerced to fact.
3 — Project resolution¶
The write path uses the permissive resolver: an unknown project key is created, and the request succeeds.
flowchart LR
W["POST with project_id"] --> Q{"key exists?"}
Q -->|yes| U["use it"]
Q -->|no| C["CREATE, return 200"]
R["GET with project_id"] --> Q2{"key exists?"}
Q2 -->|yes| U2["use it"]
Q2 -->|no| E["404"]
That asymmetry is the source of the most common integration bug: a typo on the
write path returns 200 and creates a second, empty project. The later read on
the correct key returns nothing, with no error anywhere in the chain. Pin the
key in a constant.
4 — Embedding¶
Computed inline, on the request thread, from content only — not from
metadata, not from type, not from agent_id.
Cost depends on the provider: negligible for hashing, a MiniLM forward pass
for semantic. This is the write path's latency floor.
Every write persists an embedding. That was not always true — before
2026-08-14 no caller passed one and every row stored NULL, which meant every
retrieval recomputed everything. The default now lives inside the write
function, so every channel (REST, SDK, all 15 MCP tools) gets it without having
to remember.
5 — Insert¶
One row in contexts, embedding serialised as a JSON array in
embedding_json. No queue, no outbox, no deferred work.
6 — Telemetry¶
A CONTEXT_STORED event is emitted carrying:
| Recorded | Not recorded |
|---|---|
| SHA digests of project and agent ids | The content |
type |
The metadata values |
| content length in characters | Anything readable |
| an estimated token count |
Content is hashed, never logged. The event exists to make write volume measurable without making it readable.
The update path¶
flowchart TB
P["PATCH /api/context/{id}"] --> A{"content changed?"}
A -->|no| B["update metadata only<br/>embedding untouched"]
A -->|yes| C["recompute embedding"] --> D["UPDATE row + version entry"]
Recomputation on content change is load-bearing. Without it an edited memory keeps its old vector and ranks on text it no longer contains — a silent correctness failure with no symptom except worse retrieval. The stale-embedding case is explicitly handled in the update path for exactly this reason.
Ownership is checked before the update: a context belonging to another
organization returns 404, not 403, so the response cannot confirm that an
id exists.
Write-path characteristics¶
| Consistency | Strong — readable immediately after 200 |
| Durability | Whatever your database gives you; no application-level buffer |
| Idempotency | None. Two identical POSTs create two rows. |
| Deduplication | None. See Memory. |
| Batch writes | Not supported. One memory per request. |
| Delete | Not supported per memory. PATCH to supersede, or delete the org. |
| Throughput ceiling | One embedding computation per write |
If you write in bulk¶
There is no batch endpoint. For an import:
# ponytail: sequential, because the ceiling is the server's embedding cost,
# not your client's concurrency. Parallelise only if you measure otherwise.
for item in items:
requests.post(f"{BASE}/api/memory", headers=H, timeout=30, json={
"project_id": PROJECT, "agent_id": "importer",
"content": item["text"], "type": item["type"],
"importance": item["importance"],
}).raise_for_status()
Mind the production rate limit — 300 requests/minute for an authenticated
caller. A large import needs pacing, or a 429 handler
(Errors).
Related¶
- Memory · Persistence
- Retrieval pipeline — the read path
- Memory API