Typed memory relations
Status: unreleased 0.13.0 contract. Record a directed assertion between two current memories, retain its provenance and review history, and revalidate both endpoints before serving it. This API is the durable foundation for typed memory graphs. It does not expand search or extract relations. Use the typed graph API to enumerate a bounded neighborhood.
For a graph of existing evidence dependencies, use the separate
memory evidence graph API. Typed assertions do not
replace derived_from, change memory payloads or alter cosine, BM25 or hybrid
scores. External services perform inference and supply model identities; no
provider credentials or embedding vectors are accepted by these routes.
Select a relation type#
Every assertion is directed from source to target, even when a client regards
the underlying concept as symmetric. The database does not create a reverse edge.
Both endpoints identify memories, not standalone entity nodes.
kind |
Meaning |
|---|---|
semantic_related |
The reporter asserts a semantic connection; no similarity score is implied |
same_entity |
The reporter asserts that the memories refer to the same entity; identity resolution is not performed by the database |
temporal_before |
The source's recorded event time must be strictly earlier than the target's; insertion order is irrelevant |
causal_claim |
The reporter proposes a causal connection; it is not a verified causal fact |
supports |
The reporter asserts that the source supports the target |
contradicts |
The reporter asserts that the source conflicts with the target |
The reporter's origin is one of model_inference, tool_observation,
human_statement or imported_assertion. It is a declaration by the caller,
not proof that a human, model or tool actually made the observation. Authenticated
authorship is recorded separately as reported_by. An authorized approval is a
review decision, not independent verification of truth or causality.
Authorize the request#
All four routes use POST, Authorization: Bearer <scoped-api-key> and
Content-Type: application/json. Each request contains contract_version: 1
and the existing resource scope.
| Route | Required capabilities | Result |
|---|---|---|
/api/v1/memory/relations/commands with assert, retire or restore |
memory_read and memory_write |
Durable command receipt |
/api/v1/memory/relations/commands with review |
memory_read and memory_review |
Durable review receipt |
/api/v1/memory/relations/read |
memory_read |
Current eligible relation |
/api/v1/memory/relations/inspect |
memory_read and memory_review |
Retained current relation and eligibility explanation |
/api/v1/memory/relations/revision |
memory_read and memory_review |
Immutable historical relation and its receipt |
The credential determines the company and, for private memory, the subject. Both endpoints and the relation must belong to the exact same authorized company, project, agent, mission and visibility partition. A UUID conveys no authority. Requests cannot override the company or private subject. Private assertions remain private to their subject; authorized shared-scope readers can read eligible assertions reported by another subject.
Review authority is independent of ordinary write authority. A review-only credential cannot create, retire or restore a relation. A writer without review authority cannot inspect retained metadata or historical versions. Native company relation inspection and history use separate company-administrative authority, including all private subjects in the company.
Assert a relationship#
First read the two memories and retain their exact integer revisions. The server rechecks those revisions and their complete transitive eligibility in the same write critical section used by memory mutations. A late worker cannot publish a relation against a source changed, rejected, deleted or expired in the meantime.
POST /api/v1/memory/relations/commands
Authorization: Bearer <scoped-api-key>
Content-Type: application/json
{
"contract_version": 1,
"scope": {
"project_id": "project",
"mission_id": null,
"agent_id": "agent",
"visibility": "private"
},
"idempotency_key": "extract-run-42-relation-1",
"operation": {
"type": "assert",
"relation": {
"source": {
"record_id": "018f0000-0000-4000-8000-000000000001",
"revision": 2
},
"target": {
"record_id": "018f0000-0000-4000-8000-000000000002",
"revision": 1
},
"kind": "supports",
"provenance": {
"origin": "model_inference",
"method": "support-extraction",
"method_revision": "prompt-v3",
"evidence_ref": "trace://run-42/relation-1",
"model": {
"provider": "example-provider",
"model": "example-model",
"revision": "example-pinned-revision"
}
},
"valid_from_millis": null,
"valid_until_millis": null
}
}
}
Replace the fixture IDs and model identity with actual values. Model inference
requires a non-null model with provider, model and revision. Other origins
may omit model; the database never assigns a model by assumption. The extractor
identity is distinct from any embedding-space identity attached to the memories.
The endpoints must be distinct UUIDs with positive revisions. Self-relations, unknown types, unknown fields and caller-supplied confidence or ranking weights are rejected. Method, method revision and model fields contain 1–256 UTF-8 bytes; evidence references contain 1–2048 bytes. These strings cannot be blank or contain control characters. Evidence references are identifiers, not URLs fetched by the database. Do not put secrets or sensitive source content in them.
Optional validity timestamps use Unix milliseconds. When both are supplied,
valid_from_millis must be strictly below valid_until_millis. The interval is
half-open: valid at the lower bound and invalid at the upper bound. Future and
historical assertions can be stored but are not eligible outside that interval.
Timestamp validity does not replace endpoint checks.
Receipts and retries#
A successful command returns contract_version and receipt. The receipt has:
- The server-generated
relation_id, its positiverevision, and the originalidempotency_key. action:asserted,retired,restoredorreviewed.- Authenticated
author,committed_at_millisand the operation'sevidence_ref. relation_digest,command_digestandreceipt_digest: opaque lowercase hexadecimal integrity values. They are not signatures or evidence of truth.
The canonical relation, both adjacency indexes, immutable revision, integrity metadata, adjacency completeness headers and idempotency receipt are committed in one synchronous WAL-backed RocksDB batch. Acknowledged state survives reopening and the tested abrupt process termination. Storage failures must not be interpreted as a known rejection; retry the identical command with the same key to resolve an uncertain outcome.
An idempotency key contains 1–256 nonblank UTF-8 bytes without control characters.
It is scoped by the authorized memory partition and authenticated subject.
Credential rotation for the same subject preserves replay. Reusing a key for a
different command returns 409 idempotency_conflict. A replay returns the exact
original receipt, including its original author, after verifying its retained
history. It does not establish that the relation is currently eligible.
Use a distinct key for each intended lifecycle change. Repeated assertions with different keys can create different relation IDs. They are not deduplicated into independent evidence, and their count must not be treated as corroboration.
Read and inspect#
Send this body to /read or /inspect:
{
"contract_version": 1,
"scope": {
"project_id": "project",
"mission_id": null,
"agent_id": "agent",
"visibility": "private"
},
"relation_id": "018f0000-0000-4000-8000-000000000003"
}
/read returns contract_version, scope and relation. A relation contains
its ID, revision, immutable input, original reported_by, creation and
modification timestamps, state (active or retired) and optional review.
An active, unreviewed claim can be eligible. Eligibility is permission to serve
this declared assertion, not certification that it is true.
An absent or ineligible relation returns 404 record_not_found without disclosing
the reason. Current endpoint revision changes, rejected reviews, deletion,
expiration and invalid transitive evidence suppress ordinary relation reads.
No endpoint payload is included in a relation response. The separate typed graph
response includes eligible canonical endpoint records and bounded assertions.
/inspect returns inspection.relation and inspection.eligibility. It preserves
retired or rejected metadata for authorized reviewers. The explanation contains
eligible, reason, evaluated_at_millis, an optional failing endpoint, and
dependency_work. Reasons are checked in this order:
retiredorrejected.not_yet_validorexpiredfor the relation's interval.endpoint_unavailableorendpoint_revision_changed, checking source then target and stopping at the first failure.eligibleif all checks succeed.
Inspection is a bounded diagnostic, not an exhaustive list of failures. Use the existing memory eligibility diagnostic to investigate an endpoint's ancestry when authorized. Failed byte or dependency checks return an error, never an eligible or partially validated relation.
Retire, restore and review#
Keep the same request envelope and send an operation such as:
{
"type": "retire",
"relation_id": "018f0000-0000-4000-8000-000000000003",
"expected_revision": 1,
"evidence_ref": "review://case-42/withdrawal"
}
retire changes an active relation to retired and removes its serving adjacency
entries. restore requires a retired relation, rechecks the exact original
endpoints, and makes it active. Restoring never removes a rejection or rewrites
the immutable input. To correct endpoints, type, model identity or validity,
retire the old assertion and create a new one with new evidence.
For a review, use type: "review" and additionally provide disposition as
approved or rejected. Review uses the same expected-revision guard and evidence
reference. Each accepted lifecycle operation advances the relation revision by
one. A rejected review removes serving adjacency entries. Approval cannot make
stale endpoints eligible, undo retirement, or override time validity.
Concurrent changes with the same expected_revision produce at most one accepted
new revision. Other distinct commands return 409 revision_conflict. Read the
current state and decide whether a new command is still appropriate; do not
blindly overwrite the new revision. Requests that retire an already retired
relation or restore an active one return 400.
To read an immutable version, send the ordinary read body with a positive
revision to /api/v1/memory/relations/revision. The result contains history
with that revision's relation and receipt. Historical reads do not apply
current serving eligibility. Missing revisions return 404. Revisions are unsigned
64-bit integers; clients must preserve their precision above JavaScript's safe
integer range rather than rounding through a floating-point number.
Work bounds and cache consistency#
| Boundary | Contract |
|---|---|
| HTTP body | At most 65,536 bytes on all four routes |
| Relation metadata | Canonical value and each immutable history value are independently bounded to 16 KiB |
| Endpoint records | At most 8 MiB combined serialized canonical record bytes |
| Endpoint eligibility | Existing transitive depth 8 and 64-source-node rules apply independently to each endpoint; the relation adds no derivation depth |
| Shared dependency work | At most 4096 distinct dependency lookups and 16 MiB per request; reported by inspection |
| Admission | All routes use the shared retrieval slots after authorization, through response serialization |
These are record-work bounds, not a process-memory or total response-size guarantee. Integrity-index reads, serialization overhead and temporary allocations are additional. RocksDB can fetch the value that crosses a byte bound before its size is known. Exhaustion fails the whole request with 400; it is not a complete empty result. Reduce the size or dependency footprint of the endpoints before retrying. There is no unbounded traversal option.
Reads use one snapshot and clock, but a successful read is not a lease. Re-read the relation immediately before reuse; endpoints can change and validity can expire after the response. The relation change feed now emits typed lifecycle events with history-bound cursors and subject-owned checkpoints. It is separate from memory changes: observe both streams and retain current-state validation. A memory checkpoint alone cannot certify a relation cache as current. Bounded neighbor enumeration is available through the typed graph API; automatic extraction and consolidation workers remain separate work.
Errors and validation#
All responses use Cache-Control: no-store and the published platform error
envelope. Handle errors explicitly:
| HTTP status | Common code or cause |
|---|---|
| 400 | Invalid version, malformed input, invalid lifecycle state, temporal ordering or bounded-work exhaustion |
| 401 | Missing, expired or revoked credential |
| 403 | Missing capability or unauthorized scope |
404 record_not_found |
Relation unavailable for ordinary read, or absent retained relation/history in an authorized inspection |
409 revision_conflict |
Stale relation revision or an endpoint no longer current and eligible |
409 idempotency_conflict |
Changed command under an already committed key |
| 413 | Oversized request body |
500 storage_inconsistency |
Encountered relation, receipt, history or index corruption |
503 retrieval_busy |
Shared work slots occupied; use bounded backoff with the same command key |
Qualification covers actual HTTP responses against the served OpenAPI, private subjects, companies, authorized and unauthorized resource partitions, revoked credentials, capability separation, byte and concurrency bounds, historical integrity, competing commands, eventless expiry and process-kill recovery. These tests use controlled fixtures. They do not establish search relevance, independent corroboration, agent-task improvement or production throughput. The graph research map records the remaining traversal, retrieval and evaluation requirements.