MCP tools
MCP (Model Context Protocol) is how coding agents call external tools.
remember mcp gives an agent access to your memory. It is part of the
remember package, runs next to the agent, and talks to one
deployment. It
speaks on standard input and output (the agent starts it), or over HTTP
(agents connect to a URL).
How to connect an agent step by step: Connect your coding agent.
remember mcp
remember mcp [--read-only] [--api-url URL] [--api-key KEY] [--project PROJECT]
remember mcp --transport http [--bind HOST:PORT] [--read-only] [--api-url URL]By default the server reads JSON-RPC 2.0 messages from standard input, one
per line, and writes one response per line to standard output. It finds the
deployment and key the same way as every other remember command: flags,
then environment variables, then the credential file. See CLI: How the CLI finds your deployment.
--transport http serves the same tools over HTTP; see
Over HTTP.
--read-only leaves out the two tools that change memory, ingest and
delete_document, and refuses calls to them with the error read_only.
pipeline_readiness and search_documents only read, so they stay.
Protocol
| Method | Behaviour |
|---|---|
initialize | Requires params.protocolVersion (any string). Answers with protocolVersion 2025-11-25, capabilities: {"tools": {}} and serverInfo: {"name": "rememberstack", "version": "<package version>"}. |
ping | Answers {}. |
tools/list | Returns the tool list; see below. |
tools/call | Runs one tool. params.name is required; params.arguments must be an object when present. |
Notifications (no id) | Accepted; no response. |
| Anything else | Error -32601. |
JSON-RPC error codes: -32700 for a line that is not JSON, -32600 for a
message that is not a valid request, -32602 for bad initialize or
tools/call parameters, -32603 for a failure while building the answer
(for example tools/list when the deployment cannot be reached or rejects
the key).
The server offers tools only: no resources and no prompts.
How tools/list is built
The tool definitions — names, descriptions, input schemas — come from the
remember package itself, so every server that uses the package describes
the same tools the same way. Each tool has a version number that rises when
its arguments change in a way an older engine could not handle.
Every tools/list call reads GET /deployment and lists, in this order,
each tool the deployment reports at the same version:
ingest,pipeline_readiness,delete_documentandsearch_documents(onlypipeline_readinessandsearch_documentswith--read-only).- The four assured operations:
resolve_entity,claims_and_sources_context,facts_context,combined_context. - Context expansion:
adjacent_chunks. - The seven SQL query tools, when the deployment serves the query space.
A tool the deployment does not serve, or serves at another version, is left
out: upgrade remember or the engine so their versions match. If
GET /deployment fails (unreachable, 401, 403), tools/list fails with
-32603, so a wrong key never looks like an empty tool list. A full server
lists 16 tools: 4 + 4 + 1 + 7.
Every tool carries MCP tool annotations:
readOnlyHint is true for a tool that only reads and false for
ingest and delete_document, and destructiveHint is true only for
delete_document.
The server serves one deployment, so no tool takes a project argument; a
call that passes one is refused with project_routing_unavailable. A call to
a name that is not one of these tools is refused with unknown_tool. Neither
reaches the deployment.
Over HTTP
remember mcp --transport http --api-url http://127.0.0.1:8000The server listens at http://127.0.0.1:8765/mcp (Streamable HTTP, the MCP
transport for servers an agent reaches by URL). --bind HOST:PORT changes
the address.
| Request | Behaviour |
|---|---|
POST /mcp | One JSON-RPC message per request, answered with application/json; a notification gets 202 and no body. |
initialize | A successful one opens a session: the answer carries an Mcp-Session-Id header. Every later request must send it back. |
Missing Mcp-Session-Id | 400. |
MCP-Protocol-Version other than 2025-11-25 | 400. The header is optional. |
| Unknown or expired session | 404: initialize again. A session unused for an hour expires. |
DELETE /mcp with Mcp-Session-Id | Ends the session (204). |
GET /mcp | 405: the server never sends messages of its own. |
Origin header that is not the server's own address | 403. This stops a web page from reaching the server through a hostname it controls. |
| More than 16 open connections | 503 with Retry-After: 1, and the connection is closed. An idle connection is closed after 30 seconds. |
| Body over 32 MiB | 413. Only an ingest body sent as content_base64 gets this big; use remember ingest for larger files. |
| A client that stops sending for 30 seconds | The connection is closed. |
The HTTP server holds no key. It passes each caller's Authorization
header to the engine unchanged with every call, so the engine decides what
each caller may do, and a caller without a key reaches the engine without
one. tools/list therefore shows every tool the deployment serves; a call
the caller's key may not make returns insufficient_permission.
--api-key and --project are refused with --transport http.
ingest does not offer the path body over HTTP: the caller may be on
another machine.
It listens on a loopback address only; --bind with any other address is
refused. To let agents on other machines reach it, put a reverse proxy in
front of it that authenticates callers and terminates TLS.
Results
Every tools/call result has this shape:
{
"content": [{"type": "text", "text": "<JSON>"}],
"isError": false
}The text is a JSON document: the tool's result on success, an error object
when isError is true; see Errors.
ingest
Stores one document. Returns as soon as the bytes are stored; processing
takes minutes. Call pipeline_readiness before expecting the content in
results.
Give exactly one body: text, content_base64 or path.
| Parameter | Type | Required | Limits and default |
|---|---|---|---|
text | string | one body is required | Non-empty UTF-8 text. Needs filename. Default mime: the text type of the filename's extension (.md is text/markdown), else text/plain. |
content_base64 | string | Standard base64, no data: prefix. Needs filename. Default mime: from the filename's extension (.md is text/markdown, .pdf is application/pdf; the full table is in Ingest files), else application/octet-stream. | |
path | string | A file on the machine running the server. Refused unless path ingest is enabled; see Ingest from a path. Default filename: the file's name. Default mime: from the extension of the file's real name, as for content_base64. | |
filename | string | with text or content_base64 | 1–512 characters. |
mime | string | no | 1–255 characters. An explicit value always wins over the defaults above. The engine processes only media types it has a converter for; see File formats and converters. |
title | string | no | At most 512 characters. |
source_kind | string | no | 1–128 characters. Give with source_ref. |
source_ref | string | no | 1–512 characters. Give with source_kind. The same pair later makes a new version of the same document. |
versioning_mode | "snapshot" or "living" | no | Default "snapshot". "living" needs source_kind/source_ref. |
source_modified_at | string | no | ISO 8601 timestamp in UTC (Z or +00:00). Needs source_kind/source_ref. |
source_version_ref | string | no | 1–512 characters. Needs source_kind/source_ref. |
Unknown keys are refused. The input schema enforces the one-body rule with
oneOf.
Success result:
{
"deployment_id": "…",
"doc_id": "…",
"version_id": "…",
"content_hash": "…",
"created": true,
"mime": "text/markdown",
"title": "…",
"versioning_mode": "snapshot",
"parked": null,
"pipeline": {
"status": "accepted_not_ready",
"next_tool": "pipeline_readiness",
"poll_with": {
"version_ids": ["…"],
"require": {"pipeline": true, "p1": true, "live_graph": true, "p3": false}
},
"guidance": "Ingest accepted. Wait until pipeline_readiness.ready is true …"
}
}created: false means these exact bytes were already stored; no new
processing starts, and one readiness check tells the agent whether the
content is already available.
parked: "no_route" means the file's conversion is parked waiting for a
conversion route for its media type. The bytes are stored, but they are not
read until an operator adds a route if needed and runs
remember ops resume-no-route. The reply then has pipeline.status parked_no_route, no
next_tool or poll_with, and guidance telling the agent to tell the user
instead of polling readiness.
The server does not check body size itself. The deployment refuses a body
over its limit, and the tool returns body_too_large.
Ingest from a path
The path body is off by default. To allow it, list the directories the
server may read:
export REMEMBERSTACK_MCP_INGEST_ROOTS='["/home/ravi/notes", "/srv/specs"]'
# or: REMEMBERSTACK_MCP_INGEST_ROOTS=/home/ravi/notes,/srv/specs| Variable | Default | Meaning |
|---|---|---|
REMEMBERSTACK_MCP_INGEST_ROOTS | empty: path refused | A JSON array or a comma-separated list of directories. |
REMEMBERSTACK_MCP_PATH_READ_MAX_BYTES | 268435456 (256 MiB) | The largest file the server reads from a path. It protects the server process; it is not the deployment's upload limit. |
Rules for a path:
~is expanded and the path is resolved fully, following symbolic links. The result must be inside a listed directory (path_not_allowed).- The target must exist and be readable (
path_unreadable). - It must be a regular file, not a directory, pipe or device
(
path_not_regular_file). - Its size is checked before and while reading (
path_too_large).
The path is read on the machine running remember mcp, never on the engine
host.
pipeline_readiness
Checks whether ingested versions are processed and ready to be found.
| Parameter | Type | Required | Limits |
|---|---|---|---|
version_ids | array of strings | yes | 1–1,000 version UUIDs from ingest. |
require | object | yes | Exactly four booleans: pipeline, p1, live_graph, p3. For ordinary use: pipeline, p1 and live_graph true, p3 false. |
The result is the readiness report: ready, versions (each with stages
and their status), capabilities, model_bindings, build_revision,
document_binding_generation. Field detail:
Result types.
The tool description tells the agent how to poll: wait about 30 seconds after
ingest, then every 30–60 seconds; treat a failed stage as retrying and
keep polling; stop at once and report the version and stage when a stage is
dead_letter; stop and report after 20–30 minutes without ready. An
ingest that returned created: false is polled the same way, because an
earlier run of the same bytes may still be processing. See
Wait until a document is queryable.
delete_document
Removes one document, every version of it, from the memory. Its claims stop counting as evidence, and facts that only it supported are closed with a recorded retraction; facts other documents also support stay. The claims and the stored original are kept as history. Ingesting the same file again later adds it back as a new version.
| Parameter | Type | Required | Limits |
|---|---|---|---|
doc_id | string | yes | The document's UUID, as ingest returned it or a result cites it. No other arguments are accepted. |
The result is a DocumentDeletion:
doc_id, deleted_at, claims_retired, relations_closed,
observations_closed.
The tool description tells the agent to delete only when the user asks to
remove a specific document, or the document is plainly wrong or unwanted,
and never as a way to correct a fact: to correct one, ingest a document that
says the right thing. The token needs write scope. See
DELETE /documents/{doc_id}.
search_documents
Finds documents (files) by the names they were stored under, their general metadata and their text. The tool description tells the agent to use it when the user names or describes a file ("find Q3_sales_2025.xlsx", "emails from Alice") rather than asking about its content.
| Parameter | Type | Required | Limits |
|---|---|---|---|
query | string | no | 1–4,096 characters. Words from the file name, title, source path or text. |
family | array of strings | no | Format families: markdown, text, other_text, code, config, log, html, ebook, notebook, email, word, presentation, pdf, spreadsheet, delimited, dataset, image, media, archive, binary. |
authors, recipients | array of strings | no | Names or addresses; any listed term matches. |
created_from, created_to, modified_from, modified_to | string | no | ISO 8601 with a timezone, inclusive. |
language, thread_ref | string | no | Exact match. |
doc_ids | array of strings | no | Document UUIDs. |
versions | "current" or "all" | no | Default "current". |
k | integer | no | 1–200; default 20. |
cursor | string | no | The previous page's cursor; only without query. |
Unknown keys are refused. The result is a
DocumentSearchPage:
documents (each with doc_id, version_id, file_name, title,
family, status, authors, recipients, dates, p3_path and
overview),
cursor, as_of and people_matched. When a people filter matches several
different people, people_matched lists each with a document count and the
description tells the agent to narrow the filter instead of guessing. The
token needs read scope. How matching and paging work:
POST /documents/search.
Assured operations
These four tools are the deployment's assured operations. Their input
schemas come from the remember package, as for every tool. Behaviour and
results: Assured operations.
resolve_entity
Resolves a name to ranked candidate entities; never guesses silently.
| Parameter | Type | Required | Limits |
|---|---|---|---|
name | string | yes | At least 1 character. |
claims_and_sources_context
What sources said: current claims and the source passages that confirm them.
| Parameter | Type | Required | Limits and default |
|---|---|---|---|
query | string | yes | 1–8,192 characters. |
entity_ids | array of UUID strings | no | 1–20, unique. |
k | integer | no | 1–100; default 50. |
candidate_k | integer | no | 1–400; default 200; not smaller than k. |
facts_context
What the memory holds true: relations and observations under a time scope, with a bounded look at the entities' neighbourhood.
| Parameter | Type | Required | Limits and default |
|---|---|---|---|
query | string | yes | 1–8,192 characters. |
entity_ids | array of UUID strings | no | 1–19, unique. |
k | integer | no | 1–30; default 15. |
evidence_per_fact | integer | no | 1–5; default 3. |
hops | integer | no | 1–2; default 1. |
predicate | string | no | 1–200 characters. |
time | object | no | Default {"mode": "current"}. See below. |
time is one of:
{"mode": "current"}
{"mode": "at", "at": "2026-09-01T00:00:00Z"}
{"mode": "overlap", "from": "2026-07-01T00:00:00Z", "to": "2026-09-30T23:59:59Z"}
{"mode": "history"}Timestamps are ISO 8601 date-times. What each mode means: Time.
combined_context
Both of the above in one call, returned as ContextBundle/v2.
| Parameter | Type | Required | Limits and default |
|---|---|---|---|
query | string | yes | 1–8,192 characters. |
entity_ids | array of UUID strings | no | 1–19, unique. |
hops | integer | no | 1–2; default 1. |
predicate | string | no | 1–200 characters. |
time | object | no | As in facts_context. |
For every operation, unknown keys are refused, and integers must be whole numbers.
Context expansion tool
adjacent_chunks
Retrieve neighbouring chunks preceding and succeeding a target chunk within the
same document to expand conversational or narrative context. Returns an
Envelope containing surrounding chunks ordered by ordinal.
| Parameter | Type | Required | Limits and default |
|---|---|---|---|
chunk_id | string (UUID) | yes | Target chunk UUID to expand around. |
window | integer | no | Number of neighbouring chunks on each side (1 or 2, default 1). |
SQL query tools
SQL queries run over the query space (memory_v1): prepared, read-only views
and functions. The engine parses every statement and validates it against
the query space before it runs; anything outside it is rejected. See
Query space memory_v1.
All seven tools refuse unknown keys, wrong types (including null for a
string field), booleans in place of integers, and out-of-range numbers.
| Tool | Parameters | Result |
|---|---|---|
query_sql | sql (string, required); parameters (array of values for $1, $2, …); max_rows (integer ≥ 0) | QueryResult/v1 |
explain_sql | sql (string, required); parameters (array) | QueryResult/v1 with the plan; the statement does not run |
describe_query_space | pattern (string, shell-style filter over view names); include_examples (boolean, default false) | The query space description |
search_query_space | query (string, required); k (integer 1–25, default 10) | A list of {kind, name, score, purpose, tags}; searches the query space's own documentation, never your data |
list_saved_queries | namespace (identifier); status (string) | Saved-query summaries; without status, active versions only |
describe_saved_query | namespace, name (identifiers, required); version (integer ≥ 1) | One saved query: SQL, parameters, declared columns, validation state |
run_saved_query | namespace, name (identifiers, required); version (integer ≥ 1); parameters (array); max_rows (integer ≥ 0) | QueryResult/v1 |
Identifiers (namespace, name) must match ^[a-z][a-z0-9_]*$. See
Saved queries.
Errors
Errors come back as a tool result with isError: true, so the agent can read
them. Every tool returns the same JSON:
{
"error": {
"code": "rate_limited",
"status_code": 429,
"detail": "…",
"retryable": true,
"agent_action": "Wait retry_after seconds (if given), then retry; …",
"retry_after": 7
}
}| Field | Meaning |
|---|---|
code | A stable code: the deployment's own code when it sent one (SQL query errors, rate_limited), else one of the codes below. |
status_code | The deployment's HTTP status. 0 when no answer arrived; null when the call never reached the deployment (the server refused it itself). |
detail | What went wrong. |
retryable | Whether the same call can succeed later. |
agent_action | What the agent should do next. |
retry_after | Seconds to wait, when the deployment said. |
reason_code, request_id | Present only when known. |
code | status_code | retryable | When |
|---|---|---|---|
invalid_arguments | null | no | Missing, unknown or malformed arguments; not exactly one body; bad base64; bad UUID; bad timestamp. |
unknown_tool | null | no | The name is not one of the server's tools. |
project_routing_unavailable | null | no | The call passed a project argument. |
read_only | null | no | A tool that changes memory, on a --read-only server. |
source_lineage_pair | null | no | Only one of source_kind and source_ref, or lineage-only fields without them. |
encoding_error | null | no | text cannot be encoded as UTF-8. |
empty_body | null, or the deployment's | no | The body is empty. |
path_not_allowed | null | no | Path ingest is off, or the path is outside the listed directories, or contains a NUL byte. |
path_unreadable | null | no | The path does not exist or cannot be read. |
path_not_regular_file | null | no | Directory, pipe, device or other special file. |
path_too_large | null | no | Larger than the read limit. |
document_not_found | 404 | no | delete_document: no such document, or it is already deleted. |
forget_in_progress | 503 | yes | delete_document while a hard forget is running. Nothing was deleted. |
body_too_large | 413 | no | The deployment refused the body as too large. |
unauthorized | 401 | no | The key is missing, wrong, expired or revoked. |
insufficient_permission | 403 | no | The key may not do this on this deployment. |
rate_limited | 429 | yes | The key or the deployment sent too many requests; wait retry_after seconds. |
concurrency_limited | 429 | yes | Too many calls at once; wait retry_after seconds. |
transport_error | 0 | yes | No answer from the deployment. |
| SQL query codes | as sent | as the code | A SQL query tool failed; the codes are in Errors and status codes. |
engine_client_error | the 4xx status | no | Any other 4xx from the deployment. |
engine_unavailable | the 5xx status | yes | A 5xx from the deployment. Retry 3–5 times with back-off (2 s to 30 s); then report an outage. |
local_backend_error | null | no | The deployment's answer did not match the expected shape. |
internal_error | null | no | An unexpected failure in the server. |
Configuration that remember setup writes
remember setup writes one remember
entry per agent, in one of these shapes:
| Shape | When | What the agent does |
|---|---|---|
| Stdio entry | A self-hosted engine | Starts <launcher> <args> with REMEMBER_API_URL; the server reads the key from the credential file. |
| URL entry | --mcp-url, engine without a key, agent accepts URL entries | Connects to your remember mcp --transport http server. |
| URL entry with key header | --mcp-url with --api-key, agent can refer to a variable in a header | Connects to that URL and sends Authorization: Bearer with the value of REMEMBER_API_KEY from its own environment. |
Claude Code and Codex take URL entries only when their installed command
line supports them (see remember setup);
otherwise they get the stdio entry. A key is never written into any of
these files: a URL entry names the REMEMBER_API_KEY variable, never its
value.
In a stdio entry, <launcher> and <args> are one of:
| Situation | command | args |
|---|---|---|
A remember binary installed outside a virtual environment or cache | absolute path of remember | ["mcp"] |
Otherwise, when uvx is installed | absolute path of uvx | ["remember", "mcp"] |
A stdio entry's env block holds exactly one variable: REMEMBER_API_URL
for a self-hosted engine (default http://127.0.0.1:8000).
Existing files are merged: other servers and settings are kept, and the
remember entry is replaced. A file whose content would not change is not
rewritten, and a rewritten file keeps its permissions.
Cursor
<project>/.cursor/mcp.json:
{
"mcpServers": {
"remember": {
"command": "/Users/ravi/.local/bin/remember",
"args": ["mcp"],
"env": {
"REMEMBER_API_URL": "http://127.0.0.1:8000"
}
}
}
}A URL entry is { "url": "http://127.0.0.1:8765/mcp" }. With a key header:
{
"mcpServers": {
"remember": {
"url": "http://127.0.0.1:8765/mcp",
"headers": { "Authorization": "Bearer ${env:REMEMBER_API_KEY}" }
}
}
}Cursor replaces ${env:REMEMBER_API_KEY} with the variable's value when it
connects.
It also writes a rule file, <project>/.cursor/rules/remember.mdc,
replacing any earlier version:
---
description: Use Remember bitemporal memory for codebase facts, architecture, and past decisions
globs: *
alwaysApply: false
---
# Remember Memory Integration
Before making architectural decisions, refactoring core subsystems, or answering
questions about past codebase designs, consult Remember bitemporal memory via the
available MCP tools (`facts_context`, `combined_context`, `claims_and_sources_context`, `resolve_entity`, `query_sql`).
## Retrieval Discipline
1. **Resolve entities first:** Use `resolve_entity` to obtain canonical entity IDs for people, projects, modules, or concepts.
2. **Query facts first:** Use `facts_context` (with `time.mode="history"` for historical context or achievements) as the primary authority for adjudicated truth.
3. **Fall back to claims only when needed:** Use `claims_and_sources_context` if facts are missing or verbatim source text is required.
4. **Use `query_sql`** to run sandboxed SQL against `facts_current` or `graph_edges_current`.
## Temporal Semantics
- `valid_from` / `valid_until`: When the fact was true in the real world. Granularity is given by `valid_precision` (`instant`, `day`, `month`, `quarter`, `year`, `open`, `unknown`). `open` indicates an ongoing state with a known start date and no recorded end date.
- `asserted_at`: Strictly when the source made the statement (message sent / page published). Unresolved relative phrases in claim text (e.g. "last week", "yesterday") are relative to `asserted_at`. Never confuse speech time (`asserted_at`) with event validity (`valid_from`/`valid_until`).
- Check past decisions and bitemporal validity before asserting assumptions.
- Never guess historical rationale when it is recorded in Remember.Claude Code
remember setup runs Claude Code's own command in the project directory,
after removing any earlier remember entry with
claude mcp remove --scope local remember:
claude mcp add --scope local remember -e REMEMBER_API_URL=http://127.0.0.1:8000 -- /Users/ravi/.local/bin/remember mcpA URL entry is added with
claude mcp add --scope local --transport http remember <url>. Claude Code
does not get the key-header shape; with a key it gets the stdio entry. When
claude is not on PATH, remember setup prints the command for you to
run.
Claude Desktop
Always a stdio entry, as JSON like Cursor's (the mcpServers.remember entry, no rule file),
merged into Claude Desktop's configuration file:
| System | File |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux | $XDG_CONFIG_HOME/Claude/claude_desktop_config.json, default ~/.config/Claude/claude_desktop_config.json |
Restart Claude Desktop after the change.
Codex
<project>/.codex/config.toml:
[mcp_servers.remember]
command = "/Users/ravi/.local/bin/remember"
args = ["mcp"]
[mcp_servers.remember.env]
REMEMBER_API_URL = "http://127.0.0.1:8000"A URL entry is url = "<url>" in that table; with a key header it adds
bearer_token_env_var = "REMEMBER_API_KEY".
Any earlier [mcp_servers.remember] and [mcp_servers.remember.*] tables
are removed first; the rest of the file is kept. Codex loads a project's
.codex/config.toml only when you trust the project in Codex.
Antigravity
<project>/.agents/mcp_config.json, always with a stdio
mcpServers.remember entry like Cursor's, and a skill file <project>/.agents/skills/remember/SKILL.md,
replacing any earlier version:
---
name: remember
description: Open bitemporal memory infrastructure for AI agents. Use when looking up past decisions, system architecture, factual evidence, or attested codebase knowledge.
---
# Remember Bitemporal Memory Skill
You have access to Remember, an open bitemporal memory infrastructure for AI agents.
Use the Remember MCP tools (`facts_context`, `combined_context`, `claims_and_sources_context`, `resolve_entity`, `query_sql`, `describe_query_space`)
to query past system decisions, architectural records, and entity-relationship knowledge graphs.
## Preferred Retrieval Flow
1. **Entity resolution first (`resolve_entity`)**: When an inquiry involves a named person, organization, module, file, or concept, resolve it first with `resolve_entity` to obtain the canonical `entity_id`.
2. **Fact layer first (`facts_context`)**: Query `facts_context` (anchored by `entity_ids` when available, or by semantic text query) as the primary authority for established facts, biography, attributes, relationships, and history.
- Use `time.mode="history"` for biography, achievements, and "has ever" questions so historical and completed facts remain visible.
- Use `time.mode="current"` or `"at"` for what holds at an instant, and `"overlap"` for a requested interval.
3. **Sources fallback (`claims_and_sources_context`)**: Only fall back to `claims_and_sources_context` if `facts_context` lacks the answer, or if the inquiry specifically demands verbatim quotes, speaker dialogue details, or raw source context.
4. **Combined context (`combined_context`)**: Use when both adjudicated facts and source claims are needed side by side.
## Dates and Temporal Semantics
Do not collapse distinct temporal dimensions into a single generic date:
- **Facts carry `validity` with `valid_from`, `valid_until`, and `valid_precision`:**
- `valid_from` / `valid_until`: Real-world event or state validity ("When did this happen or hold true in the world?"). Answer event-time questions using these bounds.
- `valid_precision`: The granularity of the validity window (`instant`, `day`, `month`, `quarter`, `year`, `open`, or `unknown`).
- `open`: Represents an ongoing state with a known start date and no recorded end date (still true/current).
- `unknown`: No usable real-world date was given in the source. Undated facts are clean prose without temporal bracket annotations.
- **Evidence rows (claims) carry `asserted_at`:**
- `asserted_at`: Strictly **when the source made this statement** (when the message was sent, conversation occurred, or page was published).
- Unresolved relative phrases: If claim text still contains a relative phrase (*"last week"*, *"yesterday"*, *"two months ago"*), evaluate it relative to that row's `asserted_at`.
- **Never confuse speech time (`asserted_at`) with real-world event validity (`valid_from` / `valid_until`).**
- **System transaction timestamps (`ingested_at`, `invalidated_at`):**
- Record when the database learned or superseded the record. Never present system ingestion time as an event or conversation date.remember doctor checks the Cursor, Antigravity, Codex and Claude Desktop
files; see CLI.