CortexDB · docs

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

  1. Glossary
  2. Executive Summary
  3. Design Philosophy
  4. Core Concepts
  5. Authentication & Identity
  6. Authorization Framework
  7. HTTP Conventions
  8. Layer Reference
  9. Endpoint Reference
  10. The Stratified Pack
  11. Streaming
  12. Forget Semantics
  13. Temporal Model
  14. Lifecycle Events Catalog
  15. Audit Log Schema
  16. Importer Mappings
  17. Error Catalog
  18. Compatibility Matrix
  19. Deferred Decisions
  20. Implementation Order
  21. Appendices

0. Glossary

TermDefinition
ActorThe principal performing an operation. Typed: user:, agent:, service:, system:.
Bi-temporalRecords carry two independent time axes: when the fact was true in the world (event time) and when CortexDB learned of it (ingest time).
CapabilityA discrete, named permission (e.g., forget.gdpr). Granted/denied by the four-tier policy stack.
CaptureThe synchronous write of an event to the WAL. The only synchronous step in the write path.
ConsolidateThe async stage where facts merge, beliefs revise, and understanding nodes synthesize.
Derived layerAny layer whose contents are computed from events. Facts, Beliefs, Episodes, Understanding are derived; Events are not.
Experience envelopeThe structured input payload to POST /v1/experience. Replaces messages in peer APIs.
Holistic viewA recall that traverses upward through the scope hierarchy.
LayerOne of the five memory tiers: Events, Episodes, Facts, Beliefs, Understanding.
MemoryA polymorphic noun used in marketing. Internally we say "record at layer X." Avoid in API design.
PackThe stratified output of POST /v1/recall: layers + context block + provenance + diagnostics.
RefcountThe number of cross-scope references pointing at an event. Decisive for GDPR.
ScopeA /-delimited path of type:id segments that names a memory partition. Workspaces are scopes too.
Stratified packSee Pack.
TokenA signed bearer credential carrying actor identity and a capability set.
WALWrite-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:

  1. Five addressable memory layers, not one flat memory pool. Clients can read Beliefs, Facts, Episodes, Events, and Understanding separately or as a stratified pack.
  2. Unified scope paths that subsume "workspaces" — there is one namespace primitive, hierarchical with opt-in registration and ACLs.
  3. Four-tier authorization (deployment → tenant → scope → actor) with capability-level attribution on every denial.
  4. Signed-token identity mandatory on every call; tokens can only narrow what policy already allows.
  5. Async-first writes with an SSE lifecycle stream; sync available via wait parameter.
  6. Bi-temporal everywhere, with as_of, valid_during, recorded_during, and natural-language temporal qualifiers across all layer reads.
  7. Reference-counted GDPR erasure — events are deleted when refcount drops to zero, otherwise PII is redacted while preserving cross-scope referential integrity.
  8. Streaming recall in both granular (per-layer-as-ready) and narrative (token-by-token) modes.
  9. Native importers for Mem0, Zep, Letta, OpenAI memory exports, and generic JSONL — in both strict_temporal and batch_throughput ordering 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

  1. 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.
  2. Bi-temporal at the boundary. The API accepts and returns both valid_* and recorded_* timestamps on every layer record. Single-time-axis models are not interoperable with us.
  3. Async writes are normal. Captures return immediately. Clients that need read-after-write must opt in via wait.
  4. Policy is observable. Every denial cites the tier and the specific capability. No opaque 403s.
  5. 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

2.3 Inspirations and what we did differently

SourceBorrowedDid differently
Mem0Async writes; reference_date temporal anchorKept a graph; added layers; added bi-temporal; added GDPR refcount
ZepBi-temporal facts; synthesized context blockExtended bi-temporal to all layers; stratified pack with layers + context; added Belief layer
LettaShared blocks across agentsScope = ACL primitive; no separate "blocks" concept
CogneeECL pipeline; partial+honest contractReplaced ECL with our five-stage cycle; ECL bleeds implementation; ours bleeds purpose
LangMemTuple namespaces with template variablesString paths (operator-readable); hierarchical inheritance
Du et al. (arXiv:2505.00675)Six atomic operations as observableSurfaced as SSE lifecycle events, not separate verbs
Generative AgentsCitation pointers as explainabilityRequired 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:

TierChange policyResponse header
StableAdditive 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
BetaMay break with one-release notice. Shapes tracked but not yet pinned. Capability defaults may shift between minor releases.X-Cortex-Stability: beta
ExperimentalMay 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 familyNotes
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/timelineLayer reads
GET /v1/scopes, POST /v1/scopes, PUT /v1/scopes/members, DELETE /v1/scopesScope management (query-param addressed; see §8.8)
GET /v1/lifecycle/stream (SSE), GET /v1/lifecycle/event/{lce_id}, GET /v1/lifecycleLifecycle
GET /v1/policy/effective, GET /v1/policy/deploymentPolicy introspection (read)
GET /v1/audit, GET /v1/audit/{audit_id}, POST /v1/audit/verifyAudit
POST /v1/blobs, GET /v1/blobs/{blob_id}Blobs
POST /v1/auth/revokeToken revocation
GET /v1/admin/health, GET /v1/admin/metrics, GET /v1/admin/versionAdmin introspection

2.4.2 Beta surface (shape solid, semantics maturing)

Endpoint familyMaturity notes
GET /v1/beliefs, GET /v1/beliefs/whyConfidence/CI math may shift; stance enum may extend
GET /v1/understanding, GET /v1/understanding/{id}, GET /v1/understanding/{id}/related, GET /v1/understanding/coverageReturns _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/phrasesCustom 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.

SurfaceWhy experimental
POST /v1/erasures (reference-counted GDPR erasure), POST /v1/erasures/preview, GET /v1/erasures/{erasure_id}, POST /v1/erasures/{erasure_id}/cancelRefcount 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/synthesizeSynthesizer not yet stable
forget.gdpr.cross_workspace capability and propagationCo-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/pruneCleanup automation
POST /v1/admin/flush-views, POST /v1/admin/compactOperator 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

FromToGate
Experimental → BetaTwo independent customer integrations exercising the surface for ≥30 days without semantic complaints.
Beta → StableOne 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

LayerPurposeAtomic unitMutabilityTime semantics
EventsLossless capture of experienceA WAL entryImmutableobserved_at (event time) + recorded_at (ingest time)
EpisodesBounded spans of related eventsA causal chain with start/endSealed once consolidatedstarted_at / ended_at; bi-temporal at boundaries
FactsTriple-shaped atemporal/temporal assertions(subject, predicate, object, validity)SupersedableFull bi-temporal: (valid_from, valid_to, recorded_from, recorded_to)
BeliefsProbabilistic claims about state of the worldA claim with confidence + supportsContinuously revisableBi-temporal + last_revised_at
UnderstandingSynthesized conceptual modelA concept node + edgesVersionedversion + valid_from; revisions chained

3.1.1 Why these five and not three

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.

OperationFires whenVisible via
CaptureWAL append succeedsevent: captured on SSE; wait=captured returns
IndexEvent indexed in BM25 + HNSWevent: indexed; wait=indexed returns
UpdateFact ADD/UPDATE/NOOP decisionevent: extracted and event: consolidated
ConsolidateBeliefs revised; Understanding nodes touchedevent: consolidated; wait=consolidated returns
ForgetRecords deletedevent: forgotten
CompressEpisodes sealed; Understanding versions bumpedevent: 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:

FieldMeaning
valid_fromFirst moment in the world when the claim is true
valid_toFirst moment when the claim ceases to be true (exclusive). null means open-ended.
recorded_fromFirst moment CortexDB learned the claim
recorded_toFirst 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):

TypeSemantics
orgTop-level tenant
deptDepartment / business unit
teamSmaller cross-functional unit
appApplication instance
userEnd-user owner
agentAgent identity
serviceService account
wsWorkspace (collaborative, non-hierarchical conventionally)
projectTime-bounded initiative
globalCross-tenant pool (gated; see §5)
systemReserved 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:

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:

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:

IdentitySourceMeaning
callerToken sub, verified by signatureThe authenticated principal making the API call. Always present. Used for policy, audit, and rate-limit attribution.
observed_actorEnvelope observed_actor fieldWho performed the experience being recorded. May differ from caller (delegated agent, import job, admin write). Defaults to caller.
subjectEnvelope subject fieldWho 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:

3.5.2 Time fields

Two timestamps, with distinct ownership:

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"
}
FieldRequiredTypeNotes
scopeyesscope pathMust pass policy for scope.write
observed_actornoobjectDefaults to the token's sub. When set, must be authorized by scope.write.on_behalf_of capability if it differs from caller.
observed_actor.sessionnostringUsed for run/session grouping
subjectnoobjectDefaults to observed_actor. Cross-subject writes require scope.write.about_other capability.
modalityyesenumOne of conversation, document, tool_result, observation, feedback, imported
content.kindyesenumOne of message, text, json, blob_ref, triple
content.textconditionalstringRequired for message and text
content.roleconditionalenumRequired for message: user | assistant | tool | system
content.medianoarrayBlob refs (uploaded via /v1/blobs) for multimodal
context.observed_atyesRFC3339Event time (client-supplied)
context.source_recorded_atnoRFC3339Upstream ingest timestamp (imports only); server preserves alongside its own recorded_at
context.preceded_bynoarrayCausality hints (memory event IDs evt_...)
directives.extractnoarrayOverride default extraction stages
directives.consolidate_intonoscopeTrigger consolidation into another scope; requires scope.write on that scope as well
idempotency_keyyesstringClient-generated; 64 chars max; replays return same event_id

The server adds the following to the stored event:

Server-assigned fieldSource
idUUID v7, prefixed evt_
callerFrom token sub
recorded_atServer wall clock at WAL append
wal_offsetMonotonic position
tenant_idFrom 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.

PrefixResourceFormatNotes
evt_Memory event (WAL entry)UUID v7Time-ordered, monotonic per node
ep_EpisodeUUID v7
fact_FactUUID v7
belief_BeliefUUID v7
concept_Understanding conceptUUID v7Stable across versions
lce_Lifecycle event (SSE event)UUID v7Distinct from memory events; SSE resume uses these
job_Generic async jobUUID v7Catch-all for jobs without a typed prefix
batch_Bulk write batchUUID v7
imp_Import jobUUID v7
exp_Export jobUUID v7
ervw_Erasure previewUUID v7
erasure_Erasure executionUUID v7
pack_Recall packUUID v760s TTL; referenceable by /v1/answer
audit_Audit log rowUUID v7
blob_Uploaded blobUUID v7
ent_Resolved entityUUID v7Tenant-scoped
concept_alias_Concept alias mappingUUID v7
req_Request correlation IDUUID v7Echoed in X-Cortex-Request-ID
tok_Token revocation list entryUUID v7Internal

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.

FormWhenNotes
PASETO v4 publicIssued 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

ClaimRequiredMeaning
issyesThe issuer of the token. Server has a configured allowlist.
subyesThe actor identity, in our type:id form.
audyesThe tenant binding, e.g. cortexdb:tenant:acme.
expyesExpiry as Unix timestamp. Max 24h from issue.
iatyesIssue time.
jtiyesUnique token ID (for revocation lists).
deploymentnoOptional hint; ignored if mismatched.
capsnoCapability narrowing (see §5.2). Token can only restrict, not expand.
scopesnoPath-prefixed capability scoping (e.g., scope.write:org:acme/*).

4.3 Validation order

  1. Signature verification (against issuer's public key)
  2. iss in deployment allowlist
  3. aud matches deployment's tenant binding
  4. exp > now, iat ≤ now + 60s
  5. jti not on revocation list
  6. X-Cortex-Actor equals sub
  7. Token caps intersected with policy stack to produce effective capabilities
  8. 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:

  1. 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.
  2. 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 alongside cortexdb. 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.

CapabilityGates
scope.create.globalRegistering a global:* scope
scope.create.cross_tenantRegistering a scope path that crosses tenant boundaries
scope.writeWriting to any scope (default-allow at deployment)
scope.write.elevatedWriting 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_ofSetting observed_actorcaller in the envelope (delegated writes, importer flows)
scope.write.about_otherSetting subjectobserved_actor (admin corrections, coach-observes-player scenarios)
scope.create.customRegistering a scope whose type segment is not in the deployment's allowed_scope_types builtin set
scope.read.localReading exactly the named scope (default-allow)
scope.read.holisticWalking up the hierarchy on read
scope.read.descendWalking down (admin)
scope.read.cross_tenantCrossing tenant boundaries on read
understanding.readReading concept nodes
understanding.read.cross_scopePulling concepts across sibling scopes
understanding.synthesizeForcing consolidation (admin/debug)
forget.cascade.derived_onlyDefault forget mode
forget.cascade.redact_eventsRedact mode
forget.gdprTrue erasure including events when refcount = 0
forget.gdpr.cross_workspaceGDPR propagation into shared workspaces
import.from.mem0Run the Mem0 importer
import.from.zepRun the Zep importer
import.from.lettaRun the Letta importer
import.from.openaiRun the OpenAI memory importer
import.from.jsonlRun the generic JSONL importer
export.format.mem0Export to Mem0 format
export.format.zepExport to Zep format
export.format.jsonlExport to JSONL
lifecycle.subscribeOpen an SSE lifecycle stream
llm.invokeTrigger an LLM call (used by /v1/answer, narrative views)
diagnostics.readInclude diagnostics: summary or diagnostics: full in recall/answer responses (otherwise diagnostics are stripped)
audit.readQuery the audit log
audit.read.cross_actorQuery audit log for actors other than self
auth.revokeRevoke tokens (operator or self)
temporal.phrases.readList custom temporal phrases
temporal.phrases.writeRegister/delete custom temporal phrases
vocabulary.readList vocabularies
vocabulary.writeRegister/edit vocabularies
policy.administer.deploymentOperator only
policy.administer.tenantTenant admin
policy.administer.scopeScope owner
policy.administer.actorScope owner or self
blob.uploadUpload to /v1/blobs
blob.readRead blob contents
admin.flushForce WAL flush (operator)
admin.compactTrigger compaction (operator)
admin.healthRead 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:

5.5 Policy Endpoints

MethodPathCapPurpose
GET/v1/policy/deployment(read)Read deployment preset
PUT/v1/policy/deploymentpolicy.administer.deploymentOperator 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

6.2 Content types

6.3 Idempotency

Every write endpoint accepts an idempotency_key field in the body. Duplicates return the original response with X-Cortex-Replay: true.

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

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:

FieldTypeRequiredNotes
error_codeUPPER_SNAKE stringyesStable identifier; clients pattern-match on this
messagestringyesHuman-readable; may be localized in the future
request_idstringyesEchoes X-Cortex-Request-ID
detailsobjectnoError-specific structured fields
retriableboolyesWhether the same request may succeed later
documentationURLnoLink to error reference
HTTPWhen
400Validation error in request body
401Auth failure (signature, expiry, jti revoked, actor mismatch)
403Policy denial (capability missing in effective set)
404Resource not found and the caller has read access — otherwise return 403 to avoid info leak
409Conflict (idempotency-key reuse with different body; scope path collision on register)
410Resource deleted (forget) — only when policy allows revealing existence
422Semantic validation failure (e.g., invalid scope grammar, malformed timestamp)
429Rate limit
500Server error
503Service 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

HeaderMeaning
X-Cortex-Request-IDRequest correlation
X-Cortex-Policytier=...; decision=allow; capability=... (set on policy-gated endpoints)
X-Cortex-Replaytrue if response was served from idempotency cache
X-Cortex-DeprecationSet when calling a deprecated endpoint; value is the migration link

6.9 Headers we accept

HeaderMeaning
AuthorizationBearer token (required)
X-Cortex-ActorActor identity (must match sub)
X-Cortex-Idempotency-KeyAlternative to body field for GET-shaped idempotency
X-Cortex-Trace-ParentW3C 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
}
FieldTypeNotes
idUUID v7Time-ordered
wal_offsetuint64Position in the WAL; monotonic per node
derivesarrayRecords 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
}
FieldTypeNotes
subjecttyped referenceEntity or concept reference
predicatestringFree-text or vocabulary-bound (see §7.3.1)
objecttyped valueEntity / literal / concept
confidencefloat0..1; extractor-provided
superseded_by / supersedesfact_idThe 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
}
FieldTypeNotes
claimobjectTriple form: {subject, predicate, object}
stanceenumOne of supported, contradicted, uncertain, deprecated. See §7.4.1
confidencefloatPoint estimate, 0..1
confidence_intervalobject{lower, upper, method, level} — calibrated CI. Methods: beta_posterior, bootstrap, wilson
calibrationobjectModel + recent Brier / ECE score; updated by the calibration job
supportsarrayWeighted contributing records, each with explicit polarity (supports | against)
contradictsarrayOther beliefs in tension, with tension score and proposed resolution
revision_policyobjectPer-belief revision rules (overrides tenant defaults)
last_revised_atRFC3339
revision_countintTotal revisions since creation
supporting_evidence_redactedboolTrue if any support was GDPR-redacted (see §11.3)
supporting_evidence_redacted_fractionfloat0..1; fraction of supports[] redacted
why_urlstringPermalink to the explainability endpoint for this belief
_partialboolTrue when belief is in revision or evidence-redacted-decayed state
_partial_reasonenumrevision_in_progress, supporting_evidence_redacted, calibration_stale

7.4.1 Stance enum

StanceMeaning
supportedEvidence supports the claim; confidence reflects this.
contradictedEvidence has overturned the claim; confidence reflects how strongly. The belief is kept (not deleted) for explainability.
uncertainEvidence is mixed or insufficient; confidence near 0.5.
deprecatedThe 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

PropertyRule
Concept identityA concept_id is stable across versions. The id is assigned at first synthesis and never changes; only version increments.
Concept versioningVersions are immutable. Revision creates a new version row linked to previous_version. All prior versions queryable via as_of.
CoalescenceWhen 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.
SplittingWhen 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 vocabularyEdges 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 inputsEach concept declares the input layers it was synthesized from in synthesis_inputs. Re-synthesis re-derives from these inputs; as_of synthesis is supported.
StalenessA 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 scoreTenant-level signal: fraction of supporting facts/episodes within scope that were actually consulted during synthesis. Surfaced on GET /v1/understanding/coverage.
Invalidation on forgetIf 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
}
FieldTypeNotes
idconcept_idStable across versions
canonical_idconcept_id | nullSet when this concept has been merged into another; lookups follow the alias
versionintBumped on revision
previous_versionconcept_idLinks to the prior version
derived_fromarraySet when this concept resulted from splitting another
synthesis_inputsobjectLayers + filters + synthesizer used to produce this version
edgesarrayRelations to other concepts, vocabulary-bound
stanceenumsupported, uncertain, deprecated (same semantics as §7.4.1)
staleness_scorefloat0..1; fraction of input updates not yet incorporated
support_loss_fractionfloat0..1; fraction of supports forgotten since last synthesis
coverage_scorefloat0..1; fraction of in-scope inputs consulted during synthesis
_partialboolTrue until full synthesis lands
_partial_reasonenumconsolidation_in_progress, insufficient_supporting_facts, synthesis_disabled_by_policy, no_implementation_yet, stale_inputs_pending_resynthesis
_progressfloat0..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:

ValueReturns whenTypical latency
(omitted)WAL append~5ms
capturedWAL fsync~10ms
indexedBM25 + HNSW insert~100-500ms
consolidatedBeliefs/Understanding touched~500-3000ms

Errors:

HTTPCodeWhen
401actor_mismatchHeader doesn't match token sub
403policy_deniedCapability missing
422invalid_envelopeBody validation failed; details.field and details.reason populated
409idempotency_conflictSame key, different body
503wal_unavailableWAL 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:

FieldTypeNotes
includearrayReplaces older layers field; whitelist of layers to populate. Empty/absent ⇒ all per the view default.
exclude_contentboolWhen true, omits large content/summary strings; returns IDs + metadata only. Saves tokens when callers will re-hydrate selectively.
temporal.reference_dateRFC3339Anchor for parsing temporal.natural phrases (e.g., "last week"). Defaults to request time.
budgets.max_tokensintCross-layer knapsack target
budgets.per_layer_limitsmapExplicit caps per layer; overrides default view budget split
citation_modeenumnone, inline_with_markers, block_at_end, structured_only
diagnosticsenumnone (default for non-admin tokens), summary (timings, plan name), full (scoring contributions, policy attribution, model identifiers). Requires diagnostics.read capability for non-none.
streamboolSee §10.1
idempotency_keystringOptional; 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
}
FieldTypeNotes
answer_modelstringLLM identifier (provider-routed)
answer_max_tokensintOutput ceiling
use_pack_idstring | nullSkip 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/forget with a JSON body. We use POST instead because (a) DELETE with bodies is unreliable across HTTP clients/proxies, (b) forget is a job that may be long-running, and (c) it composes uniformly with the /v1/erasures job 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:

ModeCapabilityBehavior
derived_only (default)forget.cascade.derived_onlyDeletes from named layers; events untouched
redact_eventsforget.cascade.redact_eventsBlanks 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_fromas_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:

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)

EndpointCapabilityPurpose
GET /v1/admin/healthadmin.healthLiveness + diagnostics
GET /v1/admin/metricsadmin.healthPrometheus metrics
POST /v1/admin/flush-viewsadmin.flushForce flush async views
POST /v1/admin/compactadmin.compactTrigger 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

viewWhat gets filled
rawOnly layers.events. Useful for debugging and replay.
granularAll five layers[] arrays. context_block is empty.
holistic (default)All layers + a synthesized context_block.
narrativeEmpty layers; context_block contains an LLM-rendered story. Provenance preserved.
structuredLayers 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

ModeEndpointCapabilityWhat happens
derived_onlyDELETE /v1/forgetforget.cascade.derived_onlyWipes named derived layers; events untouched. Rederivable from WAL replay.
redact_eventsDELETE /v1/forgetforget.cascade.redact_eventsBlanks event payloads; keeps id, wal_offset, scope, actor (or redacts actor). Permanent.
GDPRDELETE /v1/forget/gdprforget.gdprReference-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:

Phase 3 — Categorize.

Phase 4 — Execute.

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:

11.5 What forget does NOT do


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
FieldMeaning
valid_fromFirst moment the claim is true in the world
valid_toFirst moment it ceases to be true; null = open
recorded_fromFirst moment CortexDB learned the claim
recorded_toFirst 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:

12.3 As-of semantics

as_of defaults:

FieldDefault
as_ofnull (= now)
Implicit recorded_to pinnow
Implicit valid_from pinas_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 nameFires whenPayload
capturedWAL append succeeds{event_id, actor, modality, wal_offset}
extractedAsync extract stage done{event_id, derived: {facts, entities, beliefs, episodes}}
indexedBM25 + HNSW insert done{event_id, layers_indexed}
consolidatedBelief / understanding revised{event_id, beliefs_updated, conflicts_resolved, superseded_facts}
compressedEpisode sealed / understanding versioned{episode_id, understanding_ids}
forgottenRecords deleted{layer, count, by_actor, cascade}
policy_changedA policy mutation affected this stream{tier, scope_or_actor, capability_diff}
policy_revokedThe subscription is no longer allowed{reason} (stream closes after)
import_progressImport job tick{import_id, fraction_complete, processed, total}
import_completeImport job done{import_id, summary}
import_errorImport row failed{import_id, line, error}
export_progressExport job tick{export_id, fraction_complete}
export_completeExport job done{export_id, url, size_bytes}
erasure_progressGDPR job tick{erasure_id, phase, fraction_complete}
erasure_completeGDPR job done{erasure_id, summary}
laggingClient 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:

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:

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 fieldCortexDB targetNotes
idevent.context.labels["mem0_source_id"]Preserved for round-trip
memory (text)event.content.textModality = imported
hashevent.context.labels["mem0_hash"]
metadataevent.context.labels["mem0_metadata"]Flattened
categoriesMapped to Understanding concept tagsConfigurable via options.map_categories_to
scoreDiscardedNot a stored field
immutabledirectives.no_supersede = true
expiration_datedirectives.ttl_for_event_layer
created_atevent.context.recorded_atAnd valid_from if no event-time info
updated_atNew event with preceded_by linkEach update becomes a new event
user_id{user_id} template variableSubstituted into scope_template
agent_idevent.actor.id (as agent:)
run_idevent.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 fieldCortexDB targetNotes
Messageevent.content with kind=messagerole and name preserved
Fact edgeFactBi-temporal preserved: valid_at→valid_from, invalid_at→valid_to
source_node_uuid / target_node_uuidsubject / objectEntity references resolved via Zep's node table
episodes[]event.context.preceded_by
EpisodeEpisodeDirect mapping
Group (group_id)Scope (ws:-prefixed by default)
Custom entity typesVocabulary + Understanding concepts
min_fact_rating per factfact.confidence

15.3 Letta

Letta fieldCortexDB targetNotes
Memory blockUnderstanding conceptConfigurable: options.map_core_blocks_to
Archival passageEvent (modality=imported)Configurable: options.map_archival_to
Conversation historyEvents with kind=messageOne per message
Agent file (.af)Bundle import: scope register + blocks + archivalFull state snapshot
persona blockConcept tagged actor_self_description
human blockConcept tagged actor_subject_description

15.4 OpenAI memory export

Format: ChatGPT's JSON dump.

OpenAI fieldCortexDB target
memories[].textevent.content.text
memories[].createdevent.context.recorded_at
memories[].source.session_idevent.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/.

HTTPCodeMeaningRetriable
400INVALID_BODYJSON parse errorno
400MISSING_REQUIRED_FIELDRequired field absentno
401MISSING_TOKENNo Authorization headerno
401INVALID_TOKEN_SIGNATURESignature verification failedno
401EXPIRED_TOKENexp < nowno
401REVOKED_TOKENjti on revocation listno
401ACTOR_MISMATCHX-Cortex-Actor ≠ token subno
401WRONG_TENANTaud doesn't match deploymentno
403POLICY_DENIEDEffective capability missingno
403NOT_A_MEMBERWriting to registered scope without membershipno
404NOT_FOUNDResource absent (and caller has read access)no
409IDEMPOTENCY_CONFLICTKey reused with different bodyno
409SCOPE_REGISTRATION_EXISTSPath already registeredno
410RESOURCE_DELETEDForgotten recordno
422INVALID_SCOPE_GRAMMARScope path malformedno
422INVALID_TIMESTAMPRFC3339 parse failureno
422UNPARSEABLE_TEMPORALtemporal.natural not understoodno
422INVALID_ENVELOPEExperience envelope failed validationno
429RATE_LIMITEDToken bucket exhaustedyes (after Retry-After)
500INTERNALUnexpected server erroryes
503WAL_UNAVAILABLECannot accept writesyes
503DERIVED_LAYER_UNAVAILABLERecall succeeds but one layer is degraded; partial responsesometimes

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.

CapabilityCortexDBMem0ZepLettaCogneeSupermemory
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/JSONLn/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 toolingn/an/an/an/an/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)

  1. 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.
  2. Differential privacy on recall. Mentioned by enterprise prospects; not standard enough to commit a shape.
  3. Vector store BYO. Letting customers point at their own pgvector / Pinecone. Out of v1 scope.
  4. 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.
  5. Webhook (push) lifecycle in addition to SSE. SSE covers most use cases; webhooks add infra (retries, signing). Deferred until requested.
  6. 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.

  1. as_of × include_superseded interaction (Stable: §8.7.3 facts). Resolved: both filters apply independently and conjunctively. as_of clips both axes (recorded_*as_of, valid_fromas_of < valid_to). include_superseded toggles whether the recorded_to=null constraint is enforced — when true, superseded versions whose recorded_to ≤ as_of are returned alongside the current versions. Conformance: schema fact.schema.json declares both query params; an integration test pair exists in §12 examples.

  2. 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's derives[] entries pointing at the deleted record are replaced with redacted:<original_id> (e.g., redacted:fact_01HX...). This preserves the audit signal that something was derived without leaking what. Conformance: event.schema.json derives entries match pattern ^(evt_|ep_|fact_|belief_|concept_|redacted:[a-z]+_)[A-Za-z0-9_-]+$.

  3. Lifecycle event ordering within a single event_id (Stable: §8.9, §13). Resolved: strict ordering within one memory event. The stages for any single evt_<id> are emitted in fixed order: capturedextractedindexedconsolidated → (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.

  4. view=descend with 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's scopes_traversed includes {scope, denied: true, item_count: N, denied_reason} entries. Counts reveal existence without leaking content; denied_reason cites the policy tier. Conformance: stratified_pack.schema.json scopes_traversed[] item is a discriminated union over {scope, items} and {scope, denied, item_count, denied_reason}.

  5. "Natural level" for scope.write.elevated (Stable: §5.2). Resolved: the deepest scope path in which the caller appears as an explicit owner or writer member. Writes to any scope at or below this depth do not require scope.write.elevated; writes to ancestor scopes do. For unregistered scopes (no membership), the caller's natural level is null and any write requires the capability. Conformance: capability_set.schema.json includes natural_level: scope-path | null.

  6. Bulk import rate-limit attribution (Beta: §8.11). Resolved: synthetic per-tenant bucket service:importer_<tenant_id>, with default capacity = 10× the rate_limit.write value from the deployment preset. Importer-job submissions consume from this bucket; the originating caller's personal bucket is not charged. Override via tenant policy rate_limit.importer. Conformance: policy.schema.json allows rate_limit.importer override.

  7. 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 as excluded_from_recall: true; it remains queryable directly via GET /v1/events/{evt_id} but does not surface in /v1/recall or /v1/answer. Endpoint added to §8.9; see §8.9.5 below.

  8. _partial: true outside Understanding (Beta: §7.4 Belief). Resolved: the _partial/_partial_reason/_progress triple is promoted to every derived layer (Episode, Fact, Belief, Understanding). Events never carry these fields. Each layer's enum of _partial_reason values is its own (e.g., Belief uses revision_in_progress, supporting_evidence_redacted, calibration_stale; Understanding uses consolidation_in_progress, insufficient_supporting_facts, etc.). Conformance: each layer schema declares the fields and its specific reason enum.

  9. idempotency_key collision 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/experience and /v1/experience/bulk are separate families; /v1/import/mem0 and /v1/import/zep are separate; /v1/erasures and /v1/erasures/preview are separate. This prevents a key reused across importers from being interpreted as a replay.

  10. 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.

  11. 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). When audit.retention = P2Y, peppers persist for ~2.25 years. Conformance: policy.schema.json deployment-level audit.pepper_rotation_period (default P3M) and audit.pepper_retention_buffer (default P3M).

  12. directives.consolidate_into cross-scope write (Stable: §3.5, §8.1). Resolved: dual capability required — scope.write on both the write scope and the consolidation target. No implicit elevation. If the caller lacks scope.write on the consolidation target, the envelope is rejected at validation time (422 INVALID_ENVELOPE, details.field = "directives.consolidate_into").

  13. Empty-selector forget (Stable: §8.5). Resolved: POST /v1/forget with empty selector (all filter fields null/empty AND memory_ids: []) requires confirm_all: true. Without it, 422 EMPTY_SELECTOR_WITHOUT_CONFIRMATION. Schema validation rule: selector is "empty" iff selector.about_subject ∈ {null, ""} ∧ selector.about_entity ∈ {null, ""} ∧ selector.predicate ∈ {null, ""} ∧ selector.memory_ids = [] ∧ selector.valid_during = null ∧ selector.recorded_during = null.

  14. Default view on layer reads (Stable: §8.7). Resolved: default is local for all GET /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 pass view=holistic explicitly. The /v1/recall endpoint is the only one whose default is holistic (rationale: recall is intent-aware; layer reads are not).

  15. Audit row body_bytes (Stable: §14). Resolved: add tenant-level setting audit.store_body_bytes (boolean), defaulting to true. When false, 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.json makes body_bytes optional.

  16. 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 at GET /v1/beliefs/{id} returns 410 RESOURCE_DELETED. List endpoints (GET /v1/beliefs?...) silently omit deleted beliefs. The audit_id of the deletion is recorded in the audit log.

  17. view=raw and temporal filters (Stable: §8.3, §9). Resolved: for view=raw (events-only response), recorded_during and as_of apply (to recorded_at on events); valid_during is ignored with no error (events have no validity interval, so the filter is a no-op). A valid_during filter present in a view=raw request causes X-Cortex-Warning: valid_during_ignored_for_view_raw to be set on the response.

  18. 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).

  19. Capability scope.create.* granularity (Stable: §5.2). Resolved: the enumerated scope.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's allowed_scope_types. Any user-defined scope type not in the allowed set is rejected at scope registration time with 422 UNREGISTERED_SCOPE_TYPE. A single capability scope.create.custom controls whether custom (non-builtin) types may be registered at all.

  20. use_pack_id lifetime 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.

  21. pack_id and 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 constituent evt_/fact_/belief_/... IDs from provenance.citations immediately after receiving the pack.

  22. 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-snake error field 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:

  1. An entry in this section documenting the prior behavior, the new behavior, and the rationale.
  2. A schema update in docs/schemas/.
  3. An updated example in the affected endpoint section.
  4. A regression test under tests/api_contract/.
  5. A bump of X-Cortex-Stability to stable-revising for 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.

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.

Phase 3 — Async Write Pipeline (Week 5)

Goal: write path returns 202 on capture, derives asynchronously, exposes wait modes.

Phase 4 — Stratified Recall (Week 6)

Goal: recall returns a stratified pack, not a flat context string.

Phase 5 — Lifecycle + SSE (Week 7)

Goal: the async pipeline is observable in real time.

Phase 6 — Layer Reads (Week 8)

Goal: every layer has its own GET endpoint.

Phase 7 — Scopes + ACL (Week 9)

Goal: scope registration, member ACLs, holistic reads with denied placeholders.

Phase 8 — Forget + Erasures (Weeks 10-11)

Goal: hard delete works in proportional + reference-counted modes.

Phase 9 — Audit + Compliance (Week 12)

Goal: every policy decision is tamper-evidently logged.

Phase 10 — Imports + Exports (Weeks 13-14)

Goal: customers can move data in and out.

Phase 11 — Understanding Synthesizer (Weeks 15-16)

Goal: drop _partial: true on the Understanding layer.

Phase 12 — Vocabularies + Temporal Phrases + Blobs (Week 17)

Phase 13 — Streaming + Narrative (Weeks 18-19)

Goal: streaming surfaces for the experimental tier.

Risk gates between phases

After phaseRequired to proceed
1All four presets load without error; cortex-auth-ref mints a token that the binary accepts
2Schemas validate the new record shapes against docs/schemas/*.json
3LongMemEval-S and LoCoMo benchmarks pass through the new write/recall API at the regression bar (§19.1)
4Recall regression bar holds
8Erasure preview/execute pair validated against the manifest schema; legal sign-off on redaction semantics
11Understanding 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):

LoCoMo (categories 1-4, judge score):

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):

These are tracked in docs/LATENCY_BASELINE.md; deviations beyond 20% trigger a perf-review checkbox on the PR.

Cost budget:

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.

Capabilityon_premcloud_sharedcloud_privatedev_local
scope.create.globalYNYY
scope.create.cross_tenantYNNY
scope.create.orgYYYY
scope.create.userYYYY
scope.create.agentYYYY
scope.create.wsYYYY
scope.writeYYYY
scope.write.elevatedY~YY
scope.write.on_behalf_ofY~YY
scope.write.about_otherY~YY
scope.create.customYNYY
scope.read.localYYYY
scope.read.holisticYYYY
scope.read.descendY~YY
scope.read.cross_tenantYNNY
understanding.readYYYY
understanding.read.cross_scopeYNYY
understanding.synthesizeYYYY
forget.cascade.derived_onlyYYYY
forget.cascade.redact_eventsYYYY
forget.gdprYYYY
forget.gdpr.cross_workspaceYYYY
import.from.mem0YYYY
import.from.zepYYYY
import.from.lettaYYYY
import.from.openaiYYYY
import.from.jsonlYYYY
export.format.mem0YYYY
export.format.zepYYYY
export.format.jsonlYYYY
lifecycle.subscribeYYYY
llm.invokeYYYY
diagnostics.readY~YY
audit.readYYYY
audit.read.cross_actorYNYY
auth.revokeYYYY
temporal.phrases.readYYYY
temporal.phrases.writeYYYY
vocabulary.readYYYY
vocabulary.writeYYYY
policy.administer.deploymentY(op)Y(op)Y(op)Y
policy.administer.tenantYYYY
policy.administer.scopeYYYY
policy.administer.actorYYYY
blob.uploadYYYY
blob.readYYYY
admin.flushY(op)Y(op)Y(op)Y
admin.compactY(op)Y(op)Y(op)Y
admin.healthYYYY

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:

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):

FileDefines
experience_envelope.schema.jsonThe POST /v1/experience request body
event.schema.jsonEvent records (§7.1)
episode.schema.jsonEpisode records (§7.2)
fact.schema.jsonFact records (§7.3)
belief.schema.jsonBelief records (§7.4) including stance, confidence_interval, calibration, revision_policy
concept.schema.jsonUnderstanding concept records (§7.5) including identity/versioning/staleness fields
scope.schema.jsonScope registration + policy block
policy.schema.jsonDeployment / tenant / scope / actor policy documents
audit_row.schema.jsonAudit log row (§14)
stratified_pack.schema.jsonThe POST /v1/recall response
answer_response.schema.jsonThe POST /v1/answer response (subset of pack + LLM block)
lifecycle_event.schema.jsonEach SSE event payload, discriminated by event name
erasure_preview.schema.jsonPreview / manifest response shape
erasure_job.schema.jsonErasure execution / status / cancel responses
token_claims.schema.jsonRequired and optional JWT/PASETO claims
temporal_block.schema.jsonThe temporal block shared by recall / answer / layer reads
capability_set.schema.jsonEffective 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):