CortexDB API Reference v1
Status: Draft for review
Last updated: 2026-05-14
Owner: pmalik
Scope: the precise external HTTP / SDK contract for CortexDB. This is the reference manual.
Companion: API_DESIGN_V1.md is the narrative walkthrough — read that first if you are new to CortexDB. This document is for implementers and SDK authors who need exact shapes, capability mappings, and error codes.
Normative source: the JSON schemas in docs/schemas/ are the authoritative contract. Where this document disagrees with a schema, the schema wins. Both documents are explanatory views into the schemas.
Predecessors: docs/ARCHITECTURE_REMEMBER_INDEX_RECALL.md, docs/storage-engine-architecture.md, docs/investor_deck/cortexdb_investor_deck_explainer.md.
Constraint: there are no production users on a prior API. We may break freely; we are not designing v2-compatible.
Table of Contents
- Glossary
- Executive Summary
- Design Philosophy
- Core Concepts
- Authentication & Identity
- Authorization Framework
- HTTP Conventions
- Layer Reference
- Endpoint Reference
- The Stratified Pack
- Streaming
- Forget Semantics
- Temporal Model
- Lifecycle Events Catalog
- Audit Log Schema
- Importer Mappings
- Error Catalog
- Compatibility Matrix
- Deferred Decisions
- Implementation Order
- Appendices
0. Glossary
| Term | Definition |
|---|---|
| Actor | The principal performing an operation. Typed: user:, agent:, service:, system:. |
| Bi-temporal | Records carry two independent time axes: when the fact was true in the world (event time) and when CortexDB learned of it (ingest time). |
| Capability | A discrete, named permission (e.g., forget.gdpr). Granted/denied by the four-tier policy stack. |
| Capture | The synchronous write of an event to the WAL. The only synchronous step in the write path. |
| Consolidate | The async stage where facts merge, beliefs revise, and understanding nodes synthesize. |
| Derived layer | Any layer whose contents are computed from events. Facts, Beliefs, Episodes, Understanding are derived; Events are not. |
| Experience envelope | The structured input payload to POST /v1/experience. Replaces messages in peer APIs. |
| Holistic view | A recall that traverses upward through the scope hierarchy. |
| Layer | One of the five memory tiers: Events, Episodes, Facts, Beliefs, Understanding. |
| Memory | A polymorphic noun used in marketing. Internally we say "record at layer X." Avoid in API design. |
| Pack | The stratified output of POST /v1/recall: layers + context block + provenance + diagnostics. |
| Refcount | The number of cross-scope references pointing at an event. Decisive for GDPR. |
| Scope | A /-delimited path of type:id segments that names a memory partition. Workspaces are scopes too. |
| Stratified pack | See Pack. |
| Token | A signed bearer credential carrying actor identity and a capability set. |
| WAL | Write-Ahead Log. RocksDB-backed. The append-only source of truth. |
1. Executive Summary
CortexDB exposes a layer-aware, scope-unified, bi-temporal memory API for AI agents. It differs from peer systems on nine points:
- Five addressable memory layers, not one flat memory pool. Clients can read Beliefs, Facts, Episodes, Events, and Understanding separately or as a stratified pack.
- Unified scope paths that subsume "workspaces" — there is one namespace primitive, hierarchical with opt-in registration and ACLs.
- Four-tier authorization (deployment → tenant → scope → actor) with capability-level attribution on every denial.
- Signed-token identity mandatory on every call; tokens can only narrow what policy already allows.
- Async-first writes with an SSE lifecycle stream; sync available via
waitparameter. - Bi-temporal everywhere, with
as_of,valid_during,recorded_during, and natural-language temporal qualifiers across all layer reads. - Reference-counted GDPR erasure — events are deleted when refcount drops to zero, otherwise PII is redacted while preserving cross-scope referential integrity.
- Streaming recall in both granular (per-layer-as-ready) and narrative (token-by-token) modes.
- Native importers for Mem0, Zep, Letta, OpenAI memory exports, and generic JSONL — in both
strict_temporalandbatch_throughputordering modes.
The public surface comprises roughly 30 endpoints under /v1/ plus an SSE channel and a token verifier protocol.
2. Design Philosophy
2.1 Five non-negotiables
- Events are immutable. The WAL is the source of truth. Every derived layer is reproducible by replay. There is no API that mutates a stored event payload.
- Bi-temporal at the boundary. The API accepts and returns both
valid_*andrecorded_*timestamps on every layer record. Single-time-axis models are not interoperable with us. - Async writes are normal. Captures return immediately. Clients that need read-after-write must opt in via
wait. - Policy is observable. Every denial cites the tier and the specific capability. No opaque 403s.
- Forget means forget, but proportionally. Default forget wipes derived layers and keeps events. GDPR forget is reference-counted: shared evidence survives without authorship.
2.2 What we explicitly reject
- Soft delete. No tombstones in the user-visible API. Records are either present or gone.
- A single flat memory bag. Mem0's pattern; we will not adopt it.
- Implicit tenancy. Every call carries a token whose
subandaudare explicit. - Opaque ranking. Recall responses include the plan, the filters applied, and the scoring contribution from each signal.
- Backwards-compat shims. There is no v0; there will be no v0-to-v1 adapter.
2.3 Inspirations and what we did differently
| Source | Borrowed | Did differently |
|---|---|---|
| Mem0 | Async writes; reference_date temporal anchor | Kept a graph; added layers; added bi-temporal; added GDPR refcount |
| Zep | Bi-temporal facts; synthesized context block | Extended bi-temporal to all layers; stratified pack with layers + context; added Belief layer |
| Letta | Shared blocks across agents | Scope = ACL primitive; no separate "blocks" concept |
| Cognee | ECL pipeline; partial+honest contract | Replaced ECL with our five-stage cycle; ECL bleeds implementation; ours bleeds purpose |
| LangMem | Tuple namespaces with template variables | String paths (operator-readable); hierarchical inheritance |
| Du et al. (arXiv:2505.00675) | Six atomic operations as observable | Surfaced as SSE lifecycle events, not separate verbs |
| Generative Agents | Citation pointers as explainability | Required in every recall response; the provenance block |
2.4 Stability Tiers
The full conceptual surface ships at v1. To remove ambiguity for implementers and SDK consumers, every endpoint and capability is assigned a stability tier. The tier governs change semantics, not feature completeness:
| Tier | Change policy | Response header |
|---|---|---|
| Stable | Additive only. Breaking changes require a major version bump (none planned pre-1.0). Capability defaults frozen in deployment presets. SDKs commit to these. | X-Cortex-Stability: stable |
| Beta | May break with one-release notice. Shapes tracked but not yet pinned. Capability defaults may shift between minor releases. | X-Cortex-Stability: beta |
| Experimental | May break or be removed without notice. Behind CORTEX_ENABLE_EXPERIMENTAL=1 in cloud presets (always on for on_prem_enterprise and dev_local). Documented and shipped — but not load-bearing for early adopters. | X-Cortex-Stability: experimental |
2.4.1 Stable surface (SDK-committed)
| Endpoint family | Notes |
|---|---|
POST /v1/experience (incl. /bulk) | Core write |
POST /v1/recall (non-streaming + granular streaming) | Core read |
POST /v1/answer (non-streaming) | Recall + LLM, with full provenance contract |
POST /v1/forget (cascades: derived_only, redact_events) | Non-GDPR forget |
GET /v1/events, /v1/episodes, /v1/facts, /v1/facts/timeline | Layer reads |
GET /v1/scopes, POST /v1/scopes, PUT /v1/scopes/members, DELETE /v1/scopes | Scope management (query-param addressed; see §8.8) |
GET /v1/lifecycle/stream (SSE), GET /v1/lifecycle/event/{lce_id}, GET /v1/lifecycle | Lifecycle |
GET /v1/policy/effective, GET /v1/policy/deployment | Policy introspection (read) |
GET /v1/audit, GET /v1/audit/{audit_id}, POST /v1/audit/verify | Audit |
POST /v1/blobs, GET /v1/blobs/{blob_id} | Blobs |
POST /v1/auth/revoke | Token revocation |
GET /v1/admin/health, GET /v1/admin/metrics, GET /v1/admin/version | Admin introspection |
2.4.2 Beta surface (shape solid, semantics maturing)
| Endpoint family | Maturity notes |
|---|---|
GET /v1/beliefs, GET /v1/beliefs/why | Confidence/CI math may shift; stance enum may extend |
GET /v1/understanding, GET /v1/understanding/{id}, GET /v1/understanding/{id}/related, GET /v1/understanding/coverage | Returns _partial: true until full synthesizer ships |
POST /v1/import/* (mem0, zep, letta, openai, jsonl) | Mapping tables evolve with source-system versions |
POST /v1/export, GET /v1/export/{export_id} | Export formats track import |
POST /v1/vocabularies, GET /v1/vocabularies/{name}, PUT /v1/vocabularies/{name} | Vocabulary surface |
POST /v1/temporal/phrases, GET /v1/temporal/phrases | Custom temporal phrase registration |
2.4.3 Experimental surface (documented, ship-with-flag)
Gated by CORTEX_ENABLE_EXPERIMENTAL=1 outside on_prem_enterprise and dev_local. Calls return X-Cortex-Stability: experimental plus X-Cortex-Experimental-Notice with the migration link when the surface settles.
| Surface | Why experimental |
|---|---|
POST /v1/erasures (reference-counted GDPR erasure), POST /v1/erasures/preview, GET /v1/erasures/{erasure_id}, POST /v1/erasures/{erasure_id}/cancel | Refcount algorithm needs hardening + legal-review feedback |
POST /v1/recall with view=narrative; POST /v1/answer with stream=true (token stream) | Narrative streaming — token-level UX and shape may evolve |
POST /v1/understanding/synthesize | Synthesizer not yet stable |
forget.gdpr.cross_workspace capability and propagation | Co-owned workspace propagation rules likely to refine |
PUT /v1/policy/deployment, PUT /v1/policy/tenant/{id}, PUT /v1/policy/scope, PUT /v1/policy/actor/{id} | Policy mutation surface — read endpoints remain Stable |
POST /v1/scopes/prune | Cleanup automation |
POST /v1/admin/flush-views, POST /v1/admin/compact | Operator surface beyond introspection |
2.4.4 Per-response signal
Every response carries X-Cortex-Stability with the tier of the endpoint hit. SDKs surface a one-time deprecation warning when calling non-Stable endpoints unless the caller has explicitly opted in via client.allow_unstable(true) or equivalent.
2.4.5 Tier promotion criteria
| From | To | Gate |
|---|---|---|
| Experimental → Beta | Two independent customer integrations exercising the surface for ≥30 days without semantic complaints. | |
| Beta → Stable | One full minor-release cycle with no breaking-change PRs touching the endpoint, plus an SDK type freeze. | |
| Stable → (next major) | Explicit X-Cortex-Deprecation header set for ≥90 days before removal. |
3. Core Concepts
3.1 The Five Memory Layers
| Layer | Purpose | Atomic unit | Mutability | Time semantics |
|---|---|---|---|---|
| Events | Lossless capture of experience | A WAL entry | Immutable | observed_at (event time) + recorded_at (ingest time) |
| Episodes | Bounded spans of related events | A causal chain with start/end | Sealed once consolidated | started_at / ended_at; bi-temporal at boundaries |
| Facts | Triple-shaped atemporal/temporal assertions | (subject, predicate, object, validity) | Supersedable | Full bi-temporal: (valid_from, valid_to, recorded_from, recorded_to) |
| Beliefs | Probabilistic claims about state of the world | A claim with confidence + supports | Continuously revisable | Bi-temporal + last_revised_at |
| Understanding | Synthesized conceptual model | A concept node + edges | Versioned | version + valid_from; revisions chained |
3.1.1 Why these five and not three
- Mem0 has "facts" only — they cannot distinguish "what I observed" (Events) from "what I concluded" (Beliefs), so they cannot answer "why do you think that?"
- Zep has facts + episodes — they collapse Belief into Fact, losing confidence as a first-class field.
- We separate lossless capture (Events), bounded recall (Episodes), structured assertion (Facts), probabilistic conclusion (Beliefs), and conceptual synthesis (Understanding). This is the minimum set that lets us answer every recall mode the deck promises.
3.1.2 Layer derivation graph
(synthesize)
Events ──(window)──▶ Episodes ──(extract)──▶ Facts ──(rank+revise)──▶ Beliefs
│ │ │
└──────────────────(group + abstract)────────────┴──────────────────────────┴──▶ Understanding
Every derived record carries a supports: [event_id, ...] field. Walking this is the "why" trail.
3.2 The Six Atomic Operations
Adopted from Du et al. (arXiv:2505.00675). They are not separate endpoints — they are observable lifecycle events that fire as the async view builder works.
| Operation | Fires when | Visible via |
|---|---|---|
| Capture | WAL append succeeds | event: captured on SSE; wait=captured returns |
| Index | Event indexed in BM25 + HNSW | event: indexed; wait=indexed returns |
| Update | Fact ADD/UPDATE/NOOP decision | event: extracted and event: consolidated |
| Consolidate | Beliefs revised; Understanding nodes touched | event: consolidated; wait=consolidated returns |
| Forget | Records deleted | event: forgotten |
| Compress | Episodes sealed; Understanding versions bumped | event: compressed |
Note: Retrieve is not in this list — recall is request/response, not a lifecycle event.
3.3 Bi-temporal Model
Every record (except Events, which carry only the two timestamps below as singletons) has four time fields:
| Field | Meaning |
|---|---|
valid_from | First moment in the world when the claim is true |
valid_to | First moment when the claim ceases to be true (exclusive). null means open-ended. |
recorded_from | First moment CortexDB learned the claim |
recorded_to | First moment CortexDB superseded its own record (exclusive). null means current. |
Events have observed_at and recorded_at as singletons — events are atomic in time and never superseded.
Why both axes: the question "what did the system believe about Acme on April 15 when looked at from May 1?" requires recorded_from ≤ May 1 AND valid_from ≤ Apr 15 < valid_to. Single-axis systems can only answer one or the other.
as_of queries (see §12.3) pin both axes. By default as_of pins recorded_* to now and valid_* to the query time.
3.4 Scope Paths
A scope is a /-delimited path of type:id segments. The path is the namespace. There is no separate "workspace" concept.
3.4.1 Grammar
scope := segment ("/" segment)*
segment := type ":" id
type := lowercase identifier, [a-z][a-z0-9_]*
id := URL-safe identifier, [A-Za-z0-9_\-]+
Maximum 32 segments. Maximum 4096 characters total.
Reserved types (the system attaches special semantics):
| Type | Semantics |
|---|---|
org | Top-level tenant |
dept | Department / business unit |
team | Smaller cross-functional unit |
app | Application instance |
user | End-user owner |
agent | Agent identity |
service | Service account |
ws | Workspace (collaborative, non-hierarchical conventionally) |
project | Time-bounded initiative |
global | Cross-tenant pool (gated; see §5) |
system | Reserved for internal CortexDB scopes |
Any other type is user-defined and treated as opaque.
3.4.2 Hierarchy and inheritance
Path order is hierarchical: ancestors precede descendants. A read with view=holistic traverses upward; view=descend traverses downward (admin only).
view=local means exactly this scope — no traversal. This is the default.
3.4.3 Workspaces as scopes
A scope like ws:acme-q3-launch is structurally identical to org:acme/user:alice. It differs only in:
- Convention: workspace scopes are typically not nested under user/org trees.
- Membership: workspaces usually have multiple members, declared via
POST /v1/scopes. - Policy: workspaces typically register a consolidation policy.
There is no API distinction. A scope is a scope.
3.4.4 Registration
A scope path exists as soon as anything is written to it. Registration is optional and grants:
- ACL enforcement (writers/readers/owners)
- Audit logging at scope granularity
- Custom consolidation policy
- Inheritance directives (which ancestors to read up to)
Unregistered scopes are the "scratchpad" tier — any caller with the deployment-level write capability can use them. First write auto-registers with members=[caller] and role=owner (see §8.8.1 for the auto-register schema).
3.5 The Experience Envelope
The input to POST /v1/experience. It generalizes over conversation messages, tool calls, document ingest, observations, and feedback.
3.5.1 The three identities
The envelope carries up to three distinct identities. Collapsing them is the most common API design error in this space; we keep them explicit:
| Identity | Source | Meaning |
|---|---|---|
caller | Token sub, verified by signature | The authenticated principal making the API call. Always present. Used for policy, audit, and rate-limit attribution. |
observed_actor | Envelope observed_actor field | Who performed the experience being recorded. May differ from caller (delegated agent, import job, admin write). Defaults to caller. |
subject | Envelope subject field | Who or what the memory is about. May differ from both (a coach observing two players; an importer ingesting customer messages). Defaults to observed_actor. |
Examples of when these diverge:
- A user wires their agent to ingest Slack messages.
caller = agent:ingester,observed_actor = user:alice(the message author),subject = user:alice. - An admin writes a correction.
caller = user:admin_bob,observed_actor = user:admin_bob,subject = user:alice(whose memory is being corrected). - An import job replays Mem0 exports.
caller = service:importer_v1,observed_actor = user:alice(preserved from source),subject = user:alice.
3.5.2 Time fields
Two timestamps, with distinct ownership:
observed_at(client-supplied, required) — the moment in the world the experience occurred. Always client-side. This is event-time.recorded_at(server-assigned, optional on input) — when CortexDB ingested it. Server overrides any client-supplied value with its own wall clock. Clients in import flows may passsource_recorded_atto preserve the upstream system's ingest timestamp; server stores it alongside its ownrecorded_at.
The previous spec used happened_at and required clients to supply recorded_at. Both are now retired. Use observed_at (event time) and let the server own recorded_at (ingest time).
3.5.3 Envelope shape
{
"scope": "org:acme/dept:eng/user:alice",
"observed_actor": {
"id": "user:alice",
"type": "user",
"session": "run_abc"
},
"subject": {
"id": "user:alice",
"type": "user"
},
"modality": "conversation",
"content": {
"kind": "message",
"role": "user",
"text": "Acme moved to 200 seats and signed by 3:42pm",
"media": []
},
"context": {
"observed_at": "2026-05-13T15:42:00Z",
"source_recorded_at": null,
"location": null,
"preceded_by": ["evt_01H..."],
"intent": "deal_close",
"labels": ["sales", "q3-launch"]
},
"directives": {
"extract": ["facts", "entities", "beliefs"],
"consolidate_into": "ws:acme-q3-launch",
"confidence_floor": 0.5,
"ttl_for_belief_layer": "P30D",
"embed": "lazy"
},
"idempotency_key": "alice-msg-1747147320-001"
}
| Field | Required | Type | Notes |
|---|---|---|---|
scope | yes | scope path | Must pass policy for scope.write |
observed_actor | no | object | Defaults to the token's sub. When set, must be authorized by scope.write.on_behalf_of capability if it differs from caller. |
observed_actor.session | no | string | Used for run/session grouping |
subject | no | object | Defaults to observed_actor. Cross-subject writes require scope.write.about_other capability. |
modality | yes | enum | One of conversation, document, tool_result, observation, feedback, imported |
content.kind | yes | enum | One of message, text, json, blob_ref, triple |
content.text | conditional | string | Required for message and text |
content.role | conditional | enum | Required for message: user | assistant | tool | system |
content.media | no | array | Blob refs (uploaded via /v1/blobs) for multimodal |
context.observed_at | yes | RFC3339 | Event time (client-supplied) |
context.source_recorded_at | no | RFC3339 | Upstream ingest timestamp (imports only); server preserves alongside its own recorded_at |
context.preceded_by | no | array | Causality hints (memory event IDs evt_...) |
directives.extract | no | array | Override default extraction stages |
directives.consolidate_into | no | scope | Trigger consolidation into another scope; requires scope.write on that scope as well |
idempotency_key | yes | string | Client-generated; 64 chars max; replays return same event_id |
The server adds the following to the stored event:
| Server-assigned field | Source |
|---|---|
id | UUID v7, prefixed evt_ |
caller | From token sub |
recorded_at | Server wall clock at WAL append |
wal_offset | Monotonic position |
tenant_id | From token aud |
Modalities are open-set; unknown values are accepted with a 202 and stored verbatim, but only the listed values trigger structured extraction.
3.5.4 Multimodal content in v1
content.media[] accepts blob references for multimodal payloads, but v1 ships text-only embeddings (§18.2 item 18). The semantic vector space is computed from content.text only. Blob references are indexed by metadata (content-type, size, perceptual hash for images) and reachable via blob_id — but not embedded into the recall vector space. Multimodal embedding is a v1.x add-on; the envelope shape is forward-compatible for that addition (content.media[] already accepts additional indexing hints when they arrive).
3.6 ID Namespaces
Different resources use different ID prefixes to make every identifier self-describing in logs, audit trails, and client code. Mixing namespaces is an error — evt_ IDs are not interchangeable with lce_ IDs.
| Prefix | Resource | Format | Notes |
|---|---|---|---|
evt_ | Memory event (WAL entry) | UUID v7 | Time-ordered, monotonic per node |
ep_ | Episode | UUID v7 | |
fact_ | Fact | UUID v7 | |
belief_ | Belief | UUID v7 | |
concept_ | Understanding concept | UUID v7 | Stable across versions |
lce_ | Lifecycle event (SSE event) | UUID v7 | Distinct from memory events; SSE resume uses these |
job_ | Generic async job | UUID v7 | Catch-all for jobs without a typed prefix |
batch_ | Bulk write batch | UUID v7 | |
imp_ | Import job | UUID v7 | |
exp_ | Export job | UUID v7 | |
ervw_ | Erasure preview | UUID v7 | |
erasure_ | Erasure execution | UUID v7 | |
pack_ | Recall pack | UUID v7 | 60s TTL; referenceable by /v1/answer |
audit_ | Audit log row | UUID v7 | |
blob_ | Uploaded blob | UUID v7 | |
ent_ | Resolved entity | UUID v7 | Tenant-scoped |
concept_alias_ | Concept alias mapping | UUID v7 | |
req_ | Request correlation ID | UUID v7 | Echoed in X-Cortex-Request-ID |
tok_ | Token revocation list entry | UUID v7 | Internal |
Lifecycle event IDs. SSE event resume uses since_lifecycle_id=lce_..., NOT memory event IDs. A memory event (evt_) progresses through multiple lifecycle events (lce_) as it moves through Capture → Extract → Index → Consolidate → Compress. Older drafts used since=evt_... for SSE resume; this was ambiguous and is retired.
4. Authentication & Identity
4.1 Signed Tokens
Every request requires a signed bearer token. CortexDB issues PASETO v4 public (asymmetric, Ed25519-signed) via the reference issuer cortex-auth-ref. CortexDB accepts both PASETO v4 public and JWT (RS256, ES256) — the JWT acceptance path exists so customers can integrate their existing OIDC identity providers (Okta, Auth0, Keycloak, Azure AD) without minting separate CortexDB-specific tokens.
| Form | When | Notes |
|---|---|---|
| PASETO v4 public | Issued by cortex-auth-ref (the reference issuer we ship). Used for self-hosted, internal service accounts, dev environments, the benchmark harness. | Versioned protocol, no algorithm negotiation, signed-but-not-encrypted. The safer default. |
| JWT (RS256, ES256) | Issued by an external OIDC provider on the deployment's iss allowlist. Used by enterprise integrations. | Asymmetric only — HS256 and other symmetric algorithms are rejected to avoid algorithm confusion. |
| JWT (HS256, "none", etc.) | Never. | Explicitly disallowed. |
The verifier inspects the token's wire format and routes to the appropriate validator. Both formats share the same claim schema (token_claims.schema.json).
Authorization: Bearer <token>
X-Cortex-Actor: agent:planner_v3
The X-Cortex-Actor header is an explicit assertion of actor identity, required to match the token's sub claim. Mismatch → 401 actor_mismatch. The header is required because some clients send tokens that contain multiple identities (e.g., admin-impersonating); we force one identity per call.
Rust library choices (recorded here so engineers do not re-litigate): PASETO uses pasetors; JWT uses jsonwebtoken with the RS256/ES256 algorithms only.
4.2 Claims
| Claim | Required | Meaning |
|---|---|---|
iss | yes | The issuer of the token. Server has a configured allowlist. |
sub | yes | The actor identity, in our type:id form. |
aud | yes | The tenant binding, e.g. cortexdb:tenant:acme. |
exp | yes | Expiry as Unix timestamp. Max 24h from issue. |
iat | yes | Issue time. |
jti | yes | Unique token ID (for revocation lists). |
deployment | no | Optional hint; ignored if mismatched. |
caps | no | Capability narrowing (see §5.2). Token can only restrict, not expand. |
scopes | no | Path-prefixed capability scoping (e.g., scope.write:org:acme/*). |
4.3 Validation order
- Signature verification (against issuer's public key)
issin deployment allowlistaudmatches deployment's tenant bindingexp> now,iat≤ now + 60sjtinot on revocation listX-Cortex-Actorequalssub- Token
capsintersected with policy stack to produce effective capabilities - The specific capability for this endpoint must be in the effective set
Any failure ⇒ 401 with body {error, hint, retriable: false}.
4.4 Service accounts
For server-to-server calls (importer workers, scheduled jobs), sub is of form service:<name> and tokens may have longer exp (up to 30d). Service tokens cannot perform forget.gdpr (must come from a user-authenticated session).
4.5 Revocation
POST /v1/auth/revoke {jti} (operator-only) adds a jti to the revocation list. Lists are TTL-keyed by exp and auto-pruned. Maximum list size 1M; tenants with high revocation volume should issue shorter-lived tokens.
4.6 Token issuer
The core cortexdb binary does NOT include a token issuer — it only verifies. Operators integrate one of:
- Their existing OIDC provider (Okta, Auth0, Keycloak, Azure AD, custom). The provider's JWKS endpoint is registered in deployment policy; tokens it issues are accepted automatically. This is the recommended path for any enterprise that already runs an IdP.
cortex-auth-ref, our reference issuer, which mints PASETO v4 public tokens. Backed by Turso (cloud) or SQLite (self-hosted). Ships as a separate binary alongsidecortexdb. Recommended for self-hosted deployments without an existing IdP, for service accounts (importer workers, benchmark harness, scheduled jobs), and for the dev environment.
The two are not mutually exclusive — a deployment can register one or more JWT issuers AND run cortex-auth-ref for service accounts in parallel. The four-tier policy treats them identically once verified.
5. Authorization Framework
5.1 The Four-Tier Stack
Deployment policy ← operator, at server boot
↓ narrows
Tenant policy ← tenant admin
↓ narrows
Scope policy ← scope owner
↓ narrows
Actor policy ← scope owner or self
A capability is allowed iff every tier allows it. A tier's silence on a capability defaults to its parent's value (inherit-up).
5.1.1 Monotonicity rule
No tier can grant a capability its parent denies. Token caps, when present, are further narrowed against the effective tier stack — they cannot expand.
5.1.2 Decision attribution
Every 403 must cite the tier that denied:
{
"error": "policy_denied",
"capability": "scope.create.global",
"denied_by_tier": "deployment",
"deployment_preset": "cloud_shared_saas",
"remediation": "global scopes are not available on cloud-shared deployments"
}
5.2 Capability Catalog
Capabilities are dot-delimited and form a shallow hierarchy. Wildcards allowed in policy expressions (scope.read.*) but not in token caps.
| Capability | Gates |
|---|---|
scope.create.global | Registering a global:* scope |
scope.create.cross_tenant | Registering a scope path that crosses tenant boundaries |
scope.write | Writing to any scope (default-allow at deployment) |
scope.write.elevated | Writing to a scope above the actor's natural level (e.g., user writing to dept). "Natural level" = the deepest scope path where the actor is an explicit owner or writer member. |
scope.write.on_behalf_of | Setting observed_actor ≠ caller in the envelope (delegated writes, importer flows) |
scope.write.about_other | Setting subject ≠ observed_actor (admin corrections, coach-observes-player scenarios) |
scope.create.custom | Registering a scope whose type segment is not in the deployment's allowed_scope_types builtin set |
scope.read.local | Reading exactly the named scope (default-allow) |
scope.read.holistic | Walking up the hierarchy on read |
scope.read.descend | Walking down (admin) |
scope.read.cross_tenant | Crossing tenant boundaries on read |
understanding.read | Reading concept nodes |
understanding.read.cross_scope | Pulling concepts across sibling scopes |
understanding.synthesize | Forcing consolidation (admin/debug) |
forget.cascade.derived_only | Default forget mode |
forget.cascade.redact_events | Redact mode |
forget.gdpr | True erasure including events when refcount = 0 |
forget.gdpr.cross_workspace | GDPR propagation into shared workspaces |
import.from.mem0 | Run the Mem0 importer |
import.from.zep | Run the Zep importer |
import.from.letta | Run the Letta importer |
import.from.openai | Run the OpenAI memory importer |
import.from.jsonl | Run the generic JSONL importer |
export.format.mem0 | Export to Mem0 format |
export.format.zep | Export to Zep format |
export.format.jsonl | Export to JSONL |
lifecycle.subscribe | Open an SSE lifecycle stream |
llm.invoke | Trigger an LLM call (used by /v1/answer, narrative views) |
diagnostics.read | Include diagnostics: summary or diagnostics: full in recall/answer responses (otherwise diagnostics are stripped) |
audit.read | Query the audit log |
audit.read.cross_actor | Query audit log for actors other than self |
auth.revoke | Revoke tokens (operator or self) |
temporal.phrases.read | List custom temporal phrases |
temporal.phrases.write | Register/delete custom temporal phrases |
vocabulary.read | List vocabularies |
vocabulary.write | Register/edit vocabularies |
policy.administer.deployment | Operator only |
policy.administer.tenant | Tenant admin |
policy.administer.scope | Scope owner |
policy.administer.actor | Scope owner or self |
blob.upload | Upload to /v1/blobs |
blob.read | Read blob contents |
admin.flush | Force WAL flush (operator) |
admin.compact | Trigger compaction (operator) |
admin.health | Read full health diagnostics |
The full enumeration with defaults per preset is in Appendix A.
5.3 Deployment Presets
Operators pick one preset at server boot via CORTEX_DEPLOYMENT_PRESET=<name> or a custom YAML.
5.3.1 on_prem_enterprise
preset: on_prem_enterprise
allow: ["*"]
deny: []
defaults:
actor.require_signed: true
scope.auto_register: true
forget.cascade.default: derived_only
audit.retention: "P1Y"
Everything is on. Customer is responsible for their own internal policy.
5.3.2 cloud_shared_saas
preset: cloud_shared_saas
allow:
- "scope.create.org"
- "scope.create.user"
- "scope.create.agent"
- "scope.create.ws"
- "scope.write"
- "scope.read.local"
- "scope.read.holistic"
- "scope.read.descend"
- "understanding.read"
- "understanding.synthesize"
- "forget.cascade.derived_only"
- "forget.cascade.redact_events"
- "forget.gdpr"
- "forget.gdpr.cross_workspace"
- "import.*"
- "export.*"
- "lifecycle.subscribe"
- "audit.read"
- "blob.*"
- "policy.administer.tenant"
- "policy.administer.scope"
- "policy.administer.actor"
deny:
- "scope.create.global"
- "scope.create.cross_tenant"
- "scope.read.cross_tenant"
- "understanding.read.cross_scope"
- "audit.read.cross_actor"
defaults:
actor.require_signed: true
scope.auto_register: true
forget.cascade.default: derived_only
audit.retention: "P2Y"
rate_limit.write: "100/s"
rate_limit.read: "500/s"
5.3.3 cloud_private
preset: cloud_private
# Single-tenant cloud — global is "their private public"
allow: ["*"]
deny:
- "scope.create.cross_tenant"
- "scope.read.cross_tenant"
defaults:
actor.require_signed: true
scope.auto_register: true
forget.cascade.default: derived_only
audit.retention: "P3Y"
5.3.4 dev_local
preset: dev_local
allow: ["*"]
deny: []
defaults:
actor.require_signed: false # ONLY for local dev
scope.auto_register: true
forget.cascade.default: derived_only
audit.retention: "P7D"
For make dev. The only preset where unsigned actors are allowed.
5.4 Policy Propagation
A policy mutation (PUT on any tier) takes effect immediately for new requests. In-flight requests complete under the policy snapshot they started with.
For SSE lifecycle streams:
- A policy change that affects an open stream takes effect at the next event boundary.
- The current event is delivered if already in progress.
- The stream is then either continued (if still allowed) or closed with
event: policy_revokedfollowed by HTTP close.
5.5 Policy Endpoints
| Method | Path | Cap | Purpose |
|---|---|---|---|
| GET | /v1/policy/deployment | (read) | Read deployment preset |
| PUT | /v1/policy/deployment | policy.administer.deployment | Operator only |
| GET | /v1/policy/tenant/{tenant_id} | policy.administer.tenant (target tenant) | |
| PUT | /v1/policy/tenant/{tenant_id} | policy.administer.tenant | |
| GET | /v1/policy/scope/{scope_path} | policy.administer.scope (target) | |
| PUT | /v1/policy/scope/{scope_path} | policy.administer.scope | |
| GET | /v1/policy/actor/{actor_id} | policy.administer.actor (target) | |
| PUT | /v1/policy/actor/{actor_id} | policy.administer.actor | |
| GET | /v1/policy/effective?actor=...&scope=... | (read for self) | The effective capability set after stacking |
Effective policy response:
{
"actor": "user:alice",
"scope": "org:acme/dept:eng/user:alice",
"deployment_preset": "cloud_shared_saas",
"allowed": [
"scope.write", "scope.read.local", "scope.read.holistic",
"understanding.read", "forget.cascade.derived_only", "lifecycle.subscribe", "..."
],
"denied": [
{"capability": "scope.create.global", "denied_by_tier": "deployment"},
{"capability": "scope.read.cross_tenant", "denied_by_tier": "deployment"}
],
"rate_limits": {"write": "100/s", "read": "500/s"}
}
6. HTTP Conventions
6.1 URLs and versioning
- Base path:
/v1 - One major version. Breaking changes increment to
/v2; the old major stays online for 90 days minimum once a successor exists. (Not relevant pre-launch.) - No minor version in the URL; additive changes ship freely.
6.2 Content types
- Request body:
application/json; charset=utf-8 - Standard response:
application/json; charset=utf-8 - Streaming responses:
text/event-stream; charset=utf-8 - Blob upload:
multipart/form-datato/v1/blobs
6.3 Idempotency
Every write endpoint accepts an idempotency_key field in the body. Duplicates return the original response with X-Cortex-Replay: true.
- Keys are scoped to
(actor, endpoint, idempotency_key). - Keys expire after 24 hours.
- Without a key, writes are not idempotent. The SDK supplies one by default.
6.4 Pagination
Cursor-based pagination on all list endpoints.
GET /v1/facts?scope=...&limit=50
→
{
"items": [...],
"next_cursor": "eyJpZCI6ImZhY3RfMDFIWC4uLiJ9",
"has_more": true
}
GET /v1/facts?scope=...&limit=50&cursor=eyJpZCI6ImZhY3RfMDFIWC4uLiJ9
- Default
limit50, max 1000. - Cursors are opaque, base64url-encoded JSON. Clients MUST treat them as opaque.
- Cursors are stable across policy changes (they encode a snapshot point).
6.5 Rate limits
Per-actor token bucket. Limits set by deployment defaults, overridable per tenant.
Headers on every response:
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 487
X-RateLimit-Reset: 1715750460
On limit hit: 429 Too Many Requests with Retry-After.
6.6 Errors
Standard envelope. All error codes are UPPER_SNAKE_CASE — there is one casing rule, applied everywhere. The redundant lower-snake error field used in older drafts is retired.
{
"error_code": "POLICY_DENIED",
"message": "Cannot create scope under global:* on this deployment.",
"request_id": "req_01HX...",
"details": {
"capability": "scope.create.global",
"denied_by_tier": "deployment"
},
"retriable": false,
"documentation": "https://docs.cortexdb.ai/errors/POLICY_DENIED"
}
Normative shape: error_envelope.schema.json in Appendix D. Fields:
| Field | Type | Required | Notes |
|---|---|---|---|
error_code | UPPER_SNAKE string | yes | Stable identifier; clients pattern-match on this |
message | string | yes | Human-readable; may be localized in the future |
request_id | string | yes | Echoes X-Cortex-Request-ID |
details | object | no | Error-specific structured fields |
retriable | bool | yes | Whether the same request may succeed later |
documentation | URL | no | Link to error reference |
| HTTP | When |
|---|---|
| 400 | Validation error in request body |
| 401 | Auth failure (signature, expiry, jti revoked, actor mismatch) |
| 403 | Policy denial (capability missing in effective set) |
| 404 | Resource not found and the caller has read access — otherwise return 403 to avoid info leak |
| 409 | Conflict (idempotency-key reuse with different body; scope path collision on register) |
| 410 | Resource deleted (forget) — only when policy allows revealing existence |
| 422 | Semantic validation failure (e.g., invalid scope grammar, malformed timestamp) |
| 429 | Rate limit |
| 500 | Server error |
| 503 | Service degraded (e.g., embedding service unavailable; write WAL still succeeds but extraction queued) |
6.7 Request IDs
Every response includes X-Cortex-Request-ID. Clients may set their own via the same header on request; if absent, server generates a UUID v7. Older X-Request-ID header is accepted on input as a fallback for compatibility but is not echoed in responses.
6.8 Headers we set
| Header | Meaning |
|---|---|
X-Cortex-Request-ID | Request correlation |
X-Cortex-Policy | tier=...; decision=allow; capability=... (set on policy-gated endpoints) |
X-Cortex-Replay | true if response was served from idempotency cache |
X-Cortex-Deprecation | Set when calling a deprecated endpoint; value is the migration link |
6.9 Headers we accept
| Header | Meaning |
|---|---|
Authorization | Bearer token (required) |
X-Cortex-Actor | Actor identity (must match sub) |
X-Cortex-Idempotency-Key | Alternative to body field for GET-shaped idempotency |
X-Cortex-Trace-Parent | W3C tracecontext propagation |
7. Layer Reference
7.1 Events
The append-only WAL. Source of truth.
Record shape:
{
"id": "evt_01HX...",
"scope": "org:acme/dept:eng/user:alice",
"actor": "agent:support_bot_v3",
"modality": "conversation",
"content": { /* exactly as submitted in the envelope */ },
"context": {
"observed_at": "2026-05-13T15:42:00Z",
"recorded_at": "2026-05-13T15:42:09Z",
"preceded_by": ["evt_..."],
"intent": "deal_close",
"labels": [...]
},
"derives": ["fact_01HX...", "belief_01HX...", "ep_01HX..."],
"wal_offset": 134892,
"_partial": false
}
| Field | Type | Notes |
|---|---|---|
id | UUID v7 | Time-ordered |
wal_offset | uint64 | Position in the WAL; monotonic per node |
derives | array | Records derived from this event (back-pointer for "why" trail) |
Lifecycle: Created on Capture. Never updated. Deleted only by forget.gdpr with refcount=0.
7.2 Episodes
A causal chain of events.
{
"id": "ep_01HX...",
"scope": "org:acme/dept:eng/user:alice",
"name": "Acme deal Q3 close",
"summary": "Multi-day negotiation culminating in 200-seat signature on May 13.",
"events": ["evt_01HX...", "evt_01HX...", "..."],
"started_at": "2026-05-08T...",
"ended_at": "2026-05-13T15:42:00Z",
"valid_from": "2026-05-08T...",
"valid_to": null,
"recorded_from": "2026-05-13T15:42:09Z",
"recorded_to": null,
"actors_involved": ["user:alice", "user:priya@acme"],
"centrality_score": 0.78,
"causal_chain": [
{"event": "evt_...", "role": "trigger"},
{"event": "evt_...", "role": "decision"},
{"event": "evt_...", "role": "outcome"}
],
"_partial": false
}
Lifecycle: Built by the episode builder as related events accumulate. Sealed at Compress stage; once sealed, episode's events[] is immutable. New events with related context produce a new episode that may link as causal_successor.
7.3 Facts
Triple-shaped, optionally temporal.
{
"id": "fact_01HX...",
"scope": "org:acme/dept:eng/user:alice",
"subject": {"type": "entity", "id": "ent_acme_corp", "name": "Acme Corp"},
"predicate": "moved_to_seat_count",
"object": {"type": "literal", "datatype": "integer", "value": 200},
"supports": ["evt_01HX..."],
"valid_from": "2026-05-13T15:42:00Z",
"valid_to": null,
"recorded_from": "2026-05-13T15:42:09Z",
"recorded_to": null,
"confidence": 0.94,
"extractor": "gpt-4o-mini",
"superseded_by": null,
"supersedes": "fact_01HW...",
"_partial": false
}
| Field | Type | Notes |
|---|---|---|
subject | typed reference | Entity or concept reference |
predicate | string | Free-text or vocabulary-bound (see §7.3.1) |
object | typed value | Entity / literal / concept |
confidence | float | 0..1; extractor-provided |
superseded_by / supersedes | fact_id | The conflict-resolution chain |
Lifecycle: Created at Update (extract stage). Superseded by a newer fact via the conflict resolver. When superseded, the old fact gets recorded_to=now and superseded_by; it remains queryable via as_of.
7.3.1 Predicate vocabularies
Predicates are free-text by default. Tenants may register a controlled vocabulary:
POST /v1/vocabularies
{
"name": "sales_v1",
"predicates": [
{"id": "deal_stage", "datatype": "string", "values": ["intro","poc","close","signed","lost"]},
{"id": "seat_count", "datatype": "integer"},
{"id": "contact_at", "datatype": "entity"}
]
}
When a vocabulary is active for a scope, the extractor maps free-text predicates to vocabulary terms (with confidence). Unmapped predicates remain free-text.
7.4 Beliefs
Probabilistic claims with explicit stance, calibration, and revision policy.
{
"id": "belief_01HX...",
"scope": "org:acme/dept:eng/user:alice",
"claim": {
"subject": {"type": "entity", "id": "ent_acme_corp"},
"predicate": "is_likely_to_renew",
"object": {"type": "literal", "datatype": "boolean", "value": true}
},
"stance": "supported",
"confidence": 0.62,
"confidence_interval": {"lower": 0.48, "upper": 0.74, "method": "beta_posterior", "level": 0.90},
"calibration": {
"model": "platt_v2",
"brier_score_30d": 0.18,
"samples": 47,
"last_calibrated_at": "2026-05-13T00:00:00Z"
},
"supports": [
{"type": "fact", "id": "fact_01HX...", "weight": 0.34, "polarity": "supports"},
{"type": "fact", "id": "fact_01HX...", "weight": 0.28, "polarity": "supports"},
{"type": "episode", "id": "ep_01HX...", "weight": 0.22, "polarity": "supports"}
],
"contradicts": [
{"belief_id": "belief_01HW...", "tension": 0.41, "resolution": "newer_wins"}
],
"revision_policy": {
"trigger": "new_supporting_evidence_or_contradiction",
"min_evidence_delta": 0.05,
"cooldown": "PT5M",
"max_revisions_per_hour": 12
},
"valid_from": "2026-05-13T15:42:09Z",
"valid_to": null,
"recorded_from": "2026-05-13T15:42:09Z",
"recorded_to": null,
"last_revised_at": "2026-05-13T15:42:09Z",
"revision_count": 3,
"supporting_evidence_redacted": false,
"supporting_evidence_redacted_fraction": 0.0,
"why_url": "/v1/beliefs/why?belief_id=belief_01HX...",
"_partial": false
}
| Field | Type | Notes |
|---|---|---|
claim | object | Triple form: {subject, predicate, object} |
stance | enum | One of supported, contradicted, uncertain, deprecated. See §7.4.1 |
confidence | float | Point estimate, 0..1 |
confidence_interval | object | {lower, upper, method, level} — calibrated CI. Methods: beta_posterior, bootstrap, wilson |
calibration | object | Model + recent Brier / ECE score; updated by the calibration job |
supports | array | Weighted contributing records, each with explicit polarity (supports | against) |
contradicts | array | Other beliefs in tension, with tension score and proposed resolution |
revision_policy | object | Per-belief revision rules (overrides tenant defaults) |
last_revised_at | RFC3339 | |
revision_count | int | Total revisions since creation |
supporting_evidence_redacted | bool | True if any support was GDPR-redacted (see §11.3) |
supporting_evidence_redacted_fraction | float | 0..1; fraction of supports[] redacted |
why_url | string | Permalink to the explainability endpoint for this belief |
_partial | bool | True when belief is in revision or evidence-redacted-decayed state |
_partial_reason | enum | revision_in_progress, supporting_evidence_redacted, calibration_stale |
7.4.1 Stance enum
| Stance | Meaning |
|---|---|
supported | Evidence supports the claim; confidence reflects this. |
contradicted | Evidence has overturned the claim; confidence reflects how strongly. The belief is kept (not deleted) for explainability. |
uncertain | Evidence is mixed or insufficient; confidence near 0.5. |
deprecated | The claim's predicate vocabulary or subject identity has changed; the record is retained for as_of queries but is not surfaced by default reads. |
Lifecycle: Created at Consolidate. Revised when revision_policy.trigger fires; revision bumps last_revised_at and revision_count. When supporting_evidence_redacted_fraction == 1.0, confidence decays per policy and stance moves to uncertain. If confidence < tenant.confidence_floor (default 0.3), the belief is deleted and surfaces as 410 RESOURCE_DELETED on direct ID lookup; list endpoints silently omit it.
7.5 Understanding
Synthesized conceptual nodes. Beta tier in v1.
7.5.1 Identity, versioning, and staleness
| Property | Rule |
|---|---|
| Concept identity | A concept_id is stable across versions. The id is assigned at first synthesis and never changes; only version increments. |
| Concept versioning | Versions are immutable. Revision creates a new version row linked to previous_version. All prior versions queryable via as_of. |
| Coalescence | When two concepts are determined to refer to the same thing, the older one's id is canonical; the newer becomes an alias (canonical_id field) and is deprecated. No data is lost. |
| Splitting | When a concept is determined to conflate two things, a new concept_id is created for the split-out half; the original's valid_to is set; both new concepts list the original as derived_from. |
| Relation vocabulary | Edges use a tenant-scoped, vocabulary-bound relation field. Default relations: is_a, part_of, specializes, generalizes, depends_on, causes, contradicts, co_occurs_with, related_to. Custom relations via POST /v1/vocabularies (kind=relation). |
| Synthesis inputs | Each concept declares the input layers it was synthesized from in synthesis_inputs. Re-synthesis re-derives from these inputs; as_of synthesis is supported. |
| Staleness | A concept becomes stale when its synthesis_inputs have updates more recent than last_synthesized_at exceeding a threshold. Stale concepts retain _partial: false but carry staleness_score: 0..1 and staleness_reason. |
| Coverage score | Tenant-level signal: fraction of supporting facts/episodes within scope that were actually consulted during synthesis. Surfaced on GET /v1/understanding/coverage. |
| Invalidation on forget | If supporting facts/beliefs are deleted, the concept does NOT vanish — it gains support_loss_fraction: 0..1. If support_loss_fraction > tenant.understanding.invalidation_threshold (default 0.5), stance moves to deprecated and the concept is hidden from default reads (still queryable via ?include_deprecated=true). |
7.5.2 Shape
{
"id": "concept_01HX...",
"scope": "org:acme/dept:eng",
"name": "B2B SaaS Sales Cycle",
"canonical_id": null,
"version": 3,
"previous_version": "concept_01HW...",
"derived_from": [],
"summary": "Multi-stage process typically involving prospecting, qualification, POC, negotiation, signature, onboarding.",
"summary_embedding": null,
"synthesis_inputs": {
"layers": ["facts", "episodes", "beliefs"],
"filters": {"topic": "sales_process"},
"as_of": "2026-05-13T00:00:00Z",
"synthesizer": "claude-opus-4-6",
"synthesizer_version": "2026-05-08"
},
"supported_by": {
"facts": ["fact_01HX...", "..."],
"episodes": ["ep_01HX...", "..."],
"beliefs": ["belief_01HX...", "..."],
"external": [
{"source": "wikipedia", "id": "Sales_process", "url": "..."}
]
},
"edges": [
{"to": "concept_01HX...", "relation": "specializes", "weight": 0.81, "vocabulary": "default"},
{"to": "concept_01HX...", "relation": "depends_on", "weight": 0.55, "vocabulary": "default"}
],
"valid_from": "2026-04-01T00:00:00Z",
"valid_to": null,
"recorded_from": "2026-05-13T15:42:09Z",
"recorded_to": null,
"last_synthesized_at": "2026-05-13T15:42:09Z",
"stance": "supported",
"confidence": 0.78,
"staleness_score": 0.0,
"staleness_reason": null,
"support_loss_fraction": 0.0,
"coverage_score": 0.91,
"_partial": true,
"_partial_reason": "consolidation_in_progress",
"_progress": 0.42
}
| Field | Type | Notes |
|---|---|---|
id | concept_id | Stable across versions |
canonical_id | concept_id | null | Set when this concept has been merged into another; lookups follow the alias |
version | int | Bumped on revision |
previous_version | concept_id | Links to the prior version |
derived_from | array | Set when this concept resulted from splitting another |
synthesis_inputs | object | Layers + filters + synthesizer used to produce this version |
edges | array | Relations to other concepts, vocabulary-bound |
stance | enum | supported, uncertain, deprecated (same semantics as §7.4.1) |
staleness_score | float | 0..1; fraction of input updates not yet incorporated |
support_loss_fraction | float | 0..1; fraction of supports forgotten since last synthesis |
coverage_score | float | 0..1; fraction of in-scope inputs consulted during synthesis |
_partial | bool | True until full synthesis lands |
_partial_reason | enum | consolidation_in_progress, insufficient_supporting_facts, synthesis_disabled_by_policy, no_implementation_yet, stale_inputs_pending_resynthesis |
_progress | float | 0..1, when meaningful |
Lifecycle: Created/updated at Compress. Versioning is append-only — old versions retained for as_of. Coalescence and splitting do not delete history. Re-synthesis is triggered by (a) explicit POST /v1/understanding/synthesize, (b) staleness threshold breach, or (c) support_loss threshold breach.
8. Endpoint Reference
8.1 POST /v1/experience — Write
Capability: scope.write (or scope.write.elevated if writing above the actor's natural level)
Request: Experience envelope (see §3.5).
Response (async, default):
HTTP/1.1 202 Accepted
Content-Type: application/json
X-Cortex-Policy: tier=scope; decision=allow; capability=scope.write
X-Cortex-Request-ID: req_01HX...
{
"event_id": "evt_01HX...",
"status": "captured",
"wal_offset": 134892,
"lifecycle_stream": "/v1/lifecycle/stream?event_id=evt_01HX..."
}
Response (sync via ?wait=indexed):
HTTP/1.1 200 OK
{
"event_id": "evt_01HX...",
"status": "indexed",
"wal_offset": 134892,
"stages_completed": ["captured", "extracted", "indexed"],
"derives": ["fact_01HX...", "belief_01HX..."],
"elapsed_ms": {"capture": 4, "extract": 410, "index": 88}
}
wait parameter values:
| Value | Returns when | Typical latency |
|---|---|---|
| (omitted) | WAL append | ~5ms |
captured | WAL fsync | ~10ms |
indexed | BM25 + HNSW insert | ~100-500ms |
consolidated | Beliefs/Understanding touched | ~500-3000ms |
Errors:
| HTTP | Code | When |
|---|---|---|
| 401 | actor_mismatch | Header doesn't match token sub |
| 403 | policy_denied | Capability missing |
| 422 | invalid_envelope | Body validation failed; details.field and details.reason populated |
| 409 | idempotency_conflict | Same key, different body |
| 503 | wal_unavailable | WAL not writable (rare; usually disk pressure) |
8.2 POST /v1/experience/bulk — Bulk Write
Capability: scope.write
Request:
{
"scope": "org:acme/dept:eng/user:alice",
"actor": {...},
"items": [
{"modality": "...", "content": {...}, "context": {...}, "idempotency_key": "..."},
...
],
"directives": {...},
"ordering": "strict_temporal" | "batch_throughput"
}
Maximum 1000 items per call. Larger imports should use /v1/import/jsonl.
Response (always async, 202):
{
"batch_id": "batch_01HX...",
"accepted": 1000,
"lifecycle_stream": "/v1/lifecycle/stream?batch_id=batch_01HX..."
}
Individual event_ids are not returned synchronously; subscribe to the lifecycle stream or query by idempotency_key:
GET /v1/experience/by-idempotency-key/{key}
8.3 POST /v1/recall — Read
Capability: scope.read.local (always); scope.read.holistic if view=holistic; scope.read.descend if view=descend; diagnostics.read if diagnostics ≠ "none".
Request:
{
"scope": "org:acme/dept:eng/user:alice",
"view": "holistic",
"query": "What did we decide about the Q3 launch?",
"include": ["beliefs", "facts", "episodes"],
"exclude_content": false,
"temporal": {
"as_of": null,
"valid_during": null,
"natural": "last 30 days",
"reference_date": null
},
"filters": {
"AND": [
{"observed_actor": {"in": ["user:alice", "agent:planner_v3"]}},
{"confidence": {"gte": 0.6}}
]
},
"budgets": {
"max_tokens": 4000,
"per_layer_limits": {
"events": 0,
"episodes": 5,
"facts": 20,
"beliefs": 10,
"understanding": 3
}
},
"citation_mode": "inline_with_markers",
"diagnostics": "summary",
"stream": false,
"idempotency_key": "recall-alice-q3-001"
}
Field reference:
| Field | Type | Notes |
|---|---|---|
include | array | Replaces older layers field; whitelist of layers to populate. Empty/absent ⇒ all per the view default. |
exclude_content | bool | When true, omits large content/summary strings; returns IDs + metadata only. Saves tokens when callers will re-hydrate selectively. |
temporal.reference_date | RFC3339 | Anchor for parsing temporal.natural phrases (e.g., "last week"). Defaults to request time. |
budgets.max_tokens | int | Cross-layer knapsack target |
budgets.per_layer_limits | map | Explicit caps per layer; overrides default view budget split |
citation_mode | enum | none, inline_with_markers, block_at_end, structured_only |
diagnostics | enum | none (default for non-admin tokens), summary (timings, plan name), full (scoring contributions, policy attribution, model identifiers). Requires diagnostics.read capability for non-none. |
stream | bool | See §10.1 |
idempotency_key | string | Optional; if present, repeated calls return the same pack_id and cached pack within 60s |
Response (non-streaming): The stratified pack (see §9), including a pack_id that can be referenced by /v1/answer for follow-up.
Response (streaming, stream=true): SSE (see §10.1).
8.4 POST /v1/answer — Recall + LLM
Capability: scope.read.local + any traversal caps + llm.invoke + diagnostics.read if diagnostics ≠ "none".
/v1/answer shares the recall provenance contract: the same pack_id, provenance, and citations blocks appear in the response. It is recall composed with an LLM call, not a separate opaque endpoint.
{
"scope": "org:acme/dept:eng/user:alice",
"view": "holistic",
"question": "Did Acme renew?",
"answer_model": "claude-opus-4-6",
"answer_max_tokens": 1500,
"include": ["beliefs", "facts", "episodes"],
"exclude_content": false,
"temporal": {"as_of": null, "natural": "last 30 days"},
"filters": {},
"budgets": {"max_tokens": 4000, "per_layer_limits": {"facts": 20, "beliefs": 10}},
"citation_mode": "inline_with_markers",
"diagnostics": "summary",
"use_pack_id": null,
"stream": false
}
| Field | Type | Notes |
|---|---|---|
answer_model | string | LLM identifier (provider-routed) |
answer_max_tokens | int | Output ceiling |
use_pack_id | string | null | Skip recall and use a previously generated pack (within 60s TTL). When set, recall-related fields are ignored. |
| (other fields) | Same semantics as /v1/recall |
Response:
{
"pack_id": "pack_01HX...",
"answer": "Acme upgraded to 200 seats on May 13. They did not renew their previous contract; they signed a new one. [1][2]",
"citations": [
{"marker": "[1]", "layer": "fact", "id": "fact_01HX...", "support_strength": 0.91},
{"marker": "[2]", "layer": "event", "id": "evt_01HX...", "support_strength": 0.87}
],
"provenance": {
"trail": [...],
"citations": {...}
},
"diagnostics": {
"recall_ms": 412,
"llm_ms": 814,
"total_ms": 1240,
"pack_used": {"facts": 5, "beliefs": 2, "episodes": 1},
"answer_model": "claude-opus-4-6",
"answer_tokens": {"prompt": 1284, "completion": 218}
},
"as_of": "2026-05-13T15:42:09Z"
}
provenance and citations have the same shape as in /v1/recall (§9). as_of reflects the effective temporal anchor used.
Streaming variant: token-by-token answer, followed by pack_id, citations, provenance, diagnostics, and as_of (§10.2).
8.5 POST /v1/forget — Selective Forget
Capability: forget.cascade.<mode> matching the requested cascade.
Note: Earlier drafts used
DELETE /v1/forgetwith a JSON body. We usePOSTinstead because (a) DELETE with bodies is unreliable across HTTP clients/proxies, (b)forgetis a job that may be long-running, and (c) it composes uniformly with the/v1/erasuresjob family for true erasure (§8.6).
{
"scope": "org:acme/dept:eng/user:alice",
"layers": ["beliefs", "facts"],
"selector": {
"about_subject": "user:alice",
"about_entity": "ent_acme_corp",
"predicate": "is_likely_to_renew",
"memory_ids": [],
"valid_during": null,
"recorded_during": null
},
"cascade": "derived_only",
"confirm_all": false,
"audit_note": "User retracted speculation",
"idempotency_key": "forget-alice-001"
}
Response:
{
"deleted": {
"events": 0,
"episodes": 0,
"facts": 2,
"beliefs": 1,
"understanding": 0
},
"audit_id": "audit_01HX..."
}
Cascade modes:
| Mode | Capability | Behavior |
|---|---|---|
derived_only (default) | forget.cascade.derived_only | Deletes from named layers; events untouched |
redact_events | forget.cascade.redact_events | Blanks event payloads; keeps id and wal_offset |
events_too is NOT a cascade value here — true erasure of events goes through /v1/erasures (§8.6).
confirm_all flag. When the selector is empty (or only layers is set), the server requires confirm_all: true to proceed — otherwise it 422s with EMPTY_SELECTOR_WITHOUT_CONFIRMATION. This is the foot-gun guard for "forget everything in this scope."
8.6 /v1/erasures — Reference-Counted GDPR Erasure
This family replaces the prior DELETE /v1/forget/gdpr endpoint. It is the only path to true event deletion. It exposes preview, manifest, status, and cancel — long-running jobs deserve a proper lifecycle.
8.6.1 POST /v1/erasures/preview — Dry-run
Capability: forget.gdpr (read-only preview is gated by the same cap as the destructive action, so callers can't enumerate without authorization).
{
"scope": "org:acme/user:alice",
"audit_note": "DSR #1234 — preview"
}
Response:
{
"preview_id": "ervw_01HX...",
"scope": "org:acme/user:alice",
"estimated_affected": {
"events": 12480,
"episodes": 318,
"facts": 4120,
"beliefs": 318,
"understanding": 12
},
"refcount_breakdown": {
"events_to_delete": 9420,
"events_to_redact": 3060,
"events_under_legal_hold": 0
},
"cross_scope_propagation": {
"affected_workspaces": [
{"scope": "ws:acme-q3-launch", "events_referenced": 412, "co_owners": ["user:priya@acme"]}
],
"requires_capability": "forget.gdpr.cross_workspace"
},
"legal_holds": [],
"estimated_duration_ms": 240000,
"manifest_url": "/v1/erasures/preview/ervw_01HX.../manifest"
}
8.6.2 GET /v1/erasures/preview/{preview_id}/manifest — Full plan
Returns the line-by-line manifest — every event ID that would be deleted vs redacted, every cross-scope reference, every belief that would be demoted. Used for legal review and customer approval. Manifests expire after 24 hours.
8.6.3 POST /v1/erasures — Execute
Capability: forget.gdpr. Cross-workspace propagation additionally requires forget.gdpr.cross_workspace.
{
"scope": "org:acme/user:alice",
"from_preview_id": "ervw_01HX...",
"audit_note": "DSR #1234 — Alice exercised right to erasure",
"idempotency_key": "erasure-dsr-1234"
}
If from_preview_id is set and not expired, the server validates the manifest is still current (no new events under the scope since the preview); if drift exceeds a tenant-configurable threshold, the request 409s and a fresh preview is required. If from_preview_id is omitted, the server runs a preview inline before executing.
Response (always async, 202):
{
"erasure_id": "erasure_01HX...",
"status": "running",
"manifest_url": "/v1/erasures/erasure_01HX.../manifest",
"lifecycle_stream": "/v1/lifecycle/stream?erasure_id=erasure_01HX..."
}
8.6.4 GET /v1/erasures/{erasure_id} — Status
{
"erasure_id": "erasure_01HX...",
"status": "running",
"phase": "delete",
"fraction_complete": 0.78,
"progress": {
"deleted_events": 9420,
"redacted_events": 3060,
"demoted_beliefs": 18,
"elapsed_ms": 180000
},
"audit_id": null
}
When status: "completed", the audit_id field is populated.
8.6.5 POST /v1/erasures/{erasure_id}/cancel
Best-effort cancellation. The server stops at the next phase boundary; already-deleted events cannot be restored (their WAL slots are gone). The response indicates what was completed before cancellation took effect.
{
"erasure_id": "erasure_01HX...",
"cancellation_accepted": true,
"deleted_events_before_cancel": 4280,
"audit_id": "audit_01HX..."
}
8.6.6 Lifecycle events emitted
event: erasure_progress
data: {"erasure_id": "...", "phase": "enumerate", "fraction_complete": 0.12}
event: erasure_progress
data: {"erasure_id": "...", "phase": "refcount", "fraction_complete": 0.45}
event: erasure_progress
data: {"erasure_id": "...", "phase": "delete", "fraction_complete": 0.78,
"deleted_events": 9420, "redacted_events": 3060}
event: erasure_complete
data: {"erasure_id": "...", "summary": {...}, "audit_id": "audit_01HX..."}
Detailed semantics in §11.3.
8.7 Layer-Scoped Reads
8.7.1 GET /v1/events
Capability: scope.read.local + traversal as appropriate. Default view is local — see §18.2 item 14.
GET /v1/events
?scope=org:acme/dept:eng/user:alice
&view=local
&since=2026-04-01T00:00:00Z
&observed_actor=user:alice
&modality=conversation
&limit=50
&cursor=...
Pass view=holistic explicitly to walk up the scope hierarchy.
Response:
{
"items": [ /* Event records (see §7.1) */ ],
"next_cursor": "...",
"has_more": true
}
8.7.2 GET /v1/episodes
GET /v1/episodes?scope=...&view=...&actor=...&overlapping=2026-04-01..2026-05-01&with_causal_chain=true&limit=50&cursor=...
8.7.3 GET /v1/facts
Default view is local — pass view=holistic explicitly for hierarchy traversal.
GET /v1/facts
?scope=...
&view=local
&subject=ent_acme_corp
&predicate=deal_stage
&as_of=2026-04-15T00:00:00Z
&min_confidence=0.6
&include_superseded=false
&limit=50
as_of × include_superseded interaction (§18.2 item 1): both filters apply independently and conjunctively. as_of clips both axes (recorded_* ≤ as_of, valid_from ≤ as_of < valid_to). include_superseded=true returns superseded historical versions whose recorded_to ≤ as_of.
8.7.4 GET /v1/facts/timeline
Returns the supersession chain for (subject, predicate):
{
"subject": {"id": "ent_acme_corp", "name": "Acme Corp"},
"predicate": "deal_stage",
"timeline": [
{"fact_id": "fact_01HV...", "value": "poc", "valid_from": "...", "valid_to": "2026-04-10T..."},
{"fact_id": "fact_01HW...", "value": "close", "valid_from": "2026-04-10T...", "valid_to": "2026-05-13T..."},
{"fact_id": "fact_01HX...", "value": "signed", "valid_from": "2026-05-13T...", "valid_to": null}
]
}
8.7.5 GET /v1/beliefs
GET /v1/beliefs?scope=...&about=ent_acme_corp&min_confidence=0.6&as_of=...&limit=50
8.7.6 GET /v1/beliefs/why
The "why do you think that?" endpoint. Unique to CortexDB.
GET /v1/beliefs/why?belief_id=belief_01HX...
Response:
{
"belief": { /* belief record (§7.4) */ },
"support_graph": {
"nodes": [
{"id": "fact_01HX...", "type": "fact", "weight": 0.34, "summary": "..."},
{"id": "ep_01HX...", "type": "episode","weight": 0.22, "summary": "..."},
{"id": "evt_01HX...", "type": "event", "summary": "..."},
{"id": "evt_01HX...", "type": "event", "summary": "..."}
],
"edges": [
{"from": "fact_01HX...", "to": "evt_01HX...", "relation": "extracted_from"},
{"from": "fact_01HX...", "to": "evt_01HX...", "relation": "extracted_from"},
{"from": "ep_01HX...", "to": "evt_01HX...", "relation": "contains"},
{"from": "belief_01HX...","to": "fact_01HX...", "relation": "supported_by"}
]
},
"narrative": "I think Acme is likely to renew because on May 13 they upgraded to 200 seats (fact_01HX), and this followed a positive POC episode (ep_01HX) in April. The signal weight is 0.62 with CI [0.48, 0.74].",
"narrative_model": "claude-haiku-4-5"
}
8.7.7 GET /v1/understanding
GET /v1/understanding?scope=...&topic=B2B+SaaS&depth=2&limit=50
8.7.8 GET /v1/understanding/{concept_id}
Returns full concept record (§7.5).
8.7.9 GET /v1/understanding/{concept_id}/related
GET /v1/understanding/{concept_id}/related?relation=specializes&depth=2&limit=50
8.7.10 GET /v1/understanding/coverage
GET /v1/understanding/coverage?scope=org:acme/dept:eng
Response:
{
"scope": "org:acme/dept:eng",
"concept_count": 421,
"by_topic": [
{"topic": "sales_process", "concepts": 38, "avg_confidence": 0.74},
{"topic": "product_features","concepts": 92, "avg_confidence": 0.81},
{"topic": "customer_personas","concepts": 24, "avg_confidence": 0.62}
],
"synthesis_lag": "P2D",
"_partial": true
}
8.7.11 POST /v1/understanding/synthesize
Capability: understanding.synthesize
{
"scope": "org:acme/dept:eng",
"topics": ["sales_process"],
"force": false
}
Response: 202 + lifecycle stream URL.
8.8 Scopes
8.8.1 POST /v1/scopes — Register
Capability: policy.administer.scope for the path being created.
{
"path": "ws:acme-q3-launch",
"members": [
{"actor": "user:alice", "role": "owner"},
{"actor": "agent:planner_v3", "role": "writer"},
{"actor": "agent:summarizer", "role": "reader"}
],
"policies": {
"consolidation": "merge_compatible_beliefs",
"conflict_resolution": "latest_wins_within_confidence_band",
"inherit_from": ["org:acme"],
"audit": "full",
"children": {"auto_register": true}
}
}
Response: 201 Created with the canonical scope record.
8.8.2 GET /v1/scopes — Lookup by path
Routing note: scope paths contain
/, so we cannot put them in URL path segments without ambiguity. Scope lookup, member edit, and delete all use the query parameter?path=...form. SDKs canonicalize and percent-encode the value.
GET /v1/scopes?path=org:acme%2Fdept:eng%2Fuser:alice
Returns the scope record including auto-provisioned status. 404 if the path has never been written to.
8.8.3 PUT /v1/scopes/members?path=...
Atomic member edit. Full member list submitted; server computes diff.
PUT /v1/scopes/members?path=ws:acme-q3-launch
{
"members": [
{"actor": "user:alice", "role": "owner"},
{"actor": "agent:planner_v3", "role": "writer"},
{"actor": "agent:summarizer", "role": "reader"}
]
}
8.8.4 DELETE /v1/scopes?path=...
Capability: policy.administer.scope AND forget.gdpr if records=forget and any records exist.
Deletes the scope's policy and (configurably) its records. Two modes via query param:
?records=keep(default) — Only deletes the policy/registration; records remain accessible to other allowed scopes.?records=forget— Runs aPOST /v1/erasuresfor this scope first, then removes the policy.
8.8.5 GET /v1/scopes — List
GET /v1/scopes?prefix=org:acme/&auto_provisioned=false&limit=50
8.8.6 POST /v1/scopes/prune — Cleanup
Capability: policy.administer.tenant
Removes auto-provisioned scopes that have had no writes in N days and no explicit owner action.
{
"older_than": "P90D",
"dry_run": true
}
Response:
{
"candidates": [
{"path": "org:acme/.../user:bob/temp:debug",
"last_write": "2026-02-01T...",
"records": {"events": 4, "facts": 0}}
],
"total": 73
}
8.9 Lifecycle
8.9.1 GET /v1/lifecycle/stream — SSE
Capability: lifecycle.subscribe
GET /v1/lifecycle/stream?scope=org:acme/user:alice&since_lifecycle_id=lce_01HX...
Accept: text/event-stream
The since_lifecycle_id parameter takes a lifecycle event ID (lce_...), not a memory event ID (evt_...). Server keeps a 1h replay buffer keyed by lifecycle event arrival time.
Filter to a specific memory event via ?event_id=evt_01HX... (subscribe only to the lifecycle stages of one event). Filter to a job via ?job_id= or one of ?import_id=, ?export_id=, ?erasure_id=, ?batch_id=.
Filter to event types via ?events=captured,consolidated (subscribe to a subset; full catalog §13).
Response: SSE stream. See §10 and §13.
8.9.2 GET /v1/lifecycle/event/{lce_id} — Poll one lifecycle event
Returns one lifecycle event row:
{
"lifecycle_id": "lce_01HX...",
"event_id": "evt_01HX...",
"stage": "extracted",
"ts": "2026-05-13T15:42:09.418Z",
"payload": {"derived": {"facts": 3, "entities": 2, "beliefs": 1}}
}
8.9.3 GET /v1/lifecycle/memory-event/{evt_id} — Aggregated view
Returns the current aggregated state of a memory event (which stages are complete, which are pending):
{
"event_id": "evt_01HX...",
"stages_completed": ["captured", "extracted", "indexed"],
"stages_pending": ["consolidated"],
"lifecycle_event_ids": ["lce_01HX...", "lce_01HX...", "lce_01HX..."],
"derives": ["fact_01HX...", "belief_01HX..."],
"errors": []
}
8.9.4 GET /v1/lifecycle — Catch-up paginated
GET /v1/lifecycle?scope=...&since_lifecycle_id=lce_01HX...&limit=100&cursor=...
For clients that can't hold SSE connections. Returns lifecycle event rows in lce_ order.
8.9.5 POST /v1/lifecycle/memory-event/{evt_id}/cancel
Capability: scope.write on the event's home scope; the caller must also be the event's caller OR have scope.administer on the home scope.
Cancels queued lifecycle stages (extracted, indexed, consolidated, compressed) for a memory event that has already been captured. The WAL capture itself is not reversible — the event remains in the WAL and remains directly addressable via GET /v1/events/{evt_id} — but the event is flagged excluded_from_recall: true and will not appear in /v1/recall, /v1/answer, or layer-scoped list endpoints.
Use cases: client submitted by accident; client realized post-submit that the content shouldn't be derived from; client wants to retry with corrected content.
Request:
{
"audit_note": "Caller cancelled (incorrect content); will resubmit"
}
Response:
{
"event_id": "evt_01HX...",
"stages_cancelled": ["extracted", "indexed", "consolidated"],
"stages_already_completed": ["captured"],
"excluded_from_recall": true,
"audit_id": "audit_01HX..."
}
If any non-captured stage had already completed before the cancel arrived, the derived records produced by that stage are also marked excluded_from_recall: true (they are not deleted — to delete, the caller follows up with a POST /v1/forget).
8.10 Audit
8.10.1 GET /v1/audit
Capability: audit.read. audit.read.cross_actor additionally required to query for actors other than self.
GET /v1/audit
?scope=org:acme/...
&actor=agent:planner_v3
&capability=scope.write
&decision=deny
&since=2026-05-01T00:00:00Z
&limit=100
Returns audit log rows (§14).
8.10.2 GET /v1/audit/{audit_id}
Single row by ID.
8.10.3 POST /v1/audit/verify
Tamper-evidence check. Submit a request body; server hashes it and confirms whether it matches an audit row.
{
"audit_id": "audit_01HX...",
"body": "{ ... raw JSON of the original request ... }"
}
Response:
{
"match": true,
"hashed_at": "2026-05-13T15:42:09Z",
"algorithm": "sha256",
"tenant_pepper_version": 3
}
8.11 Import
8.11.1 POST /v1/import/mem0
Capability: import.from.mem0
{
"scope_template": "org:acme/user:{user_id}",
"source": {
"format": "mem0_export_v3",
"url": "s3://bucket/mem0-export.jsonl.gz",
"inline": null,
"credentials_ref": "secret:s3_reader_v1"
},
"options": {
"ordering": "strict_temporal",
"preserve_timestamps": true,
"map_categories_to": "understanding",
"extract": ["facts", "entities", "beliefs"],
"embed": "lazy",
"skip_on_conflict": false
}
}
Response:
{
"import_id": "imp_01HX...",
"status": "running",
"estimated_items": 12480,
"lifecycle_stream": "/v1/lifecycle/stream?import_id=imp_01HX..."
}
Mapping table in §15.1.
8.11.2 POST /v1/import/zep
Capability: import.from.zep
Same shape; Zep-specific options:
{
"options": {
"preserve_bi_temporal": true,
"ontology_mapping": {
"PreferenceEdge": "user_preference",
"PurchaseEdge": "purchase"
}
}
}
Mapping in §15.2.
8.11.3 POST /v1/import/letta
Capability: import.from.letta
{
"scope_template": "agent:{agent_name}",
"source": {"format": "letta_af", "url": "..."},
"options": {
"map_core_blocks_to": "understanding",
"map_archival_to": "events"
}
}
Mapping in §15.3.
8.11.4 POST /v1/import/openai
Capability: import.from.openai
For ChatGPT memory exports (JSON dump format).
8.11.5 POST /v1/import/jsonl
Capability: import.from.jsonl
Generic line-delimited envelope JSON. Each line is a full experience envelope.
8.11.6 GET /v1/import/{import_id}
Status poll.
{
"import_id": "imp_01HX...",
"status": "running",
"fraction_complete": 0.42,
"processed": 5240,
"total": 12480,
"errors": [
{"line": 1208, "error": "invalid_envelope", "detail": "missing scope"}
]
}
8.11.7 POST /v1/import/{import_id}/cancel
8.12 Export
8.12.1 POST /v1/export
Capability: export.format.<target> matching requested format.
{
"scope": "org:acme/user:alice",
"format": "jsonl" | "mem0" | "zep" | "letta",
"layers": ["events", "facts", "beliefs"],
"as_of": null,
"destination": {
"kind": "url",
"url": "s3://bucket/export.jsonl.gz",
"credentials_ref": "secret:s3_writer_v1"
}
}
Response:
{
"export_id": "exp_01HX...",
"estimated_size_bytes": 4823648,
"lifecycle_stream": "/v1/lifecycle/stream?export_id=exp_01HX..."
}
8.12.2 GET /v1/export/{export_id}
Status poll.
8.13 Blobs
For multimodal content.
8.13.1 POST /v1/blobs — Upload
Capability: blob.upload
multipart/form-data with field file. Maximum 100MB per blob by default.
Response:
{
"blob_id": "blob_01HX...",
"size_bytes": 4823648,
"content_type": "image/png",
"sha256": "..."
}
Reference in an experience envelope via content.media: [{"blob_id": "blob_01HX..."}].
8.13.2 GET /v1/blobs/{blob_id}
Capability: blob.read
Returns the raw bytes. Content-Type reflects the original.
8.14 Vocabularies
8.14.1 POST /v1/vocabularies
Capability: policy.administer.tenant
See §7.3.1.
8.14.2 GET /v1/vocabularies/{name}
8.14.3 PUT /v1/vocabularies/{name}
8.15 Admin (existing surface, retained)
| Endpoint | Capability | Purpose |
|---|---|---|
GET /v1/admin/health | admin.health | Liveness + diagnostics |
GET /v1/admin/metrics | admin.health | Prometheus metrics |
POST /v1/admin/flush-views | admin.flush | Force flush async views |
POST /v1/admin/compact | admin.compact | Trigger compaction |
GET /v1/admin/version | (open) | Build info |
8.16 Auth
8.16.1 POST /v1/auth/revoke
Capability: auth.revoke (operator or, for self-revocation, the token holder).
Adds a token's jti to the revocation list. The token becomes invalid for all subsequent calls until exp passes.
{
"jti": "01HX...",
"reason": "rotated"
}
Response: 204 No Content.
Revocation lists are TTL-keyed by exp and auto-pruned. Maximum list size 1M; tenants with high revocation volume should issue shorter-lived tokens.
8.16.2 GET /v1/auth/whoami
Capability: (open to authenticated callers)
Returns the effective identity and capability set for the current token. Useful for clients and SDKs to introspect their environment.
{
"caller": "agent:planner_v3",
"tenant_id": "acme",
"deployment_preset": "cloud_shared_saas",
"token": {"jti": "01HX...", "iss": "...", "exp": 1715836800},
"effective_capabilities": ["scope.write", "scope.read.holistic", "lifecycle.subscribe", "..."]
}
8.17 Temporal Phrases
8.17.1 POST /v1/temporal/phrases
Capability: temporal.phrases.write (tenant admin)
Register a custom natural-language temporal phrase. See §12.4 for the parsing model.
{
"name": "fiscal_q3",
"pattern": "fiscal Q3",
"resolves_to": {"valid_during": ["2026-07-01", "2026-10-01"]}
}
Response: 201 Created.
8.17.2 GET /v1/temporal/phrases
Capability: temporal.phrases.read (any authenticated reader)
{
"items": [
{"name": "fiscal_q3", "pattern": "fiscal Q3", "resolves_to": {...}}
]
}
8.17.3 DELETE /v1/temporal/phrases/{name}
Capability: temporal.phrases.write
9. The Stratified Pack
The output of POST /v1/recall in non-streaming mode.
{
"request_id": "req_01HX...",
"scope": "org:acme/dept:eng/user:alice",
"view": "holistic",
"context_block": "Acme is a B2B SaaS in late-stage negotiation. They moved to 200 seats on May 13, 2026 and signed. Prior history includes a successful POC in April and an executive intro in February.",
"layers": {
"beliefs": [
{"id": "belief_01HX...", "claim": {...}, "confidence": 0.62, "ranked_position": 1, "score": 0.94}
],
"facts": [
{"id": "fact_01HX...", "subject": {...}, "predicate": "...", "object": {...}, "ranked_position": 1, "score": 0.91}
],
"episodes": [
{"id": "ep_01HX...", "name": "Acme deal Q3 close", "summary": "...", "ranked_position": 1, "score": 0.88}
],
"events": [],
"understanding": [
{"id": "concept_01HX...", "name": "B2B SaaS Sales Cycle", "ranked_position": 1, "score": 0.42}
]
},
"provenance": {
"trail": [
{"phase": "ARIL_classify", "plan": "DIRECT_LOOKUP_RECHECK", "elapsed_ms": 12},
{"phase": "hybrid_retrieve", "bm25_hits": 124, "hnsw_hits": 2000, "elapsed_ms": 88},
{"phase": "rerank", "model": null, "elapsed_ms": 0},
{"phase": "graph_expand", "added_nodes": 14, "elapsed_ms": 22},
{"phase": "score", "weights": {"semantic": 0.4, "graph": 0.2, "recency": 0.15, "importance": 0.15, "confidence": 0.1}, "elapsed_ms": 4},
{"phase": "mmr", "lambda": 0.7, "elapsed_ms": 6}
],
"citations": {
"belief_01HX...": ["fact_01HX...", "fact_01HX...", "ep_01HX..."],
"fact_01HX...": ["evt_01HX..."],
"ep_01HX...": ["evt_01HX...", "evt_01HX...", "evt_01HX..."]
}
},
"diagnostics": {
"policy": {"tier_attribution": "scope", "capability": "scope.read.holistic"},
"filters_applied": ["actor.in", "confidence.gte"],
"scopes_traversed": ["org:acme/dept:eng/user:alice", "org:acme/dept:eng", "org:acme"],
"time_ms": {
"auth": 3, "policy_check": 1, "recall": 412, "rerank": 0,
"rank": 8, "render_context_block": 84, "total": 508
},
"_partial": false
}
}
9.1 Views
view | What gets filled |
|---|---|
raw | Only layers.events. Useful for debugging and replay. |
granular | All five layers[] arrays. context_block is empty. |
holistic (default) | All layers + a synthesized context_block. |
narrative | Empty layers; context_block contains an LLM-rendered story. Provenance preserved. |
structured | Layers empty; a single knowledge_subgraph field with JSON-LD shape. |
9.2 Ranking and limits
limits.per_layer caps each layer's array. limits.total_tokens triggers MMR-based knapsack across all layers (the same algorithm as our current recall pipeline). When token budget is binding, the response includes diagnostics.knapsack_evictions: N.
9.3 Citation map
provenance.citations is a directed acyclic graph from derived records back to events. Walking it gives the "why" trail for any item in the pack. The same data is reachable via /v1/beliefs/why for beliefs specifically.
10. Streaming
10.1 Granular streaming
Used for /v1/recall with stream=true (any view except narrative).
event: plan
data: {"plan": "DIRECT_LOOKUP_RECHECK", "filters_applied": [...]}
event: layer
data: {"layer": "facts", "items": [...], "elapsed_ms": 210}
event: layer
data: {"layer": "beliefs", "items": [...], "elapsed_ms": 315}
event: layer
data: {"layer": "episodes", "items": [...], "elapsed_ms": 488}
event: layer
data: {"layer": "understanding", "items": [...], "elapsed_ms": 612}
event: context_block
data: {"text": "Acme is a B2B SaaS in late-stage..."}
event: provenance
data: {"trail": [...], "citations": {...}}
event: diagnostics
data: {"time_ms": {...}, "scopes_traversed": [...]}
event: done
data: {"total_ms": 1042}
10.2 Narrative streaming
Used for /v1/answer with stream=true and /v1/recall with view=narrative&stream=true.
event: token
data: {"text": "Acme"}
event: token
data: {"text": " upgraded"}
event: token
data: {"text": " to 200"}
...
event: citations
data: [{"marker": "[1]", "layer": "fact", "id": "fact_01HX..."}, ...]
event: diagnostics
data: {"recall_ms": 412, "llm_ms": 814, "total_ms": 1240}
event: done
data: {}
10.3 Lifecycle streaming
Used for /v1/lifecycle/stream. Full event catalog in §13.
10.4 Reconnection and resume
Clients pass Last-Event-ID: lce_01HX... (standard SSE) or ?since_lifecycle_id=lce_01HX... on reconnect. The ID is a lifecycle event ID (lce_ prefix), not a memory event ID.
Server-side: every recall job's events are cached for 60s under the request's idempotency_key. Reconnect-with-resume replays from the next unsent event.
Lifecycle subscriptions support ?since_lifecycle_id=lce_01HX... for resume — server keeps a 1-hour replay buffer anchored from lifecycle-event arrival (§18.2 item 10). The window is "any lifecycle event whose server-side arrival timestamp is within the last hour is available for resume." Subscriber lifecycle (connect/disconnect timing) is irrelevant. Buffer is per-server-node; in cluster mode, replay across node failover may be incomplete for events that were in-flight during the failover.
10.5 Backpressure
We rely on HTTP/2 flow control. No application-level token bucket on streaming responses; we don't buffer events beyond the OS socket buffer.
If a client falls > 30s behind on a lifecycle stream, the connection is closed with event: lagging and they must reconnect with ?since=.
11. Forget Semantics
11.1 Cascade modes
| Mode | Endpoint | Capability | What happens |
|---|---|---|---|
derived_only | DELETE /v1/forget | forget.cascade.derived_only | Wipes named derived layers; events untouched. Rederivable from WAL replay. |
redact_events | DELETE /v1/forget | forget.cascade.redact_events | Blanks event payloads; keeps id, wal_offset, scope, actor (or redacts actor). Permanent. |
| GDPR | DELETE /v1/forget/gdpr | forget.gdpr | Reference-counted true erasure (§11.3). |
11.2 Selector semantics
The selector block on /v1/forget is conjunctive:
{
"about_entity": "ent_acme_corp",
"predicate": "is_likely_to_renew",
"memory_ids": [],
"valid_during": ["2026-04-01", "2026-05-01"],
"created_during": null
}
A record matches if ALL non-null filters match. Empty selector matches every record in the named layers within scope.
memory_ids is an "OR" with the other filters — if present, those IDs are deleted regardless of other filters (within the scope).
11.3 GDPR reference-counted erasure
Triggered by DELETE /v1/forget/gdpr. Five-phase pipeline.
Phase 1 — Enumerate. Walk WAL and find every event where author_actor matches the scope's owners OR home_scope ⊆ <scope>. Emit event: erasure_progress phase=enumerate.
Phase 2 — Refcount. For each candidate event E:
refcount(E)= count of cross-scope citations to E from scopes NOT under the GDPR scope.- Cross-scope citations come from: (a) derived records (facts/beliefs/episodes/understanding) in other scopes whose
supports[]contains E, (b) explicitcross_scope_referenceledger rows.
Phase 3 — Categorize.
delete_set= { E : refcount(E) == 0 }redact_set= { E : refcount(E) > 0 }
Phase 4 — Execute.
- For each E in
delete_set: delete WAL entry, compact downstream indexes, remove fromderivesback-pointers in surviving events. Truly gone. - For each E in
redact_set:- Replace
actorwithredacted:gdpr_<erasure_id> - Replace
contentwith{"kind": "redacted", "original_kind": "..."} - Keep
id,wal_offset,scope,context.observed_at,context.recorded_at - For each entry in this event's
derives[]that points to a record being deleted in the same erasure, replace the entry withredacted:<original_id>(e.g.,redacted:fact_01HX...). See §18.2 item 2. This preserves the audit signal that something was derived without leaking what. - Mark all surviving derived records citing this E with
supporting_evidence_redacted: true
- Replace
Phase 5 — Demote. Any belief whose supporting_evidence_redacted_fraction == 1.0 is demoted: confidence decays by confidence_after_full_redaction_factor from policy (default 0.5), _partial: true with _partial_reason: "supporting_evidence_redacted". If the resulting confidence falls below tenant.confidence_floor (default 0.3), the belief is deleted.
Audit. A single audit row with gdpr: true, erasure_id, and a summary {deleted_events, redacted_events, deleted_facts, demoted_beliefs} is written. GDPR audit rows are exempt from normal retention TTLs.
11.4 Cross-workspace propagation
When forget.gdpr.cross_workspace is enabled and the actor exercising erasure is a member of cross-workspace scopes:
- Workspace scopes containing references to Alice's events are scanned.
- For each: same refcount logic, but the "outside the GDPR scope" set is updated to exclude the workspace itself if Alice was its sole owner.
- Workspaces co-owned by other actors retain the redacted evidence; Alice-only workspaces are fully deleted.
11.5 What forget does NOT do
- Does not affect audit log entries (audit is the immutable history of policy decisions, not memory content).
- Does not delete tokens (revoke separately via
/v1/auth/revoke). - Does not propagate to imported source systems (you must delete from Mem0/Zep/etc separately).
12. Temporal Model
12.1 Time axes
Two independent axes, four endpoints per axis:
valid_from ─────────────────── valid_to
event time
recorded_from ─────────────── recorded_to
ingest time
| Field | Meaning |
|---|---|
valid_from | First moment the claim is true in the world |
valid_to | First moment it ceases to be true; null = open |
recorded_from | First moment CortexDB learned the claim |
recorded_to | First moment CortexDB superseded its own record; null = current |
Events have observed_at and recorded_at as singletons.
12.2 Temporal query primitive
The temporal block on any layer read or recall:
{
"temporal": {
"as_of": "2026-04-15T00:00:00Z",
"valid_during": ["2026-03-01", "2026-04-01"],
"recorded_during": null,
"natural": "last 30 days"
}
}
Semantics:
as_ofpins both axes.recorded_*≤as_of, andvalid_from≤as_of<valid_to. This answers "what did we believe at point T?"valid_duringfilters records whose[valid_from, valid_to)overlaps the range.recorded_*is pinned tonowunlessrecorded_duringis also set.recorded_duringfilters by ingest-time range only.naturalis parsed server-side and reduced tovalid_during. Supported phrases:"last N {days|weeks|months|years}","yesterday","this week","between X and Y","since X". Locale-aware. Anchored torequest_timeunlessreference_dateis also set.
12.3 As-of semantics
as_of defaults:
| Field | Default |
|---|---|
as_of | null (= now) |
Implicit recorded_to pin | now |
Implicit valid_from pin | as_of (or now) |
?include_superseded=true on layer reads disables the recorded_to=null filter, returning all historical versions.
12.4 Natural-language parsing
A small deterministic parser handles common phrases. Unparseable strings 422 with unparseable_temporal. Tenants can register custom phrase mappings:
POST /v1/temporal/phrases
{
"name": "fiscal_q3",
"pattern": "fiscal Q3",
"resolves_to": {"valid_during": ["2026-07-01", "2026-10-01"]}
}
13. Lifecycle Events Catalog
All events delivered via SSE on /v1/lifecycle/stream. Every event has event_id, timestamp, scope, and an event-specific payload.
| Event name | Fires when | Payload |
|---|---|---|
captured | WAL append succeeds | {event_id, actor, modality, wal_offset} |
extracted | Async extract stage done | {event_id, derived: {facts, entities, beliefs, episodes}} |
indexed | BM25 + HNSW insert done | {event_id, layers_indexed} |
consolidated | Belief / understanding revised | {event_id, beliefs_updated, conflicts_resolved, superseded_facts} |
compressed | Episode sealed / understanding versioned | {episode_id, understanding_ids} |
forgotten | Records deleted | {layer, count, by_actor, cascade} |
policy_changed | A policy mutation affected this stream | {tier, scope_or_actor, capability_diff} |
policy_revoked | The subscription is no longer allowed | {reason} (stream closes after) |
import_progress | Import job tick | {import_id, fraction_complete, processed, total} |
import_complete | Import job done | {import_id, summary} |
import_error | Import row failed | {import_id, line, error} |
export_progress | Export job tick | {export_id, fraction_complete} |
export_complete | Export job done | {export_id, url, size_bytes} |
erasure_progress | GDPR job tick | {erasure_id, phase, fraction_complete} |
erasure_complete | GDPR job done | {erasure_id, summary} |
lagging | Client too slow; stream closing | {behind_by} |
Filtering: ?events=captured,consolidated to subscribe to a subset.
14. Audit Log Schema
Every policy-gated call writes one row. Schema:
{
"id": "audit_01HX...",
"ts": "2026-05-13T15:42:09.018Z",
"actor": "agent:planner_v3",
"token_jti": "...",
"tenant_id": "acme",
"scope": "org:acme/dept:eng/user:alice",
"endpoint": "POST /v1/experience",
"capability": "scope.write.elevated",
"decision": "allow",
"decided_by_tier": "scope",
"request": {
"method": "POST",
"path": "/v1/experience",
"body_hash": "sha256:abc...",
"headers_hash": "sha256:def...",
"body_bytes": 4823
},
"response_status": 202,
"event_id": "evt_01HX...",
"client_ip": "1.2.3.4",
"user_agent": "cortex-py/1.0.0",
"request_id": "req_01HX...",
"elapsed_ms": 12,
"gdpr": false
}
Hashes:
body_hash = sha256(body || tenant_pepper)— per-tenant pepper prevents cross-tenant rainbow attacksheaders_hash = sha256(canonicalized_headers_minus_Authorization || tenant_pepper)
tenant_pepper_version is rotated quarterly. Old peppers are retained for audit.retention + audit.pepper_retention_buffer (default P3M buffer) so any audit row hashed under that pepper remains verifiable for the duration of its retention window plus the safety margin. See §18.2 item 11.
Body bytes storage. The body_bytes field is controlled by tenant setting audit.store_body_bytes (default true; see §18.2 item 15). When false, the audit row omits the field but retains the hash.
Retention:
- Default 2 years.
gdpr: truerows: never deleted.- Configurable per tenant: 90 days minimum, 7 years maximum.
Storage: Audit rows are appended to a dedicated RocksDB column family + mirrored to Tantivy for search. Not indexed in HNSW; not part of the recall pipeline.
15. Importer Mappings
15.1 Mem0
| Mem0 field | CortexDB target | Notes |
|---|---|---|
id | event.context.labels["mem0_source_id"] | Preserved for round-trip |
memory (text) | event.content.text | Modality = imported |
hash | event.context.labels["mem0_hash"] | |
metadata | event.context.labels["mem0_metadata"] | Flattened |
categories | Mapped to Understanding concept tags | Configurable via options.map_categories_to |
score | Discarded | Not a stored field |
immutable | directives.no_supersede = true | |
expiration_date | directives.ttl_for_event_layer | |
created_at | event.context.recorded_at | And valid_from if no event-time info |
updated_at | New event with preceded_by link | Each update becomes a new event |
user_id | {user_id} template variable | Substituted into scope_template |
agent_id | event.actor.id (as agent:) | |
run_id | event.actor.session | |
app_id | {app_id} template variable |
Graph memory (legacy v1 exports only): edges become Facts with predicate= the edge label.
15.2 Zep
| Zep field | CortexDB target | Notes |
|---|---|---|
Message | event.content with kind=message | role and name preserved |
| Fact edge | Fact | Bi-temporal preserved: valid_at→valid_from, invalid_at→valid_to |
source_node_uuid / target_node_uuid | subject / object | Entity references resolved via Zep's node table |
episodes[] | event.context.preceded_by | |
| Episode | Episode | Direct mapping |
Group (group_id) | Scope (ws:-prefixed by default) | |
| Custom entity types | Vocabulary + Understanding concepts | |
min_fact_rating per fact | fact.confidence |
15.3 Letta
| Letta field | CortexDB target | Notes |
|---|---|---|
| Memory block | Understanding concept | Configurable: options.map_core_blocks_to |
| Archival passage | Event (modality=imported) | Configurable: options.map_archival_to |
| Conversation history | Events with kind=message | One per message |
Agent file (.af) | Bundle import: scope register + blocks + archival | Full state snapshot |
persona block | Concept tagged actor_self_description | |
human block | Concept tagged actor_subject_description |
15.4 OpenAI memory export
Format: ChatGPT's JSON dump.
| OpenAI field | CortexDB target |
|---|---|
memories[].text | event.content.text |
memories[].created | event.context.recorded_at |
memories[].source.session_id | event.actor.session |
15.5 Generic JSONL
Each line MUST be a full experience envelope. No mapping needed.
{"scope":"...", "actor":{...}, "modality":"...", "content":{...}, "context":{...}}
{"scope":"...", "actor":{...}, "modality":"...", "content":{...}, "context":{...}}
16. Error Catalog
Selected entries. Full list maintained at docs/errors/.
| HTTP | Code | Meaning | Retriable |
|---|---|---|---|
| 400 | INVALID_BODY | JSON parse error | no |
| 400 | MISSING_REQUIRED_FIELD | Required field absent | no |
| 401 | MISSING_TOKEN | No Authorization header | no |
| 401 | INVALID_TOKEN_SIGNATURE | Signature verification failed | no |
| 401 | EXPIRED_TOKEN | exp < now | no |
| 401 | REVOKED_TOKEN | jti on revocation list | no |
| 401 | ACTOR_MISMATCH | X-Cortex-Actor ≠ token sub | no |
| 401 | WRONG_TENANT | aud doesn't match deployment | no |
| 403 | POLICY_DENIED | Effective capability missing | no |
| 403 | NOT_A_MEMBER | Writing to registered scope without membership | no |
| 404 | NOT_FOUND | Resource absent (and caller has read access) | no |
| 409 | IDEMPOTENCY_CONFLICT | Key reused with different body | no |
| 409 | SCOPE_REGISTRATION_EXISTS | Path already registered | no |
| 410 | RESOURCE_DELETED | Forgotten record | no |
| 422 | INVALID_SCOPE_GRAMMAR | Scope path malformed | no |
| 422 | INVALID_TIMESTAMP | RFC3339 parse failure | no |
| 422 | UNPARSEABLE_TEMPORAL | temporal.natural not understood | no |
| 422 | INVALID_ENVELOPE | Experience envelope failed validation | no |
| 429 | RATE_LIMITED | Token bucket exhausted | yes (after Retry-After) |
| 500 | INTERNAL | Unexpected server error | yes |
| 503 | WAL_UNAVAILABLE | Cannot accept writes | yes |
| 503 | DERIVED_LAYER_UNAVAILABLE | Recall succeeds but one layer is degraded; partial response | sometimes |
17. Compatibility Matrix
Framing. Peer systems are not flat. Mem0 has conversation/session/user/org scopes with filter/rerank operators; Zep ships bi-temporal facts on graph edges plus context blocks plus semantic/full-text/graph retrieval; Letta has tiered memory tools and shared blocks; Cognee has 14 search strategies. CortexDB's differentiation is not "they're simple, we're rich." It is event-sourced provenance, all-layer first-class access, policy with attribution, lifecycle observability, and reference-counted erasure — the rows below reflect that re-framing. We also benchmark our ergonomics against Supermemory's "raw content in +
customId+ tags" simple-SDK happy path: CortexDB keeps the rich contract, but the SDK MUST offer a one-liner experience for the happy path.
| Capability | CortexDB | Mem0 | Zep | Letta | Cognee | Supermemory |
|---|---|---|---|---|---|---|
| Memory layers separately addressable as API resources | ✅ 5 | ⚠️ 1 (filterable) | ✅ 2 (facts, episodes) | ⚠️ 3 (block tiers) | ⚠️ 2 (graph, vector) | ⚠️ 1 (chunks/memories) |
| Bi-temporal across ALL layers (valid_+recorded_) | ✅ | ❌ | ⚠️ facts/edges only | ❌ | ⚠️ via Graphiti episodes | ❌ |
as_of point-in-time recall on any layer | ✅ | ⚠️ via filters | ⚠️ facts only | ❌ | ⚠️ episode-anchored | ❌ |
| Hierarchical scope with inheritance | ✅ | ⚠️ org/project/user/agent/run (no inherit) | ⚠️ user/group/thread (flat) | ⚠️ project-only | ⚠️ datasets + node_set tags | ⚠️ container tags |
| Workspace = scope (unified primitive) | ✅ | ❌ scoping is intersection-only | ⚠️ groups separate | ⚠️ shared blocks separate | ⚠️ dataset is shared | ❌ |
| Per-tier authorization with capability attribution | ✅ four-tier | ❌ API-key flat | ❌ API-key flat | ❌ API-key flat | ❌ | ❌ |
| Signed-token identity (caller / observed_actor / subject split) | ✅ | ❌ | ❌ | ❌ | ⚠️ user param | ❌ |
| Async-first writes with lifecycle visibility | ✅ + SSE | ✅ async; ❌ no lifecycle | ❌ | ❌ | ✅; ❌ no lifecycle | ✅ queued |
| SSE / push lifecycle stream (capture→consolidate observability) | ✅ | ❌ | ❌ | ⚠️ agent step stream | ❌ | ❌ |
| Hard delete (no soft delete in user-visible API) | ✅ | ⚠️ delete by id | ⚠️ delete by id | ⚠️ block edit | ⚠️ dataset delete | ⚠️ |
| Reference-counted erasure preserving co-owned evidence | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Erasure preview + manifest before destructive run | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Streaming recall (granular per-layer) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Streaming recall (narrative token stream) | ✅ | ❌ | ❌ | ⚠️ agent reply | ❌ | ❌ |
| Provenance + citation trail returned in every recall | ✅ | ❌ | ⚠️ episodes/edges only | ❌ | ⚠️ graph trace | ❌ |
| "Why do you think that?" endpoint (explainability primitive) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Layer-aware forget (forget just beliefs / just facts) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Bulk importer from peer systems | ✅ Mem0/Zep/Letta/OpenAI/JSONL | n/a | ❌ | ❌ | ❌ | ❌ |
| Bulk export to peer systems | ✅ | ⚠️ memory export | ❌ | ⚠️ Agent File (.af) | ❌ | ❌ |
| Custom predicate / relation vocabularies | ✅ | ⚠️ custom categories | ✅ Pydantic ontology (10/10/10) | ❌ | ✅ Pydantic graph_model | ❌ |
| Multimodal blob attachment | ✅ | ⚠️ partial | ❌ | ❌ | ⚠️ partial | ✅ files/URLs |
| Auto-registered scopes with prune tooling | ✅ | n/a | n/a | n/a | n/a | n/a |
| Audit log with tamper-evident peppered hashes | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Simple-SDK happy-path one-liner (raw text in, ID out) | ✅ via client.experience(text=...) | ✅ | ✅ | ⚠️ verbose | ⚠️ verbose | ✅ very ergonomic |
(⚠️ = partial / different shape; ❌ = absent; ✅ = present)
Reading the matrix. The right way to look at CortexDB vs the field is not "do they have it" but "is it observable, addressable, and explainable through the API." Mem0 has reranking — but you cannot ask why a result ranked where it did. Zep has bi-temporal facts — but only on facts. Letta has shared memory — but no policy. Our bar: every claim has a trail, every trail has an endpoint, every endpoint has a stability tier.
18. Deferred Decisions & Open Ambiguities
18.1 Deferred (intentionally out of v1)
- Federation / multi-region replication API. Cluster mode exists internally; the API contract for cross-region writes is deferred until we have a paying customer that needs it.
- Differential privacy on recall. Mentioned by enterprise prospects; not standard enough to commit a shape.
- Vector store BYO. Letting customers point at their own pgvector / Pinecone. Out of v1 scope.
- Memory editing tool surface for agents. Letta's tool-callable memory editing is interesting but conflicts with our "events are immutable" stance. Could ship as a higher-level shim that maps tool calls to
experience+forget; deferred. - Webhook (push) lifecycle in addition to SSE. SSE covers most use cases; webhooks add infra (retries, signing). Deferred until requested.
- GraphQL surface. REST + SDK feels sufficient; GraphQL adds maintenance burden. Deferred.
18.2 Resolved Ambiguities (formerly "Open before Implementation")
All 22 items from the prior round of disambiguation have been resolved on 2026-05-14. Each item below states the canonical resolution that the schemas and prose sections of this document have been updated to reflect. These are normative — if the schemas or prose disagree, the schemas win; if both disagree with this section, this section is authoritative for design intent.
The Stable surface gating list at the bottom of the prior version is cleared. Phase 1 of §19 is unblocked.
-
as_of×include_supersededinteraction (Stable: §8.7.3 facts). Resolved: both filters apply independently and conjunctively.as_ofclips both axes (recorded_*≤as_of,valid_from≤as_of<valid_to).include_supersededtoggles whether therecorded_to=nullconstraint is enforced — when true, superseded versions whoserecorded_to ≤ as_ofare returned alongside the current versions. Conformance: schemafact.schema.jsondeclares both query params; an integration test pair exists in §12 examples. -
Orphan
derives[]back-pointers after GDPR redaction (Experimental: §11.3). Resolved: option (c). When a derived record is deleted by GDPR and its source event is redacted-but-kept, the event'sderives[]entries pointing at the deleted record are replaced withredacted:<original_id>(e.g.,redacted:fact_01HX...). This preserves the audit signal that something was derived without leaking what. Conformance:event.schema.jsonderivesentries match pattern^(evt_|ep_|fact_|belief_|concept_|redacted:[a-z]+_)[A-Za-z0-9_-]+$. -
Lifecycle event ordering within a single
event_id(Stable: §8.9, §13). Resolved: strict ordering within one memory event. The stages for any singleevt_<id>are emitted in fixed order:captured→extracted→indexed→consolidated→ (optionally)compressed. No cross-event ordering guarantee. Conformance: SSE delivery has a per-event sequence number; an SSE conformance test validates monotonicity within event_id. -
view=descendwith partial read access (Stable: §8.3 recall). Resolved: option (b) — opaque placeholders with count-only stats. When admin descends into a tree containing child scopes the caller cannot read, the response'sscopes_traversedincludes{scope, denied: true, item_count: N, denied_reason}entries. Counts reveal existence without leaking content;denied_reasoncites the policy tier. Conformance:stratified_pack.schema.jsonscopes_traversed[]item is a discriminated union over{scope, items}and{scope, denied, item_count, denied_reason}. -
"Natural level" for
scope.write.elevated(Stable: §5.2). Resolved: the deepest scope path in which the caller appears as an explicitownerorwritermember. Writes to any scope at or below this depth do not requirescope.write.elevated; writes to ancestor scopes do. For unregistered scopes (no membership), the caller's natural level isnulland any write requires the capability. Conformance:capability_set.schema.jsonincludesnatural_level: scope-path | null. -
Bulk import rate-limit attribution (Beta: §8.11). Resolved: synthetic per-tenant bucket
service:importer_<tenant_id>, with default capacity = 10× therate_limit.writevalue from the deployment preset. Importer-job submissions consume from this bucket; the originating caller's personal bucket is not charged. Override via tenant policyrate_limit.importer. Conformance:policy.schema.jsonallowsrate_limit.importeroverride. -
Cancellation of in-flight async writes (Stable: §8.1). Resolved: ship
POST /v1/lifecycle/memory-event/{evt_id}/cancel. Drops queued stages (extract, index, consolidate, compress). The WAL capture itself cannot be reversed (it has already happened). The cancellation marks the event asexcluded_from_recall: true; it remains queryable directly viaGET /v1/events/{evt_id}but does not surface in/v1/recallor/v1/answer. Endpoint added to §8.9; see §8.9.5 below. -
_partial: trueoutside Understanding (Beta: §7.4 Belief). Resolved: the_partial/_partial_reason/_progresstriple is promoted to every derived layer (Episode, Fact, Belief, Understanding). Events never carry these fields. Each layer's enum of_partial_reasonvalues is its own (e.g., Belief usesrevision_in_progress,supporting_evidence_redacted,calibration_stale; Understanding usesconsolidation_in_progress,insufficient_supporting_facts, etc.). Conformance: each layer schema declares the fields and its specific reason enum. -
idempotency_keycollision scope (Stable: §6.3). Resolved: scope is(caller, endpoint_family, key). Endpoint family = the path prefix up to (but not including) the first parameter or sub-resource — e.g.,/v1/experienceand/v1/experience/bulkare separate families;/v1/import/mem0and/v1/import/zepare separate;/v1/erasuresand/v1/erasures/previeware separate. This prevents a key reused across importers from being interpreted as a replay. -
SSE replay buffer window (Stable: §10.4). Resolved: anchored from lifecycle-event arrival at the server. The 1-hour window means "any lifecycle event whose server-side arrival timestamp is within the last hour is available for resume via
?since_lifecycle_id=." Subscriber lifecycle (when they connected, when they disconnected) is irrelevant. Buffer is per-server-node; in cluster mode, replay across node failover may be incomplete for events that were in-flight during the failover. -
Tenant pepper rotation (Stable: §14). Resolved: peppers rotate quarterly. Old peppers are retained for
audit.retention+ 1 quarter — i.e., for as long as any audit row hashed under that pepper might still be present (plus a one-quarter safety margin for verification). Whenaudit.retention = P2Y, peppers persist for ~2.25 years. Conformance:policy.schema.jsondeployment-levelaudit.pepper_rotation_period(defaultP3M) andaudit.pepper_retention_buffer(defaultP3M). -
directives.consolidate_intocross-scope write (Stable: §3.5, §8.1). Resolved: dual capability required —scope.writeon both the write scope and the consolidation target. No implicit elevation. If the caller lacksscope.writeon the consolidation target, the envelope is rejected at validation time (422INVALID_ENVELOPE,details.field = "directives.consolidate_into"). -
Empty-selector forget (Stable: §8.5). Resolved:
POST /v1/forgetwith emptyselector(all filter fields null/empty ANDmemory_ids: []) requiresconfirm_all: true. Without it, 422EMPTY_SELECTOR_WITHOUT_CONFIRMATION. Schema validation rule:selectoris "empty" iffselector.about_subject ∈ {null, ""} ∧ selector.about_entity ∈ {null, ""} ∧ selector.predicate ∈ {null, ""} ∧ selector.memory_ids = [] ∧ selector.valid_during = null ∧ selector.recorded_during = null. -
Default
viewon layer reads (Stable: §8.7). Resolved: default islocalfor allGET /v1/events|episodes|facts|beliefs|understanding. Principle of least surprise: a caller passing a scope gets exactly that scope. To aggregate up the tree, they must passview=holisticexplicitly. The/v1/recallendpoint is the only one whose default isholistic(rationale: recall is intent-aware; layer reads are not). -
Audit row
body_bytes(Stable: §14). Resolved: add tenant-level settingaudit.store_body_bytes(boolean), defaulting totrue. Whenfalse, the audit row omits the field. Preset defaults:on_prem_enterprise = true,cloud_shared_saas = true(overridable per tenant),cloud_private = true,dev_local = true. Conformance:audit_row.schema.jsonmakesbody_bytesoptional. -
Belief deletion threshold behavior (Beta: §7.4). Resolved: confirmed as documented. When
confidence < tenant.confidence_floor(default 0.3), the belief record is deleted (not soft-deleted, no tombstone). Direct ID lookup atGET /v1/beliefs/{id}returns410 RESOURCE_DELETED. List endpoints (GET /v1/beliefs?...) silently omit deleted beliefs. Theaudit_idof the deletion is recorded in the audit log. -
view=rawand temporal filters (Stable: §8.3, §9). Resolved: forview=raw(events-only response),recorded_duringandas_ofapply (torecorded_aton events);valid_duringis ignored with no error (events have no validity interval, so the filter is a no-op). Avalid_duringfilter present in aview=rawrequest causesX-Cortex-Warning: valid_during_ignored_for_view_rawto be set on the response. -
Multimodal embedding strategy (Stable: §3.5). Resolved: v1 ships text-only embeddings. Image and other binary blobs are indexed by metadata (content-type, size, perceptual hash) and reachable via blob_id, but not embedded into the semantic vector space. Multimodal embedding is a v1.x add-on; the API contract is forward-compatible (envelope
content.media[]is already structured to accept additional indexing hints). -
Capability
scope.create.*granularity (Stable: §5.2). Resolved: the enumeratedscope.create.<type>set is closed at the deployment policy tier. The default set is{org, user, agent, service, ws, project, team, dept, global, system}. Operators may extend the set via the deployment YAML'sallowed_scope_types. Any user-defined scope type not in the allowed set is rejected at scope registration time with422 UNREGISTERED_SCOPE_TYPE. A single capabilityscope.create.customcontrols whether custom (non-builtin) types may be registered at all. -
use_pack_idlifetime in/v1/answer(Stable: §8.4). Resolved: 60 seconds from pack creation, no extension on reuse. Consistency over caching efficiency. After 60s the pack ID is invalid; the caller must re-run recall. -
pack_idand stateless recall (Stable: §8.3, §8.4). Resolved: ephemeral, 60s server-side TTL, not a stable client handle. Clients that need durable references should extract and store the constituentevt_/fact_/belief_/...IDs fromprovenance.citationsimmediately after receiving the pack. -
Error envelope casing consistency (Stable: §6.6, §16). Resolved 2026-05-14: unified on
error_code(UPPER_SNAKE) as the single error identifier field; lower-snakeerrorfield retired. See §6.6. Normative source:error_envelope.schema.json.
Resolution discipline going forward. Any future contract change that is non-additive against a Stable endpoint requires:
- An entry in this section documenting the prior behavior, the new behavior, and the rationale.
- A schema update in
docs/schemas/. - An updated example in the affected endpoint section.
- A regression test under
tests/api_contract/. - A bump of
X-Cortex-Stabilitytostable-revisingfor one minor release before the change becomes default.
19. Implementation Order
Updated 2026-05-14 to match the grounded gap analysis (
docs/GAP_ANALYSIS.md). The earlier numbered plan was theoretical; this version is informed by what actually exists in the codebase. Each phase is shippable in isolation; phases 1-3 are prerequisite (they reshape the contract). Estimated cumulative time: 10-14 weeks at full focus; the agent's gap-analysis estimate of 19 weeks is the conservative upper bound.
Phase 1 — Foundation (Weeks 1-2)
Goal: every request authenticates, identifies three actors, names a scope, and gets a policy decision with attribution.
- PASETO v4 public + JWT (RS256, ES256 only) token verifier
- Scope path parser + canonical form + grammar enforcement (ABNF in Appendix B)
- Auto-registration on first write
- Three-identity extraction (caller from token; observed_actor and subject from envelope)
- Four-tier policy evaluator skeleton (deployment + tenant tiers wired up; scope and actor tiers stubbed for phase 7)
X-Cortex-Policyheader on every response withtier,decision,capability- Deployment-preset loader (reads
crates/cortexdb/presets/*.yaml) - Capability catalog enum + intersection logic
GET /v1/policy/effective,GET /v1/policy/deploymentGET /v1/auth/whoami,POST /v1/auth/revoke
Phase 2 — Core Memory Layers (Weeks 3-4)
Goal: every layer has its full record shape in storage, even if the layer's full pipeline isn't running yet.
- Facts: triple storage
(subject, predicate, object), bi-temporal (valid_from/to,recorded_from/to), supersession chain, confidence + extractor identifier - Beliefs: stance enum,
confidence_interval,calibration, weighted supports with polarity,revision_policy,_partialreasons - Understanding: concept nodes, versioning, edges,
derived_from/canonical_idfor split/merge, staleness + coverage scores (synthesizer stub returns_partial: truewith_partial_reason: no_implementation_yet) - Lifecycle event types (5 stages: Capture, Extract, Index, Reconcile, Compress) defined in
cortex-types _partialtriple promoted to every derived layer (per §18.2 item 8)
Phase 3 — Async Write Pipeline (Week 5)
Goal: write path returns 202 on capture, derives asynchronously, exposes wait modes.
POST /v1/experienceaccepts the new envelope shape (three identities, scope path, observed_at, directives, idempotency_key)- WAL append becomes the synchronous part; everything else queues to background workers
wait=captured|indexed|consolidatedmodesPOST /v1/experience/bulk(single + bulk paths share the worker pool)- Idempotency deduplication (key scope:
(caller, endpoint_family, idempotency_key), §18.2 item 9) directives.consolidate_intocross-scope write validation (dual capability check, §18.2 item 12)
Phase 4 — Stratified Recall (Week 6)
Goal: recall returns a stratified pack, not a flat context string.
POST /v1/recallreturns{pack_id, context_block, layers, provenance, diagnostics}- View modes:
raw,granular,holistic,structured(narrative deferred to phase 13) - Per-layer budgets +
exclude_content+citation_mode temporalblock parser includingas_of,valid_during,recorded_during,natural- Provenance trail (every phase, every elapsed_ms, every filter applied)
provenance.citationsmap (derived-record → events trail)pack_id60s TTL cache for/v1/answer use_pack_idreuse
Phase 5 — Lifecycle + SSE (Week 7)
Goal: the async pipeline is observable in real time.
- Lifecycle event log (new RocksDB column family or dedicated event store)
lce_ID namespaceGET /v1/lifecycle/stream(SSE) withsince_lifecycle_idresume, 1h server-side buffer (§18.2 item 10)GET /v1/lifecycle/event/{lce_id},GET /v1/lifecycle/memory-event/{evt_id}, paginated catch-upPOST /v1/lifecycle/memory-event/{evt_id}/cancel(drops queued stages, §8.9.5)- Emit lifecycle events at every pipeline stage hand-off
Phase 6 — Layer Reads (Week 8)
Goal: every layer has its own GET endpoint.
GET /v1/events(withderives[], labels filtering, defaultview=local)GET /v1/episodes(withcausal_chaintagging)GET /v1/facts,GET /v1/facts/timeline(withas_of×include_supersededsemantics, §18.2 item 1)GET /v1/beliefs,GET /v1/beliefs/why(with the full support graph + narrative)GET /v1/understanding,/{id},/{id}/related,/coverage(all returning_partial: trueuntil phase 11)
Phase 7 — Scopes + ACL (Week 9)
Goal: scope registration, member ACLs, holistic reads with denied placeholders.
POST /v1/scopes,GET /v1/scopes?path=...,PUT /v1/scopes/members?path=...,DELETE /v1/scopes?path=...- Scope tier of the policy stack wired up (members, roles, inheritance)
- Holistic read traversal (
view=holisticwalks up;view=descendwalks down with denied placeholders, §18.2 item 4) 403 NOT_A_MEMBERdenials with tier attribution- Auto-provisioned vs explicitly-registered tracking
Phase 8 — Forget + Erasures (Weeks 10-11)
Goal: hard delete works in proportional + reference-counted modes.
POST /v1/forgetwithderived_only+redact_eventscascades- Selector grammar (
about_subject,about_entity,predicate,memory_ids,valid_during,recorded_during) confirm_allflag for empty-selector forget (§18.2 item 13)POST /v1/erasures/preview(refcount breakdown, cross-scope propagation plan, manifest)GET /v1/erasures/preview/{preview_id}/manifestPOST /v1/erasures(executes),GET /v1/erasures/{id},POST /v1/erasures/{id}/cancel- Reference-counted erasure pipeline (§11.3): enumerate → refcount → categorize → delete/redact → demote
- Belief demotion logic when supporting evidence is fully redacted
- Orphan
derives[]replacement withredacted:<id>markers (§18.2 item 2)
Phase 9 — Audit + Compliance (Week 12)
Goal: every policy decision is tamper-evidently logged.
- Audit log writer (RocksDB CF + Tantivy mirror)
- Per-tenant pepper rotation (P3M default, retention buffer P3M, §18.2 item 11)
- Body + headers hash (
sha256(body || pepper)) audit.store_body_bytestenant setting (§18.2 item 15)GET /v1/audit,GET /v1/audit/{audit_id},POST /v1/audit/verifyaudit.read.cross_actorcapability enforcement
Phase 10 — Imports + Exports (Weeks 13-14)
Goal: customers can move data in and out.
POST /v1/import/jsonlfirst (smallest; generic envelope ingest)POST /v1/import/mem0(mapping per §15.1)POST /v1/import/zep(preserves bi-temporal per §15.2)POST /v1/import/letta(Agent File.afper §15.3)POST /v1/import/openai(ChatGPT export per §15.4)- Order modes (
strict_temporalvsbatch_throughput) - Per-tenant importer rate-limit bucket (§18.2 item 6)
POST /v1/exportfor each format, async destination routing (URL or inline)- Bidirectional round-trip tests
Phase 11 — Understanding Synthesizer (Weeks 15-16)
Goal: drop _partial: true on the Understanding layer.
- Real concept synthesis pipeline (consolidation stage)
- Coalescence, splitting, version chaining
- Coverage + staleness scoring jobs
POST /v1/understanding/synthesize(admin-triggered re-synthesis)GET /v1/understanding/coverage- Vocabularies for relation labels (
vocabulary.read/writecapabilities)
Phase 12 — Vocabularies + Temporal Phrases + Blobs (Week 17)
POST/GET/PUT /v1/vocabularies/...POST/GET/DELETE /v1/temporal/phrases/...POST /v1/blobs,GET /v1/blobs/{blob_id}(multipart upload, text-only embedding for v1)POST /v1/scopes/prunecleanup automation
Phase 13 — Streaming + Narrative (Weeks 18-19)
Goal: streaming surfaces for the experimental tier.
- Streaming recall (
POST /v1/recall stream=true) — granular per-layer SSE - Streaming answer (
POST /v1/answer stream=true) — token-by-token - Narrative view (
view=narrative) use_pack_idanswer reuse (60s TTL)
Risk gates between phases
| After phase | Required to proceed |
|---|---|
| 1 | All four presets load without error; cortex-auth-ref mints a token that the binary accepts |
| 2 | Schemas validate the new record shapes against docs/schemas/*.json |
| 3 | LongMemEval-S and LoCoMo benchmarks pass through the new write/recall API at the regression bar (§19.1) |
| 4 | Recall regression bar holds |
| 8 | Erasure preview/execute pair validated against the manifest schema; legal sign-off on redaction semantics |
| 11 | Understanding endpoint can return _partial: false for at least one real concept |
19.1 Benchmark Regression Bar
Concrete thresholds for any PR that touches the read or write pipeline (and therefore could regress retrieval quality).
LongMemEval-S (full 500 questions):
- Floor: 92.5% (≥462/500 correct)
- Shipped baseline: 93.8% (commit
83ca464, 2026-05-08) - Tolerance: 1.3 percentage points
- Bench command:
benchmarks/longmemeval/reproduce.sh - CI gate: PR may not merge if the bench reports below the floor.
LoCoMo (categories 1-4, judge score):
- Floor: 83.5%
- Recovered baseline: 85.5% (2026-05-11)
- Tolerance: 2.0 percentage points
- Bench command:
python benchmarks/locomo/run_locomo.pythenllm_judge_memori.py - CI gate: Same.
Recovery PRs. A PR may merge below the floor if and only if it is explicitly tagged regression-fix and the previous (last-green) commit is identified. The next non-fix PR must restore the bar.
Latency budgets (informational, not gating for v1):
- Recall p50 ≤ 3500ms (currently ~2882ms; we have headroom)
- Answer p50 ≤ 8500ms end-to-end
- Capture (sync portion of
/v1/experience) p50 ≤ 50ms
These are tracked in docs/LATENCY_BASELINE.md; deviations beyond 20% trigger a perf-review checkbox on the PR.
Cost budget:
- Per-question LongMemEval-S cost ≤ $0.10 (currently ~$0.10 at 93.8%; this is a hard cap)
- Recovery PRs may temporarily exceed; restore within two PRs.
What is NOT in the bar. Code-correctness tests, API contract tests, and unit tests live separately and are not part of this gate — they are independent merge requirements. The regression bar is specifically about retrieval and answer quality.
20. Appendices
Appendix A: Capability Reference (Full)
Defaults per preset. Y = allowed, N = denied, ~ = configurable per tenant default.
| Capability | on_prem | cloud_shared | cloud_private | dev_local |
|---|---|---|---|---|
scope.create.global | Y | N | Y | Y |
scope.create.cross_tenant | Y | N | N | Y |
scope.create.org | Y | Y | Y | Y |
scope.create.user | Y | Y | Y | Y |
scope.create.agent | Y | Y | Y | Y |
scope.create.ws | Y | Y | Y | Y |
scope.write | Y | Y | Y | Y |
scope.write.elevated | Y | ~ | Y | Y |
scope.write.on_behalf_of | Y | ~ | Y | Y |
scope.write.about_other | Y | ~ | Y | Y |
scope.create.custom | Y | N | Y | Y |
scope.read.local | Y | Y | Y | Y |
scope.read.holistic | Y | Y | Y | Y |
scope.read.descend | Y | ~ | Y | Y |
scope.read.cross_tenant | Y | N | N | Y |
understanding.read | Y | Y | Y | Y |
understanding.read.cross_scope | Y | N | Y | Y |
understanding.synthesize | Y | Y | Y | Y |
forget.cascade.derived_only | Y | Y | Y | Y |
forget.cascade.redact_events | Y | Y | Y | Y |
forget.gdpr | Y | Y | Y | Y |
forget.gdpr.cross_workspace | Y | Y | Y | Y |
import.from.mem0 | Y | Y | Y | Y |
import.from.zep | Y | Y | Y | Y |
import.from.letta | Y | Y | Y | Y |
import.from.openai | Y | Y | Y | Y |
import.from.jsonl | Y | Y | Y | Y |
export.format.mem0 | Y | Y | Y | Y |
export.format.zep | Y | Y | Y | Y |
export.format.jsonl | Y | Y | Y | Y |
lifecycle.subscribe | Y | Y | Y | Y |
llm.invoke | Y | Y | Y | Y |
diagnostics.read | Y | ~ | Y | Y |
audit.read | Y | Y | Y | Y |
audit.read.cross_actor | Y | N | Y | Y |
auth.revoke | Y | Y | Y | Y |
temporal.phrases.read | Y | Y | Y | Y |
temporal.phrases.write | Y | Y | Y | Y |
vocabulary.read | Y | Y | Y | Y |
vocabulary.write | Y | Y | Y | Y |
policy.administer.deployment | Y(op) | Y(op) | Y(op) | Y |
policy.administer.tenant | Y | Y | Y | Y |
policy.administer.scope | Y | Y | Y | Y |
policy.administer.actor | Y | Y | Y | Y |
blob.upload | Y | Y | Y | Y |
blob.read | Y | Y | Y | Y |
admin.flush | Y(op) | Y(op) | Y(op) | Y |
admin.compact | Y(op) | Y(op) | Y(op) | Y |
admin.health | Y | Y | Y | Y |
Y(op) = operator-only (deployment-tier admin).
Appendix B: Scope Path Grammar (ABNF)
scope = segment *( "/" segment )
segment = type ":" id
type = ALPHA *( lowercase / DIGIT / "_" )
id = 1*( ALPHA / DIGIT / "_" / "-" )
ALPHA = %x61-7A ; a-z
lowercase = %x61-7A
DIGIT = %x30-39
Constraints:
- 1 ≤ segments ≤ 32
- 1 ≤ total length ≤ 4096
type≤ 32 chars;id≤ 128 chars
Appendix C: Sample Token (PASETO v4 public)
Decoded payload:
{
"iss": "https://auth.acme.com/cortexdb",
"sub": "agent:planner_v3",
"aud": "cortexdb:tenant:acme",
"exp": 1715836800,
"iat": 1715750400,
"jti": "01HX...",
"deployment": "cloud_shared_saas",
"caps": [
"scope.write",
"scope.read.local",
"scope.read.holistic",
"lifecycle.subscribe"
],
"scopes": [
"scope.write:org:acme/dept:eng/*",
"scope.read.holistic:org:acme/*"
]
}
Appendix D: JSON Schemas (Normative)
Status: normative. The schemas in docs/schemas/ define the contract. Where this prose spec disagrees with a schema, the schema wins. SDK code generators, request validation middleware, and server-side validators all run against these files. Spec text is explanatory; schemas are authoritative.
Schemas (JSON Schema draft 2020-12):
| File | Defines |
|---|---|
experience_envelope.schema.json | The POST /v1/experience request body |
event.schema.json | Event records (§7.1) |
episode.schema.json | Episode records (§7.2) |
fact.schema.json | Fact records (§7.3) |
belief.schema.json | Belief records (§7.4) including stance, confidence_interval, calibration, revision_policy |
concept.schema.json | Understanding concept records (§7.5) including identity/versioning/staleness fields |
scope.schema.json | Scope registration + policy block |
policy.schema.json | Deployment / tenant / scope / actor policy documents |
audit_row.schema.json | Audit log row (§14) |
stratified_pack.schema.json | The POST /v1/recall response |
answer_response.schema.json | The POST /v1/answer response (subset of pack + LLM block) |
lifecycle_event.schema.json | Each SSE event payload, discriminated by event name |
erasure_preview.schema.json | Preview / manifest response shape |
erasure_job.schema.json | Erasure execution / status / cancel responses |
token_claims.schema.json | Required and optional JWT/PASETO claims |
temporal_block.schema.json | The temporal block shared by recall / answer / layer reads |
capability_set.schema.json | Effective capability response from GET /v1/policy/effective |
Change discipline. A schema change accompanies every breaking change in this doc, in the same PR. CI fails if the schemas don't validate the examples in this spec.
Appendix E: SDK shape (illustrative, Python)
client = CortexClient(
base_url="https://api-v1.cortexdb.ai",
token_provider=OktaTokenProvider(...),
)
# Write
res = client.experience(
scope="org:acme/dept:eng/user:alice",
modality="conversation",
content={"kind": "message", "role": "user", "text": "..."},
context={"happened_at": "...", "recorded_at": "..."},
wait="indexed",
)
print(res.event_id, res.stages_completed)
# Recall (stratified pack)
pack = client.recall(
scope="org:acme/dept:eng/user:alice",
view="holistic",
query="What did we decide about Q3?",
layers=["beliefs", "facts", "episodes"],
temporal={"natural": "last 30 days"},
)
print(pack.context_block)
for b in pack.layers.beliefs:
print(b.claim, b.confidence)
# Why
why = client.beliefs.why(belief_id="belief_01HX...")
print(why.narrative)
# Streaming recall
async for chunk in client.recall_stream(
scope="...", view="narrative", query="...",
):
print(chunk.text, end="", flush=True)
# Forget
client.forget(
scope="org:acme/user:alice",
layers=["beliefs"],
selector={"about_entity": "ent_acme_corp"},
cascade="derived_only",
)
# GDPR
job = client.forget_gdpr(scope="org:acme/user:alice", audit_note="DSR #1234")
async for ev in client.lifecycle(erasure_id=job.erasure_id):
print(ev)
Sign-off
This spec is the contract we will implement against. Each phase in §19 ships an internally consistent slice; the spec evolves only through explicit revision PRs that update this file and the schemas in Appendix D in lockstep.
Open items (acknowledged):
- The Understanding layer remains partial in v1; the API surface is honest about it via
_partial. - Federation, BYO vector store, GraphQL, and webhook lifecycle are deferred (§18).
- All deployment-policy presets ship YAML examples in
crates/cortexdb/presets/(TBD by phase 1).