Skip to main content
Glama
CPersona

CPersona

Official
by Cloto-dev

CPersona

Long-term memory for AI agents, in one SQLite file you own.

CPersona is an MCP server that lets Claude Code, Codex, Claude Desktop, Cursor, VS Code or any other MCP client remember what you decided, why, and what happened last time — across sessions.

PyPI CI Python License: MIT

Documentation · Getting Started · Tools · Discussions

The built-in dashboard, showing a made-up store of 72 memories.

Why CPersona

Every new session with a coding agent starts from zero. CPersona gives the agent a memory it can write to as it works and read from before it acts.

  • Yours, on your machine. Everything lives in a single SQLite file. No account, no cloud, no external database; sqlite3 .backup copies it.

  • No language model inside. CPersona never calls a generative model to store or recall. Your agent writes the summary; CPersona keeps it and finds it again.

  • Answers you can trace. reconstruct, the recommended way to answer from memory, quotes the stored records behind each item, so you can see where an answer came from.

  • Search that catches what vectors miss. Vector search runs beside full-text and keyword search, so exact names, identifiers and error strings are found too, in Japanese and other scripts without spaces as well.

  • See what it remembers. cpersona dashboard opens a read-only page of the store: what was stored last, what is recalled most, every memory in full, and the network above.

  • Honest when it degrades. Without an embedding server it still runs on keyword search, and says so on every recall instead of quietly returning less.

Related MCP server: mcp-memory-graph

Quick start

With Claude Code or Codex, install the agent plugin and let the agent do the rest:

/plugin install cpersona --marketplace Cloto-dev/cpersona

Then say "Set up CPersona." The plugin carries the setup skill and the hooks that remind the agent to recall before it acts and to store as it goes. (Claude Code 2.1.275 or later. In Codex: codex plugin marketplace add Cloto-dev/cpersona, then codex plugin add cpersona@cpersona.)

By hand, in three steps:

# 1. Run an embedding server (recommended; it powers vector search)
uvx --from "cembedding[onnx]" cembedding-download-model --model bge-m3
EMBEDDING_PROVIDER=onnx_bge_m3 uvx --from "cembedding[onnx]" cembedding

# 2. Register CPersona with your client (Claude Code shown)
claude mcp add-json cpersona '{"type":"stdio","command":"uvx","args":["cpersona"],"env":{"CPERSONA_DB_PATH":"/home/you/.claude/cpersona.db","EMBEDDING_MODE":"http","EMBEDDING_HTTP_URL":"http://127.0.0.1:8401/embed"}}' -s user

# 3. In a new session, ask the agent to store something, then recall it in another new session

# 4. See what it remembers, in the browser
cpersona dashboard

Getting Started covers the other clients, Windows, containers, and the short policy block that makes every future session use the memory.

Upgrading an existing install? Read the When you upgrade section of the release notes and SUPPORT.md first.

A look inside

Overview

A memory, opened

The dashboard overview: the memories stored last and recalled most

A memory opened in the dashboard, its text first

Memories are kept per agent, and project_id and channel let several agents and projects share one database without mixing.

Benchmarks

On OmniMemEval's LongMemEval-S pipeline (500 questions, answered by gpt-4.1-mini, judged by gpt-4o-mini), in this project's own run. Overall is the share of questions judged correct; Context Tokens is the average sent to the answer model per question:

Backend

Deployment

SS-User

SS-Asst

SS-Pref

Temp. Reas

Multi-S

Know. Upd

Overall

Context Tokens

CPersona 2.6.11 (pro mode)

local

97.14

94.64

96.67

86.47

85.71

88.46

89.60

4,262.5

CPersona 2.6.11 (lite mode)

local

95.71

83.93

90.00

80.45

74.44

82.05

82.20

993.3

MemOS

cloud

100.00

100.00

100.00

89.47

78.95

84.62

89.20

4,151

Pro mode is level with MemOS within sampling error, and on LoCoMo it answers 84.03%. Setups, the registered rules and every departure from a clean run are in the results.

Documentation

cloto-dev.github.io/CPersona is the full reference, also in Japanese:

Guide

What it covers

Getting Started

Install, embedding server, client registration, verification

Behavior Contracts

What you may rely on: recall ordering, dedup, scan window, response shapes

Tools

Every tool, grouped by what you reach for it for

Architecture

Storage, the retrieval pipeline, isolation axes

Roadmap

What each release line is for and may break; planned retrieval features and the scale ladder

Operations Runbook

Backup, degradation detection, tuning, CJK guidance, corpus sync

Configuration

Every environment variable and its default

Quality Assurance

How a release is gated: audits, the bug ledger, structural and mutation gates

FAQ

Short answers to the questions operators actually ask

Contributing

Bug fixes, tests and documentation can be sent as a pull request straight away. For changes to the database schema, the tool contracts or the tool list, open an issue first. CONTRIBUTING.md has the setup — no embedding server is needed to run the ~4,150 test functions — and the three checks CI runs.

Questions and ideas are welcome in Discussions. A sentence in the docs that confused you is worth reporting too: it is a defect of its own.

Support

If CPersona has earned a place in your workflow, you can sponsor the work on GitHub. It buys no feature and no place in the issue queue; starring the repository, filing a clear issue or fixing a sentence in the docs helps too.

License

MIT — see LICENSE.

Available Tools

35 tools
archive_episodeA

Archive a conversation episode with pre-computed summary, keywords, and resolved status. All LLM processing is performed by the caller. A summary that runs past the embedding window adds nodes:{status:'queued'} to the response, as on store.

Inputs: summary (required), agent_id (required), channel, history, +4 more.

calibrate_thresholdA

Auto-calibrate the vector search threshold from the null (random-pair) cosine distribution. Samples random memory pairs and places the threshold ABOVE the null mean so unrelated pairs are rejected. method='separation' (default) learns the operating point from two populations — null pairs vs temporally-adjacent same-session positives (nearest-neighbour fallback when too few exist); method='percentile' uses a quantile of the null distribution (robust to anisotropic models such as bge-m3); method='zscore' uses mean + z*std. No labels used, purely statistical. Adapts to both embedding model and corpus characteristics.

Inputs: agent_id (required), method, z_factor, percentile, +2 more.

check_healthA

Check memory database health (38-check registry, each issue tagged critical/warn/info): contamination, duplicates, oversized content, embeddings, FTS and schema integrity, SQLite file integrity, naming, JSON and timestamp drift, stale tasks, profiles and sources. Returns storage stats with project_id/channel distributions. The verdict is status (healthy / degraded / unhealthy, from severity counts; info never degrades). Read it as a verdict on what is IN the database, not on whether the pipeline that fills it is working: a corpus whose every embedding is NULL scores healthy while semantic recall is dead. Only fix=true judges the backend's liveness: a report-only run still contacts it (a probe embed, its identity, token counts) but reads none of that as liveness, so a quiet report-only run is not evidence the backend answered; the null_embedding finding says why its repair cannot run. checks runs a subset: an unknown name is rejected (ok=false), and every response echoes checks_run. fix=true repairs, agent-scoped and locked-safe, except block_index, coarse_index, dedup_msg_id_index, file_permissions, whose repairs reach every agent, so under an ACL they need read-write on '*'; leave them out of checks to stay agent-scoped. critical file-integrity findings are report-only. Lossy, irreversible repairs: oversized memories are cut to CPERSONA_MAX_CONTENT_LENGTH (default 16000) and the profile to CPERSONA_MAX_PROFILE_LENGTH (default 2000), keeping the start (lowering a cap shortens rows that fit the old one); duplicate_content and empty_content delete rows; memory_annotation and discord_mention strip text; invalid_json blanks unparseable JSON to '{}'; stale_pending_tasks deletes queued writes over an hour old; dedup_msg_id_index blanks colliding msg_ids; timestamp checks copy created_at over bad stamps. Some repairs are bounded per run (source canonicalisation classifies at most 10000 rows): a fix response with remaining > 0 has NOT converged; run fix again until remaining stops decreasing.

Inputs: fix, checks, agent_id, session_key.

check_updateA

Report whether a newer release of this server exists, and — only if you ask — install it. The check itself runs ONCE per process start, in a background task that nothing waits on, and its verdict is cached for 24h (CPERSONA_UPDATE_CHECK_INTERVAL_SECONDS) in a file beside the database; a bare call here reads that verdict and reaches neither the network nor the disk. state is one of: ok (running the newest release) / newer (a newer final release exists — pre-releases are never proposed) / yanked (every file of the RUNNING version has been withdrawn on PyPI; reason carries the publisher's text) / unlisted (this version is not on the index at all — a development checkout; not a defect) / unknown (no check has completed, e.g. no network) / disabled. install names how this process was installed (uvx / pip / checkout / unknown) and the exact command that would update it. refresh=true performs the fetch now (3s budget) and updates the cache. apply=true runs that command as an argv list (never a shell), returning exit_code and the last 40 lines of output — supported for pip and checkout installs only; under uvx the environment is a cache entry keyed by the launch arguments, so the update belongs in your client's config (uvx cpersona@latest), and an install here would be discarded on the next launch. A checkout parked at a tag (detached HEAD) is likewise refused before anything runs, and answers with the git fetch --tags && git checkout <tag> form to use instead (install.command shows it too). One apply runs at a time; a second is refused until it ends. Updating is NEVER automatic and never a side effect of any other call. A RESTART IS ALWAYS REQUIRED afterwards: this process keeps serving the old code until it is replaced. Unaffected by pause_persistence — an install writes no memory row, so a no-persist session can still repair a withdrawn version. Set CPERSONA_UPDATE_CHECK=false to disable the feature entirely: no fetch, no cache, no notice on recall or check_health, and this tool answers state=disabled.

Inputs: apply, refresh.

declare_associationsA
DestructiveIdempotent

Declare associative memory after the fact: entities with aliases, and subject–predicate–object relations, recorded verbatim and walked by reconstruct. The server extracts nothing and infers nothing — coverage is exactly what was declared. anchor_ref names the record (mem:<id> / ep:<id>) the declaration is evidenced by: every entity named is recorded as mentioned by it and every relation carries it. Malformed items are reported in dropped and skipped; nothing else in the call is refused for them. retract removes relations by id and mentions by {entity, ref} — the only way a declaration leaves the store. Response: {ok, result:'declared', entities:[{id, name, created}], mentions, relations:[ids], dropped:[{item, reason}], retracted?:{relations, mentions}}. Under pause_persistence nothing is written (result:'skipped', persisted:false).

Inputs: agent_id (required), channel, retract, anchor_ref, +3 more.

deep_checkA

Deep heuristic analysis of memory data quality. Detects issues requiring recovery or judgment (anonymous sources, short/trivial content, stale profiles, orphaned episodes, stale threshold calibration, embedding-space near-duplicate pairs as merge candidates). fix=true applies repairs for: anonymous_source, short_content. The short_content repair deletes every unlocked row it reports; run without fix first and read its samples. Report-only (fix is accepted and ignored): stale_profile, orphaned_episodes, calibration_staleness, near_duplicate, unnormalized_content, embedding_norm — apply those decisions via merge_memories / delete_memory / calibrate_threshold / update_profile. Use checks parameter to select specific checks.

Inputs: agent_id (required), fix, checks, session_key.

delete_agent_dataA
DestructiveIdempotent

Delete ALL data (memories, profiles, episodes) for a specific agent. Used by kernel during agent deletion.

Inputs: agent_id (required), session_key.

delete_episodeA
DestructiveIdempotent

Delete a single episode by ID. Ownership is enforced when agent_id is provided.

Inputs: episode_id (required), agent_id, session_key.

delete_memoryB
DestructiveIdempotent

Delete a single memory by ID. Ownership is enforced when agent_id is provided.

Inputs: memory_id (required), agent_id, session_key.

export_memoriesA
Destructive

Export memories, episodes, profiles and declared associations (entities, aliases, mentions, relations) to a JSONL file for backup or portability.

Inputs: agent_id (required), output_path (required), include_embeddings.

get_contentsA
Read-only

Fetch full, untrimmed content for recall preview refs. Use after a preview-tier recall to expand only the rows that matter instead of opting the whole recall out with full_content=true. Bounded twice: at most 20 refs per call, and a 40,000-character budget across the batch (2.5.4a2) that does not move when CPERSONA_MAX_CONTENT_LENGTH does. Rows are never cut to fit — when the budget is spent the remaining refs come back in deferred (absent otherwise) alongside budget_chars; re-fetch them in a second call. A single row larger than the budget is still returned in full, because this tool is the only path back to a row's complete text. RANGES: a ref may instead be an object that names part of its record -- {ref, node: i} or {ref, node: [first, last]} (inclusive) for overflow-tree nodes, e.g. the node.index of a reconstruct quote and its neighbours, or {ref, span: [start, end]} for characters. Offsets are in the stored text (a memory's content, an episode's summary without the '[Episode] ' label). The item then carries that slice as content and range = {span, content_len, and node + of when nodes were named, or block + of when blocks were}; a span end past the text is clamped and range.span says what was served. A ref may also carry revision, the digest a reconstruct quote's expand hands out for the text its offsets were measured in: a record rewritten since refuses rather than serving different characters under the same numbers. A range that cannot be served exactly is never widened to the whole row: it comes back in unresolved (absent otherwise) as {ref, reason}, reason one of invalid_range, no_current_nodes (the record has no complete node set -- short records have none, and a new long one gets them shortly after store), node_out_of_range, span_out_of_range, no_current_blocks, block_out_of_range, stale_revision. Only the slice counts against the budget.

Inputs: refs (required), agent_id (required).

get_operating_contextA
Read-only

Read the server-served operating context (v2.5.1): the operator-owned doctrine distributed to every connected client. Without arguments returns the preview tier — context_revision, instructions_summary, project_id registry (+ enforce mode), @auto defaults, and doctrine section names. Pass section to fetch one section's full body. Read-only: the context is edited by the operator on the filesystem (~/.cpersona/operating-context.toml), never via MCP.

Inputs: section.

get_profileC
Read-only

Get the current profile for an agent.

Inputs: agent_id (required).

get_queue_statusA
Read-only

Get the status of the background task queue (pending tasks, retry config).

Inputs: no named parameters.

get_recall_precisionA
Read-only

Read an agent's effective recall precision (knob 3) — the read-back companion to set_recall_precision. Returns the resolved specificity weight (beta) and its named precision level (strict / balanced / lenient, or 'custom' for a raw beta), and flags whether the value is a per-agent override or the global CPERSONA_RECALL_PRECISION default (overridden + global_precision / global_beta). Read-only: it never recalibrates and never persists, so a UI can load the current setting, let the user edit it, and write it back instead of the control being write-only.

Inputs: agent_id (required).

get_session_findingsA
Read-only

Pull the storage-integrity findings on demand (the SuperAuditor pull contract, docs/SUPERAUDITOR_STANDARD.md) instead of reading them off check_health: the same detector as check_health(fix=false) over the WHOLE database. Read-only, never repairs. Each finding carries kind (a check registry name, or an escalation tier this seam mints for a runner that grades its own severity, e.g. null_embedding_pipeline_down — a tier is NOT a registry name), check (the registry name that produced it: check_health(checks=[finding['check']]) re-runs exactly that probe) and a static per-kind severity (critical = the read contract is broken now / warn = two stored facts contradict / info = an observation). check_health's own verdict rides along as health_severity; a probe that raised is reported as kind check_crashed instead of failing the pull. NOT free: the registry runs unfiltered, including two whole-database reads (the FTS5 integrity-check over both indexes and PRAGMA quick_check), so every pull is O(database); budget it by call frequency. There is deliberately no cheap subset. Findings are NOT filtered by agent_id or project_id: slicing them by the caller's bucket would hide exactly the forgotten rows (scope a repair with check_health(agent_id=...)). Honest caps: findings holds at most per_kind_limit rows per kind, capped_kinds names every kind that had more, and total and the counts describe the RETURNED set only. summary restates it in prose (include_summary=false skips it). Without a session_key on a shared remote transport the response carries identity_shared: true; _meta.server_version identifies the running instance.

Inputs: session_key, per_kind_limit, include_summary.

import_memoriesA
DestructiveIdempotent

Import memories, episodes, profiles and declared associations from a JSONL file; the associations are pointed at the records they name as restored. Idempotent: memories deduplicate on msg_id (and on content within a project/channel), episodes on their summary within a project/channel.

Inputs: input_path (required), dry_run, session_key, target_agent_id.

list_episodesA
Read-only

List archived episodes for an agent (for dashboard display). bug-385: limit is clamped to 200 rows. When the caller asked for more and rows past the cap exist, the response carries budget_rows (the cap), so a capped listing can be told from one that reached the end of the data — reach the rest through export_memories or a narrower filter, not a larger limit. bug-255: within that cap the response holds an 800,000-character budget across summary and keywords together, with the same degradation and ceiling semantics as list_memories — rows past the budget that exceed the preview cap carry pure prefixes plus summary_truncated/summary_len and keywords_truncated/keywords_len; budget_chars appears iff at least one row was degraded. Their ref expands the summary via get_contents (under the row's own agent_id); a full keywords string is only available through export_memories.

Inputs: limit, agent_id, project_id.

list_memoriesA
Read-only

List recent memories for an agent (for dashboard display). bug-385: limit is clamped to 500 rows. When the caller asked for more and rows past the cap exist, the response carries budget_rows (the cap), so a capped listing can be told from one that reached the end of the data — reach the rest through export_memories or a narrower filter, not a larger limit. bug-255: within that cap the response holds a 1,000,000-character content budget. Rows are returned newest-first and none is dropped by the budget; once it is spent, later rows LONGER than the preview cap (CPERSONA_RECALL_PREVIEW_CHARS, default 500) degrade to a pure prefix with content_truncated/content_len and a ref that get_contents expands under the row's own agent_id (in an all-agents listing, pair the ref with the row's agent_id field). budget_chars appears iff at least one row was degraded. The effective ceiling is the budget plus one whole row plus the degraded rows' prefixes, so it scales with the preview cap; preview cap 0 disables trimming and the budget with it.

Inputs: limit, agent_id, project_id.

lock_memoryA
Idempotent

Lock a memory to prevent deletion and editing (delete_memory, update_memory). Operator re-scoping (migrate_channel_axis, merge_memories move) still relocates it, lock kept; delete_agent_data removes it. Ownership enforced when agent_id provided.

Inputs: memory_id (required), agent_id, session_key.

merge_memoriesA
DestructiveIdempotent

Merge memories, episodes, profiles and declared associations from one agent into another. Atomic one-shot equivalent of export→import without intermediate files. Strategy 'skip' deduplicates by msg_id (memories) and summary (episodes).

Inputs: source_agent_id (required), target_agent_id (required), mode, dry_run, +2 more.

migrate_channel_axisA

Re-channel bridge-type memories to their concrete channel (knob2 v2 default flip prep). Memories the kernel filed under the bridge type ('discord') are rewritten to the concrete channel recovered from the stored session_id ('{channel_id}:{user_id}:{chunk}' | '{channel_id}:shared' → channel_id), so per-channel recall can match them. Non-destructive (only the channel column changes) and idempotent (re-running is a no-op once moved). dry_run=true (default) reports the recoverable count, the channels that would be recovered, and an unrecoverable bucket (channel='discord' rows with no snowflake session_id) without mutating. globalize_unrecoverable=true moves the unrecoverable bucket to channel='' (global, matched by every channel-scoped recall) so the flip orphans nothing; default false (report only).

Inputs: dry_run, agent_id, session_key, globalize_unrecoverable.

open_dashboardA
Read-onlyIdempotent

Deprecated, removed in CPersona 2.7: the person can run cpersona dashboard instead. Open the read-only dashboard of this memory store and return its address, for the person to view in a browser: what is stored and when, the logical directory, each memory in full with how it is split, and the store's health. It runs on 127.0.0.1 behind a per-start token and writes nothing. Give the returned url to the person; open_browser=true also opens it on this machine.

Inputs: open_browser.

pause_persistenceA
Idempotent

Pause write operations on this MCP server for an opt-in TTL window (default 1800 seconds, at most 86400). While paused, every write tool — store, declare_associations, archive_episode, update_memory, delete_memory, delete_episode, delete_agent_data, lock_memory, unlock_memory, update_profile, import_memories, merge_memories, calibrate_threshold, set_recall_precision — returns a no-op response carrying persisted: false, dry_run: true and a reason (with the TTL remaining) instead of writing. Branch on persisted: false, not on an id: an id reads "no-persist" (store, archive_episode), and the action-specific id keys (deleted_id / updated_id / locked_id / unlocked_id / episode_id) are null. migrate_channel_axis is instead forced to dry_run and reports repairs_skipped, with no persisted key; check_health and deep_check downgrade to fix=false (repairs_skipped: true). Read tools answer normally, but recall does not bump recall_count / last_recalled_at. Blast radius follows session_key (response scope). Pass the same session_key here and on your write calls and the pause covers that key alone (scope: "session"): a session that sends a different key is neither silenced by it nor able to clear it. The key is a partition hint, not a credential, so anyone who sends the same string shares the pause. Omit it and you arm the bucket every keyless caller shares (scope: "process") — on a streamable-HTTP deployment one process serves every client, so a keyless pause silences writes for every other keyless session until resume or TTL elapse, and those sessions get no signal. Under stdio (one process per client) that bucket is the session. This affects only cpersona; pause cscheduler separately.

Inputs: session_key, ttl_seconds.

persistence_statusA
Read-only

Report whether persistence is currently paused and the TTL remaining (in seconds). It reports the bucket session_key selects (response scope), not the server as a whole: with a session_key it answers for your session only, so paused: false here does not mean no other session is paused. Without one it reflects the bucket every keyless caller shares, which on a streamable-HTTP deployment means paused: true may have been armed by a different keyless session.

Inputs: session_key.

recallA
Read-only

Recall memories by multi-strategy search (vector + FTS5 + keyword). To answer a question from memory, prefer reconstruct: it returns items that quote the rows supporting them, within a character budget. Rows come in ascending order of relevance: the LAST row is the best match (docs/behavior-contracts.md section 1). gate_fallback: true (absent otherwise) means every candidate fell below the quality gate and the below-gate lexical matches were returned: treat them as low-confidence. That rescue runs only with CPERSONA_CONFIDENCE_ENABLED=true and CPERSONA_CONFIDENCE_ORDERING=legacy; otherwise an all-below-gate recall is empty. suggestion (absent otherwise, at most once per session) is something only the user can decide: that this scope has outgrown the scan window with no coarse index for a time cue to search the rest. Relay its message, and run its fix only if the user agrees. Content is a preview of the record's start by default. A row the preview cut also carries excerpt, the part of the record that matched the query (at most 800 characters, CPERSONA_RECALL_EXCERPT_CHARS, 0 = none; passages joined by ' … ' in text order), and excerpt_basis (blocks / lexical / start). Read the excerpt before expanding a row with get_contents(refs). full_content=true returns whole rows, capped at 200,000 characters per response: rows past it fall back to the preview and the response carries full_content_budget_chars. Each scored row carries match_reason {signal, score, ...}: signal is the branch the quality gate keyed on (rsf > cosine > rrf), the other keys are the per-retriever contributions, and prior is the age weight when one ordered the row. An enabled confidence score is returned beside each row but neither orders nor gates it (unless CPERSONA_CONFIDENCE_ORDERING=legacy). Unscored rows (cascade FTS/keyword) have no match_reason.

Inputs: query (required), agent_id (required), deep, limit, +8 more.

recall_with_contextA
Read-only

Recall memories and merge with external conversation context. Automatically deduplicates, sorts chronologically, and returns a unified list. Replaces separate recall + manual merge in the caller. Content is preview-tiered by default — see recall's full_content / get_contents (full_content shares recall's 200k-char response budget, bug-211); a recalled row the preview cut carries excerpt / excerpt_basis as recall's do. Every external_context entry's content filters the recall (the caller already holds that text), but only role=user / role=assistant entries are merged into messages. When entries of other roles are present the response carries context_filter_only={roles:[...]} — those entries filtered the recall without appearing in the output, whether or not they dropped a memory this time. context_field_issues={entries:[{index, fields}]} (absent otherwise) names entries whose declared field was not a string: it was read as absent and the entry merged without it. CPERSONA_EXTERNAL_CONTEXT_MODE=reject refuses such a call instead. gate_fallback=true (absent otherwise) is forwarded from the underlying recall: every candidate fell below the quality gate and the below-gate lexical matches were returned — treat them as low-confidence. Only CPERSONA_CONFIDENCE_ORDERING=legacy with confidence on does that; otherwise such a recall contributes no rows.

Inputs: query (required), agent_id (required), deep, limit, +6 more.

reconstructA
Read-only

The recommended way to answer a question from memory. Returns up to count ITEMS (10 by default): units of memory, each quoting verbatim its most relevant record. content is the parts of that record that matched the query; pass head_ref, or an item's expand as it is, to get_contents to read more. No model is called and nothing is summarised; stored rows are never modified. COUNT IS A CEILING, not a fill target or a search depth: fewer items is a normal result and carries shortfall_reason, and nothing is padded. reserved_count held items (block, cue, far, mode seats) may trail the window. Breadth is set apart from count: raise top_k when items are missing evidence; max_hops and max_evidence bound the declared-relation walk and the rows kept per item. BUDGET bounds the characters of quoted text (content plus excerpts), where count bounds items: excerpts are omitted before items, and raising the budget never removes an item or an excerpt. Omit it for the server default. mode (lite / pro) caps the tokens returned (see it). READ FURTHER IN STEPS, smallest first: an item's expand, then the neighbours of a quote's node or block index ({ref, node: [i-1, i+1]}, or block), and the bare ref, the whole record, last. ITEM: claims lists the retained rows newest first (ref, as_of, why, hops, roles); excerpts quote the other claims. roles[].role names what the REFERENCED row is to this claim (supports / supersedes / corrects / qualifies / contradicts / temporal_predecessor); ignore a role you do not know. conflicts appears only when two rows cannot be ordered. A response adds fields only when the server did something other than asked (count_policy, budget_policy, bounds, gate_fallback); their absence does not say the items suffice or that the whole store was searched. trace=true returns the full audit. Declared associations widen the lexical search and add walked evidence inside items, never items. Full contract: docs/RELIABLE_RECALL_2_6.md section 7.

Inputs: query (required), agent_id (required), deep, lite, +12 more.

resume_persistenceA
Idempotent

Re-enable persistence immediately, clearing this caller's active no-persist TTL. Returns was_active=true if THIS bucket was paused before the call. It clears only the bucket session_key selects (response scope): with a session_key, your own pause and no other session's; without one, the shared keyless bucket, which on a streamable-HTTP deployment re-enables writes for every other keyless session too.

Inputs: session_key.

set_recall_precisionA

Set an agent's recall precision (knob 3) and recalibrate its quality gate. precision = strict | balanced | lenient maps to a specificity weight beta of 2.0 / 1.0 / 0.5 in the gate separation objective (sensitivity + beta*specificity): strict sits the gate higher (fewer contaminants, more misses), lenient lower (fewer misses, more contaminants). A raw beta > 0 overrides the named level; an empty precision with beta <= 0 clears the per-agent override and returns the agent to the global CPERSONA_RECALL_PRECISION default. The gate is recalibrated at the new beta immediately and persisted, so the change is live without a restart. Precision is a per-agent setting, not a per-recall argument: the gate threshold is precomputed on the separation curve at a fixed beta, so this tool recalibrates once instead.

Inputs: agent_id (required), beta, precision, session_key.

storeA
Idempotent

Store a memory for future recall: put the text in content. agent_id may be omitted on a connection that can write to exactly one agent (the agent used is echoed as resolved_agent_id), and source defaults to that agent. lock:true also locks the memory, as lock_memory does, and the response says locked:true|false. Every response carries result, the one field to branch on: 'stored' (a new row: id, and embedded, whether a vector was kept; truncated:true when content was shortened to the length cap; nodes:{status:'queued'} when the text runs past the embedding window and its overflow-tree nodes were queued), 'skipped' (nothing written and nothing wrong: a duplicate id, or content already stored under the same source id, echoes the existing row's id, except reason='duplicate (unique index)', a concurrent-write race, which has none), or 'rejected' (ok:false, nothing written; reason is human-readable, not a stable token). Under pause_persistence the write is skipped and the response carries persisted:false (id:'no-persist'), which tells a paused write from a duplicate. The older message={content, id, source, timestamp, metadata} form still works; use it or the top level, not both.

Inputs: id, lock, source, channel, +8 more.

traverseA
Read-only

The neighbourhood of a declared entity, as a graph: the entity named, its aliases, the entity -> entity relations declared on it and on what they reach, up to max_hops in either direction, and the refs of the records that mention each entity. Only what was declared (declare_associations, or associations on store); nothing is inferred. No record text: expand a ref with get_contents. entity is a name or an alias, compared after normalization; when it names more than one entity this call can read (a project's and the global pool's), all are starts. ORDER: entities by hops, then by the most recently declared relation that reached them, then by id; mentions by record id; relations most recently declared first. limit bounds both the entities returned and the refs listed per entity. Response: {entity, max_hops, limit, entities:[{id, name, hops, aliases?, mentions?, mentions_omitted?}], relations:[{id, subject, predicate, object, declared_by, declared_at, anchor_ref?}] (subject/object are entity ids from entities; a relation is listed when both ends are), entities_omitted?, bounds?:{omitted:[max_hops | limit]}, reason?:'no_such_entity'}. Relation ids are what declare_associations' retract takes. Records are listed only when this call could read them: the project, channel and source_id filters apply as in recall.

Inputs: entity (required), agent_id (required), limit, channel, +3 more.

unlock_memoryA
Idempotent

Unlock a memory to allow deletion and editing. Ownership enforced when agent_id provided.

Inputs: memory_id (required), agent_id, session_key.

update_memoryA
Idempotent

Update memory content by ID. Rejects if memory is locked. Ownership enforced when agent_id provided. The new content passes through the same sanitizer as store: it is capped at the content length limit (the response carries truncated:true when the cap bit) and [Memory from ...] annotations are stripped, so content consisting only of those is refused rather than written as an empty row. A new text that another memory in the same agent, project and channel already holds is refused with ok:false naming that memory, since one copy is kept per bucket. A new text that runs past the embedding window gets nodes:{status:'queued'}, as on store.

Inputs: content (required), memory_id (required), agent_id, session_key.

update_profileA

Save a pre-computed agent profile to the database. The text passes through the same sanitizer as store, against the profile's own ceiling: it is capped at 2000 characters (CPERSONA_MAX_PROFILE_LENGTH) and the response carries truncated:true when the cap bit — branch on it, the discarded remainder is not stored anywhere else.

Inputs: profile (required), agent_id (required), session_key.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.6/5.0

    Scored across 35 tools

    Disambiguation4/5

    Most tools sit on distinct resources (memories, episodes, profiles, associations, persistence) with readable boundary docs — reconstruct vs recall vs get_contents vs recall_with_context are explicitly tiered, not redundant. Real overlap clusters remain around diagnostics (check_health vs deep_check vs get_session_findings) and listings (list_memories/list_episodes vs recall), where an agent could pick the wrong probe.

    Naming Consistency4/5

    Almost everything is snake_case with a verb or noun prefix (get_/set_/list_/delete_/update_/lock_/pause_), so the pattern is predictable and readable. Minor deviation: several bare verbs (store, traverse, reconstruct) and noun-first names (persistence_status, check_health, get_operating_context) break the verb_noun rhythm, though not comprehension.

    Tool Count2/5

    35 tools is well past the point where an agent can hold the surface in view; the domain (memory CRUD, recall tiers, persistence control, diagnostics, migrations, self-update) is broad but the count is heavy. At least one tool is already dead (open_dashboard, deprecated/removed in 2.7) and others (get_queue_status, check_update) are peripheral to the core purpose.

    Completeness4/5

    Coverage is strong: full memory lifecycle (store/recall/expand/update/lock/unlock/delete), episodes, profiles, associations with retract, bulk export/import/merge, persistence pausing, health/diagnostics, and precision tuning with read-back. Minor gaps: no update_episode (episodes are archive/delete only), no agent/registry listing tool, and a stale deprecated tool left in the surface.

    Maintenance

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      A
      maintenance
      Persistent AI memory server with 3-layer hybrid search (vector + FTS5 + keyword), confidence scoring via Reciprocal Rank Fusion, episodic/profile memory, and 16 tools. Zero LLM dependency. Works standalone with Claude Desktop and Claude Code. MIT licensed.
      3
      Business Source 1.1
    • A
      license
      B
      quality
      C
      maintenance
      Cross-agent memory server: hybrid (vector + full-text + knowledge-graph) recall, per-user private/shared visibility, write-side secret/PII redaction, and as-of-date bi-temporal fact queries. Auto-injection hooks for Claude Code / Cursor / Codex; self-hostable on PostgreSQL or SQLite.
      46
      60 PyPI
      10
      MIT