Versioned Memory API
The platform memory API stores records, version indexes and idempotency receipts
in the existing RocksDB memory backend at <data-directory>/agent-memory.
Mutations acknowledge one atomic batch with WAL and synchronous writes. There is
no implicit episode-retention quota or required model-provider service.
Base URL and authentication#
http://localhost:7474/api/v1/memory
Use Authorization: Bearer <platform-credential>. Provision credentials through
the platform API. Every command, read and query authenticates
and checks the exact resource grant. The tenant comes from the credential.
A scope contains project_id, nullable mission_id, agent_id and visibility
(private or shared). Missing/null mission means the missionless scope, never a
wildcard. Private memory belongs to the authenticated subject. Shared memory is
available to other subjects only with the same exact grant inside that tenant.
No JSON field can override tenant, record author, revision or storage namespace.
Endpoint summary#
| Method and path | Capability | Success |
|---|---|---|
POST /commands |
memory_write |
200 with durable receipt, including on replay |
GET /records/{record_id} |
memory_read |
200 with current record and scope |
POST /query |
memory_read |
200 with filtered records and continuation |
Create, update and delete share the command endpoint so the same receipt protocol covers every mutation. Reads and queries return current state and do not persist idempotency receipts or reserve a historical snapshot.
Create a record#
curl --request POST 'http://localhost:7474/api/v1/memory/commands' \
--header "Authorization: Bearer $QILBEE_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"contract_version": 1,
"idempotency_key": "session-42-observation-1",
"scope": {
"project_id": "research", "mission_id": "mission-42",
"agent_id": "researcher", "visibility": "shared"
},
"operation": {
"type": "create",
"record": {
"episode_type": "Observation",
"event_time_millis": 1700000000000,
"valid_until_millis": null,
"content": {
"primary": "The tool returned three matching documents.",
"secondary": null,
"context": "Literature review",
"data": {"document_ids": ["paper-a", "paper-b", "paper-c"]},
"embedding": null
},
"tags": ["literature", "tool-output"],
"metadata": {"source_request_id": "tool-call-17"}
}
}
}'
Record fields:
| Field | Type | Contract |
|---|---|---|
episode_type |
Enum | Conversation, TaskExecution, Observation, Decision, Error, or {"Custom":"name"}; case-sensitive existing Rust episode types |
content.primary |
String | Required text; stored without assigning truth or verification status |
content.secondary |
String/null | Optional second text, such as a response |
content.context |
String/null | Optional context text |
content.data |
JSON/null | Structured user data; remains untrusted content |
content.embedding |
Number array/null | Optional finite values retained as data; this endpoint does not build or validate a semantic index |
event_time_millis |
Signed integer | Required event timestamp in Unix milliseconds; unsupported date range is rejected |
valid_until_millis |
Signed integer/null | Exclusive visibility expiry; null means no configured expiry |
tags |
String array | Defaults to an empty list; exact tag filtering |
metadata |
JSON object | Defaults to an empty object; caller annotations, not authentication or verification authority |
Unknown contract fields are rejected rather than silently discarded. Optional content fields may be omitted or null. Payloads use JSON; no tokenizer or implicit token count is involved.
Illustrative response:
{
"contract_version": 1,
"receipt": {
"contract_version": 1,
"idempotency_key": "session-42-observation-1",
"record_id": "15d5c731-4e35-443c-9309-06a6561314e9",
"revision": 1,
"action": "created",
"committed_at_millis": 1700000000123,
"author": {
"credential_id": "c096714c-4c0b-4b99-900b-8b9aad818d0d",
"subject_id": "research-agent-user"
}
}
}
The server generates record IDs and captures the credential and subject that performed the mutation. This is attribution, not evidence that content is true.
Idempotency and uncertain responses#
An idempotency key is scoped to the authenticated tenant, resource namespace and subject. It contains 1–256 UTF-8 bytes, must not be blank, and cannot contain control characters. Two credentials for the same subject and scope can recover the same receipt if both currently have write authority.
Retry the same normalized command with the same key after a timeout or lost
response. The original receipt, timestamp, author, ID and revision are returned
without another mutation. Field order and equivalent omitted/null optional
fields do not change the normalized command. Changed effective payload under the
same key returns 409 idempotency_conflict.
Receipts survive updates, deletion and restart. Replaying an old create after deleting its record returns the original create receipt and does not recreate content. A receipt proves the command committed then; read the record separately for its current state. Invalid requests and revision conflicts do not consume a key. Receipts have no automatic retention policy in this increment.
Read a current record#
curl --get 'http://localhost:7474/api/v1/memory/records/15d5c731-4e35-443c-9309-06a6561314e9' \
--header "Authorization: Bearer $QILBEE_TOKEN" \
--data-urlencode 'contract_version=1' \
--data-urlencode 'project_id=research' \
--data-urlencode 'mission_id=mission-42' \
--data-urlencode 'agent_id=researcher' \
--data-urlencode 'visibility=shared'
For a missionless scope, omit mission_id; do not send the literal string null.
The response contains contract_version, scope, and record. A record contains
schema_version, record_id, revision, created_at_millis,
modified_at_millis, author for the last mutation, and payload with the record
fields above. Creation time stays fixed across updates. Expired, deleted, absent
or differently scoped IDs return 404. A malformed UUID returns 400.
Update with an expected revision#
Send a new key and a complete replacement payload:
{
"contract_version": 1,
"idempotency_key": "session-42-observation-1-correction",
"scope": {"project_id":"research","mission_id":"mission-42","agent_id":"researcher","visibility":"shared"},
"operation": {
"type": "update",
"record_id": "15d5c731-4e35-443c-9309-06a6561314e9",
"expected_revision": 1,
"record": {
"episode_type": "Observation",
"event_time_millis": 1700000000000,
"content": {"primary":"Two of the documents meet the criteria."},
"tags": ["corrected"]
}
}
}
A successful update returns action updated and revision 2. It replaces the
payload; omitted optional values reset to defaults rather than acting as a patch.
Only one concurrent write against the same revision can succeed. Other writers
receive 409 revision_conflict and must read current state before deciding on a
new command. An explicit authorized update can renew an expired record's validity.
Historical payload versions and transitive source invalidation are separate work.
Delete a record#
{
"contract_version": 1,
"idempotency_key": "session-42-observation-1-delete",
"scope": {"project_id":"research","mission_id":"mission-42","agent_id":"researcher","visibility":"shared"},
"operation": {
"type": "delete",
"record_id": "15d5c731-4e35-443c-9309-06a6561314e9",
"expected_revision": 2
}
}
Success returns action deleted and revision 3. The current payload is replaced
with a tombstone, preserving revision and receipt identity. Ordinary reads and
queries no longer return it. A new command cannot update a tombstone. This is
logical content deletion, not guaranteed physical erasure from historical WAL,
SST files or backups. Receipt digests do not contain the original payload text.
Query and pagination#
{
"contract_version": 1,
"scope": {"project_id":"research","mission_id":"mission-42","agent_id":"researcher","visibility":"shared"},
"filter": {
"limit": 20,
"after": null,
"text_contains": "documents",
"episode_type": "Observation",
"tag": "literature"
}
}
POST /query returns page.records, page.next_after and
page.scanned_records. Text filtering performs case-insensitive substring
matching independently against primary, secondary and context text. It does not
search structured JSON, emit synthetic similarity scores, or claim indexed BM25
or semantic retrieval. Type and tag filters are exact. Deleted/expired records
are excluded, and record/index versions are checked before using each record.
Results are ordered by UUID bytes. Pass next_after as the next request's after
until it is null. A continuation may yield an empty final page. Each request
returns at most 1,000 records and scans at most 10,000 entries; continuation
exposes remaining work instead of silently truncating the traversal. This is a
technical work bound, not a stored-memory quota. Pagination is a live traversal:
concurrent inserts before the cursor are not a snapshot or a durable changes feed.
Errors, consistency and limits#
The platform error envelope
applies. Memory-specific codes include record_not_found (404),
idempotency_conflict (409), revision_conflict (409), and
storage_inconsistency (500). Unsupported stored versions, missing point-lookup
counterparts or mismatched record/index revision/digest fail explicitly.
JSON request bodies are limited to 65,536 bytes, not tokens. All memory I/O runs in blocking workers. Credential checks apply to each request; a request already authorized may finish while a credential is revoked concurrently. The storage engine serializes these local memory operations; this is not a multi-node consensus guarantee.
Platform records use separate versioned key prefixes inside the same existing memory column families. Legacy global agent records are not assigned to tenants automatically. Both formats remain readable by their respective explicit APIs.
Validation#
The workspace suite tests receipts after reopen, changed-payload rejection, concurrent retries, conditional update conflicts, deletion replay, scope and capability boundaries, private/shared subjects, expiry, filtering, pagination and injected record/index version mismatch. A real HTTP child process acknowledges 20 commands, is killed without graceful shutdown, then recovers every record and returns every original receipt on retry with an exact total of 20 records.
This validates process-crash recovery on the test filesystem. Hardware power loss, backup/restore, indexed retrieval benchmarks, procedural HTTP and shared change feeds remain separate acceptance criteria.
Model-bound semantic retrieval#
The text query above remains a substring/filter endpoint. Use the separate semantic search API to attach externally generated vectors to exact source revisions and retrieve by cosine similarity. It enforces model identity and scope and discloses partial scan coverage.