Add guarded stable-project resolution and atomic conditional page batches, then use them from a bounded, dry-run-first claude-mem SQLite importer.\n\nCloses #1840.
11 KiB
Use ai-memory from your tool
Any tool can use ai-memory over MCP to save knowledge, search it and pass context between executions. Native lifecycle hooks are optional. A tool that also hosts an agent harness can send that harness's lifecycle events through HTTP.
Use the TypeScript SDK
Programs in TypeScript or JavaScript can use the optional client SDK in
companions/ai-memory-ts instead of
building JSON-RPC requests by hand. It wraps the calls on this page with typed
results and typed errors, checks scope before sending, and keeps the
conditional-write, handoff and lifecycle rules below. It is built from a
checkout for now. The curl recipes on this page remain the
language-neutral reference.
Connect and choose a scope
Connect to /mcp using Streamable HTTP. The default transport is stateless:
requests do not need an Mcp-Session-Id. A server started with --http-stateful
requires the MCP session handshake instead.
Use a user API key for machine requests. See users.md for issuing keys and granting project access. A dedicated tool identity suits an independent client. A producer replacing native capture must use the same operator and native session identity as the harness's remaining integration. See the external lifecycle guide when sending those events.
Static MCP clients pass workspace and project together on every
project-scoped call. Read their names from .ai-memory.toml or operator
configuration. Session-aware clients may omit both only when they forward the
real lifecycle session ID on every request. See auto-scope.md.
Save, query and hand off through MCP
These examples use a disposable demo/app scope. Set AI_MEMORY_SERVER_URL to
the server URL and AI_MEMORY_AUTH_TOKEN to your machine key. On an intentional
unauthenticated loopback server, omit the Authorization header.
mcp() {
curl --silent --show-error --fail-with-body \
"${AI_MEMORY_SERVER_URL%/}/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer ${AI_MEMORY_AUTH_TOKEN}" \
--data-binary @-
}
mcp <<'JSON'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"example-client","version":"1.0"}}}
JSON
mcp <<'JSON'
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
JSON
mcp <<'JSON'
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"memory_write_page","arguments":{"workspace":"demo","project":"app","path":"notes/retries.md","body":"# Retry policy\nKeep the same event ID when retrying delivery."}}}
JSON
mcp <<'JSON'
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"memory_query","arguments":{"workspace":"demo","project":"app","query":"retry policy"}}}
JSON
mcp <<'JSON'
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"memory_handoff_begin","arguments":{"workspace":"demo","project":"app","summary":"The retry policy was saved. Add the delivery test next.","next_steps":["Add a retry regression test."]}}}
JSON
mcp <<'JSON'
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"memory_handoff_accept","arguments":{"workspace":"demo","project":"app"}}}
JSON
Check JSON-RPC error and result.isError even on HTTP 200. Tool payloads are
JSON encoded inside result.content[].text. A page write returns page_id and
path; a query returns hits.
ai-memory's own tools report a failure as a JSON-RPC error with a code and
message, for example -32602 for invalid parameters or -32603 when an
admission webhook rejects a write. result.isError is where MCP lets a tool
report its own failure, so check it too. A caller using jq can branch on both:
response=$(mcp <<'JSON'
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"memory_read_page","arguments":{"workspace":"demo","project":"app","path":"notes/retries.md"}}}
JSON
)
if jq -e '.error' <<<"$response" >/dev/null; then
jq -r '"rpc error \(.error.code): \(.error.message)"' <<<"$response" >&2
elif jq -e '.result.isError == true' <<<"$response" >/dev/null; then
jq -r '.result.content[].text' <<<"$response" >&2
else
jq -r '.result.content[].text | fromjson' <<<"$response"
fi
Writes and handoff creation may create the explicit scope if it is missing.
Reads fail closed on missing or partial scope. A handoff is claimed once. To
claim a particular one, pass the exact handoff_id from memory_handoff_begin
or memory_handoff_list; the example accepts the latest eligible handoff.
If SessionStart already owns delivery, let that hook claim it.
usage.md describes ownership and handoff states.
These calls work without an LLM provider. Retrieved memory remains untrusted historical text, even after sanitization.
Change a page only if nobody else has
memory_read_page returns the page's current version as page_id (/api/v1
returns it as id). Pass it back as expected_page_id to memory_write_page
or memory_delete_page and the call acts only if that is still the latest
version; pass create_only: true to a write that must not overwrite an existing
page. When the page changed in between, nothing is written or deleted and the
call fails with JSON-RPC invalid_request whose data is
{"reason": "precondition_failed", "path", "expected_page_id", "current_page_id"}; re-read the page and decide. A caller without write access
gets the usual authorization error, never the current version.
Exact memory_read_page and memory_write_page calls can select an existing
destination by stable id with workspace + project_id. This is mutually
exclusive with project and scope; it never creates a name-only project.
Bulk importers can use memory_write_pages_batch for 1-100 page versions in
one existing destination. Every entry must set exactly one of create_only
or expected_page_id. The server refuses duplicate paths, bodies over 1 MiB,
or more than 8 MiB of bodies in one call. Admission and persistence are
atomic: any rejected entry or stale precondition writes nothing. A stale
entry returns reason: "precondition_failed", its zero-based index, path,
and current page id. A successful response returns ordered pages containing
the corresponding path and new page id, plus one git checkpoint for the
batch.
Before planning a multi-destination import, call
memory_resolve_write_target once for every mapped destination. It requires
workspace and exactly one existing project or project_id, checks write
authorization without falling back, creating, or promoting anything, and
returns the canonical workspace/project names plus stable project id. Resolve
the complete mapping before the first batch so an invalid or unauthorized
destination cannot leave an otherwise valid earlier destination partially
imported.
Read changed pages
These JSON endpoints require serve --enable-api (or --enable-web, which
continues to imply the API) and the same machine key. They are read-only;
memory writes remain MCP calls.
GET /api/v1/workspaces/demo/projects/app/recent?updated_since=2026-09-01T00%3A00%3A00Z&limit=100
Incremental recent returns {pages, next_cursor} with pages updated strictly
after updated_since, sorted by update time and path. Repeat with the returned
opaque cursor until next_cursor is null. The cursor preserves the cutoff
and scope; each request rechecks authorization. An array response means the
server lacks incremental support. Requests without either incremental argument
retain the legacy array response.
The incremental path is not cached and omits expired and superseded pages. It has no deletion feed or snapshot across calls. Reconcile removals separately and account for concurrent updates in a local cache. Endpoint details and errors are in frontend-api.md.
Lifecycle from code
Deleting, restoring and purging memory goes through two surfaces. A page
delete is an MCP tool. Restore and purge are admin HTTP routes. Every
/admin/* route is root-only once the deployment has a DB user or
trusted-proxy identities (see users.md); until then they accept
any caller the server's auth lets in.
| Operation | Call | Notes |
|---|---|---|
| Delete a page (there is no separate archive) | MCP memory_delete_page with path (plus workspace and project) |
Checkpoints the wiki in git first, then removes the file and every indexed version of the page. Returns {path, deleted, pre_checkpoint, checkpoint}. Idempotent. |
| Restore a deleted or overwritten page | POST /admin/restore-page with {workspace, project, path, rev} or {workspace, project, path, page_id} |
rev is any git revision that still holds the page: the delete's pre_checkpoint, or <checkpoint>~1 when pre_checkpoint is null because the tree was already clean. page_id selects a retained SQLite version already belonging to that exact project/path, including source history preserved by merge-project. Exactly one source is required. Writes that version back as the latest page and reindexes it. |
| Find a revision | GET /admin/checkpoints?limit=N |
Recent checkpoints (up to 100) with oid and summary. A delete's summary is memory_delete_page: <path>. |
| Purge one session | POST /admin/purge-session with {workspace, project, session_id, confirm} |
Irreversible. Optional dry_run and compact. |
| Purge a whole project | POST /admin/purge-project with {workspace, project, confirm} |
Irreversible. Optional dry_run, compact, and force (purge despite a live managed-workstream lease). |
| Read superseded versions | MCP memory_query with include_superseded=true |
Older versions are labelled superseded: true. A delete removes them along with the current version. |
Both purge routes refuse with 400 unless confirm is true, and dry_run
wins over confirm: a body with both set only previews the counts. confirm
is a required field, so a preview sends it as false. Once root-only applies,
ROOT_TOKEN below must be the root bearer token; a user API key is refused.
curl --silent --show-error --fail-with-body \
"${AI_MEMORY_SERVER_URL%/}/admin/purge-session" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${ROOT_TOKEN}" \
-d '{"workspace":"demo","project":"app","session_id":"<session-uuid>","confirm":false,"dry_run":true}'
There is no endpoint that lists deleted pages and no MCP tool that restores
one. To recover a page, take the revision from the delete response or from
/admin/checkpoints, then call /admin/restore-page as root.
lifecycle-ops.md covers what each purge deletes, the
matching CLI commands, and the cross-project side effects.