{"id":49369678,"url":"https://github.com/bolnet/attestor","last_synced_at":"2026-04-27T22:00:51.333Z","repository":{"id":342909699,"uuid":"1175503666","full_name":"bolnet/attestor","owner":"bolnet","description":"Auditable memory for agent teams. Self-hosted, deterministic retrieval, no LLM in the critical path. Python + MCP + Docker.","archived":false,"fork":false,"pushed_at":"2026-04-25T20:37:33.000Z","size":25254,"stargazers_count":12,"open_issues_count":4,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-25T21:14:37.414Z","etag":null,"topics":["agents","ai","claude-code","llm","mcp","memory","neo4j","pgvector","sqlite"],"latest_commit_sha":null,"homepage":"https://attestor.dev","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bolnet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-07T19:55:30.000Z","updated_at":"2026-04-25T04:11:11.000Z","dependencies_parsed_at":"2026-04-02T01:08:12.691Z","dependency_job_id":null,"html_url":"https://github.com/bolnet/attestor","commit_stats":null,"previous_names":["bolnet/agent-memory","bolnet/attestor"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/bolnet/attestor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bolnet%2Fattestor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bolnet%2Fattestor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bolnet%2Fattestor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bolnet%2Fattestor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bolnet","download_url":"https://codeload.github.com/bolnet/attestor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bolnet%2Fattestor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32356602,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-27T20:07:02.737Z","status":"ssl_error","status_checked_at":"2026-04-27T20:07:00.910Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agents","ai","claude-code","llm","mcp","memory","neo4j","pgvector","sqlite"],"created_at":"2026-04-27T22:00:47.916Z","updated_at":"2026-04-27T22:00:51.315Z","avatar_url":"https://github.com/bolnet.png","language":"Python","readme":"# Attestor\n\n**The memory layer for agent teams.** Self-hosted, deterministic retrieval, zero LLM in the critical path.\n\n[![PyPI](https://img.shields.io/pypi/v/attestor?label=PyPI\u0026color=C15F3C\u0026labelColor=1A1614)](https://pypi.org/project/attestor/)\n[![PyPI Downloads](https://img.shields.io/pypi/dm/attestor?label=installs%2Fmo\u0026color=C15F3C\u0026labelColor=1A1614)](https://pypi.org/project/attestor/)\n[![GitHub Stars](https://img.shields.io/github/stars/bolnet/attestor?style=flat\u0026label=stars\u0026color=C15F3C\u0026labelColor=1A1614)](https://github.com/bolnet/attestor/stargazers)\n[![Build](https://github.com/bolnet/attestor/actions/workflows/workflow.yml/badge.svg)](https://github.com/bolnet/attestor/actions/workflows/workflow.yml)\n[![Evals](https://github.com/bolnet/attestor/actions/workflows/evals.yml/badge.svg)](https://github.com/bolnet/attestor/actions/workflows/evals.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-1A1614.svg?labelColor=C15F3C)](LICENSE)\n\n```\npip install attestor\n```\n\n| | |\n|---|---|\n| **Version** | `4.0.0a1` (alpha; greenfield rebuild — no v3 migration path) |\n| **PyPI** | `attestor` |\n| **Import** | `attestor` |\n| **Live site** | \u003chttps://attestor.dev/\u003e |\n| **Repo** | \u003chttps://github.com/bolnet/attestor\u003e |\n| **License** | MIT |\n\n---\n\n## What it is\n\nAttestor is a memory store for agent teams that need a **shared, tenant-isolated memory** with **bi-temporal replay**, **deterministic retrieval**, and an **auditable supersession chain**. It runs as a Python library, a Starlette REST service, or an MCP server — same API in all three.\n\nIt is built around three claims, each grounded in code:\n\n1. **Bi-temporal — replay any past state.** Every memory has both event time (`valid_from` / `valid_until`) and transaction time (`t_created` / `t_expired`). Nothing is deleted; everything is queryable forever (`attestor/temporal/manager.py:43-73`, `core.py:888-890`).\n2. **Semantic-first retrieval, no LLM in the hot path.** A six-step deterministic pipeline. Same query → same ranking. Unit-testable (`attestor/retrieval/orchestrator.py:1-14`).\n3. **Conversation ingest with auditable conflict resolution.** Two-pass speaker-locked extraction, then a four-decision (`ADD / UPDATE / INVALIDATE / NOOP`) resolver per fact. Every supersession carries an `evidence_episode_id` (`attestor/extraction/conflict_resolver.py:98`).\n\n### Designed for\n\n- Multi-agent products where many LLMs write to the same memory store\n- Regulated chat systems that need point-in-time reconstruction (compliance, audit, FOIA-style queries)\n- Self-hosted deployments — your VPC, your Postgres, your Neo4j\n\n### *Not* designed for\n\n- A general-purpose vector database\n- A RAG framework with built-in chunking, reranking, and orchestration\n- An LLM agent runtime — Attestor is the memory backend; the agent loop is yours\n\n---\n\n## Quick start\n\n### 1. Install\n\n```bash\npip install attestor                 # or: pipx install attestor\n```\n\n### 2. Bring up local Postgres + Neo4j\n\n```bash\nattestor setup local                                       # writes attestor/infra/local/docker-compose.yml\ndocker compose -f attestor/infra/local/docker-compose.yml up -d\n```\n\nPostgres 16 ships with `pgvector` (document + vector roles). Neo4j 5 ships with GDS (graph role: PageRank, BFS, Leiden).\n\n### 3. Pull the default embedder\n\n```bash\nollama pull bge-m3                   # 1024-D, 8K context, local-first default\n```\n\nThe provider chain in `attestor/store/embeddings.py` checks `http://localhost:11434` first; cloud providers are fallbacks. Override via `ATTESTOR_EMBEDDING_PROVIDER` / `ATTESTOR_EMBEDDING_MODEL`.\n\n### 4. Verify (mandatory)\n\n```bash\nattestor doctor\n```\n\nAll four checks must be green for the default install: **Document Store**, **Vector Store**, **Graph Store**, **Retrieval Pipeline**. Graph (Neo4j) is required — the 6-step retrieval pipeline narrows on graph neighborhoods and the conversation ingest path writes typed edges (`uses`, `authored-by`, `supersedes`). The only hard dependency that *cannot* be down is the document store (Postgres); transient vector-probe failures are surfaced in the response trace rather than swallowed (`retrieval/orchestrator.py` — `vector_error` field).\n\n### 5. Use it\n\n```python\nfrom attestor import AgentMemory, AgentContext, AgentRole\n\nmem = AgentMemory()                  # picks up env / ~/.attestor.toml automatically\n\nctx = AgentContext(\n    agent_id=\"researcher-1\",\n    role=AgentRole.RESEARCHER,\n    namespace=\"acme-prod\",\n)\n\nmem.add(\n    content=\"Alice is the engineering manager\",\n    entity=\"alice\",\n    category=\"role\",\n    context=ctx,\n)\n\nresults = mem.recall(query=\"who runs engineering?\", context=ctx)\nfor r in results:\n    print(r.score, r.memory.content)\n```\n\n\u003e **SOLO mode (zero-config).** In v4, `AgentMemory().add('foo')` auto-provisions a singleton `local` user, an Inbox project (`metadata.is_inbox=true`), and a daily session — so the snippet above works on a fresh database without configuring identity (`core.py:179-209`). For multi-tenant production use, pass an explicit `AgentContext` with a real `namespace`.\n\n### 6. Run a smoke benchmark (optional)\n\nVerify your install end-to-end against a tiny LongMemEval slice. Defaults match the canonical benchmark stack: `openai/gpt-5.2` answerer, dual judges (`openai/gpt-5.2` + `anthropic/claude-sonnet-4.6`), `openai/gpt-5.2` distiller, OpenAI `text-embedding-3-large` truncated to 1024-D.\n\n```bash\nexport OPENAI_API_KEY=...\n.venv/bin/python scripts/lme_smoke_local.py --n 2\n```\n\nEvery model and parameter is overridable via env var or CLI flag. See `--help` for the full table.\n\n---\n\n## Architecture\n\n### Bi-temporal — replay any past state\n\nEvery memory carries two time axes:\n\n| Axis | Columns | Meaning |\n|------|---------|---------|\n| **Event time** | `valid_from`, `valid_until` | When the fact is true *in the world* |\n| **Transaction time** | `t_created`, `t_expired` | When the row landed *in the store* |\n\nPlus a `superseded_by` chain. Old facts are never deleted — they remain queryable forever (`attestor/temporal/manager.py:30-66`).\n\n```python\n# What did we believe on March 1?\nmem.recall(query=\"who runs engineering?\", as_of=\"2026-03-01T00:00:00Z\", context=ctx)\n\n# Show me everything we knew about Alice between Feb and Apr\nmem.recall(query=\"alice\", time_window=(\"2026-02-01\", \"2026-04-01\"), context=ctx)\n```\n\n`as_of` and `time_window` propagate end-to-end through the orchestrator and document store. Auto-supersession on write is wired into `core.py:add()` (`core.py:762, 784-785`): on every `add`, the temporal manager finds active rows with the same `(entity, category, namespace)` and different content, marks them `superseded`, sets `valid_until=now`, and links `superseded_by=\u003cnew_id\u003e`. Detection is rule-based string equality today.\n\n### Tenant isolation — Postgres Row-Level Security\n\nEvery tenant table (`users`, `projects`, `sessions`, `episodes`, `memories`, `user_quotas`, `deletion_audit`) carries a `tenant_isolation_*` policy keyed off the `attestor.current_user_id` session variable. An empty / unset value fails closed — no rows visible (`attestor/store/schema.sql:311-327`).\n\n\u003e **Honest disclosure.** Enforcement lives in **Postgres**, not Python. The `AgentRole` enum in `attestor/context.py:49-56` is metadata that flows onto memories for provenance; it does *not* gate operations in Python. RLS is what actually controls access. This is correct architecture for a memory backend, but worth knowing if you read the Python alone.\n\n### The retrieval pipeline — semantic-first, six steps\n\n`attestor/retrieval/orchestrator.py` runs the same six steps for every query:\n\n1. **Vector top-K** — pgvector cosine, k=50\n2. **Graph narrow** — Neo4j BFS depth ≤ 2 from each candidate's entity to the question entities; affinity bonus per hop (0-hop=+0.30, 1-hop=+0.20, 2-hop=+0.10; unreachable=−0.05). Discrete, not \"soft\".\n3. **Triples inject** — typed-edge facts (`uses`, `authored-by`, `supersedes`) injected as synthetic memories\n4. **MMR rerank** — λ=0.7\n5. **Confidence decay + temporal boost** — recency lifts; stale, low-confidence rows fall\n6. **Budget fit** — greedy monotonic-by-score pack into the caller's token budget\n\nEvery call writes a JSONL trace to `logs/attestor_trace.jsonl` (disable via `ATTESTOR_TRACE=0`).\n\n### Three storage roles\n\n| Role | Purpose | Default | Alternatives |\n|------|---------|---------|--------------|\n| **Document** | Source of truth (content, tags, entity, ts, provenance, confidence) | Postgres 16 | AlloyDB, ArangoDB, DynamoDB, Cosmos DB |\n| **Vector** | Dense embedding per memory | pgvector | AlloyDB ScaNN, ArangoDB, OpenSearch Serverless, Cosmos DiskANN |\n| **Graph** | Entity nodes + typed edges | Neo4j 5 + GDS | Apache AGE on AlloyDB, ArangoDB, Neptune, NetworkX (Azure) |\n\nPostgres is the source of truth. Neo4j is **derived state, rebuildable from Postgres** — but it's required for the canonical install: graph expansion is step 2 of the retrieval pipeline and conversation ingest writes typed edges. The only role that cannot be down is the document store; the orchestrator records transient vector-probe failures in the response trace (`vector_error`) instead of swallowing them.\n\n### Optional BM25 / FTS lane\n\nA trigger-maintained `content_tsv` tsvector + GIN index lifts queries that embeddings under-recall (acronyms, IDs, rare proper nouns). Enabled when v4 schema is detected; fuses with the vector lane via Reciprocal Rank Fusion (RRF, k=60). Graceful no-op on backends without the column (`core.py:122-130`).\n\n---\n\n## Conversation ingest\n\nThe heavyweight write path that turns conversation turns into auditable memories. `core.py:ingest_round(turn)` orchestrates four passes:\n\n```\nturn  →  extract_user_facts(user_turn)        ┐\n        extract_agent_facts(assistant_turn)   ┘  → resolve_conflicts → apply\n```\n\n### Two-pass speaker-locked extraction\n\n`attestor/extraction/round_extractor.py:216, 258` — separate prompts for user vs assistant turns. The user-turn extractor only emits facts attributable to the user; the assistant-turn extractor only emits facts the assistant introduced. Stops cross-attribution. The \"+53.6 over Mem0\" delta in our LongMemEval scores comes from this split.\n\n### Four-decision conflict resolver\n\n`attestor/extraction/conflict_resolver.py:40, 98` — for each newly-extracted fact, an LLM call against existing similar memories returns one of:\n\n| Decision | Effect |\n|----------|--------|\n| `ADD` | New info, no existing match — write fresh memory |\n| `UPDATE` | Same entity + predicate, refined value — keep existing id |\n| `INVALIDATE` | Old memory contradicted — mark superseded (timeline replays) |\n| `NOOP` | Already represented — skip |\n\nEach `Decision` carries `evidence_episode_id`. Every supersession is auditable. Failsafe: parse failure on a single fact yields `ADD`-by-default — better a duplicate-ish row than a silent drop.\n\n\u003e **Two write paths, two contracts.** `mem.add(...)` runs the lightweight rule-based supersession (§Bi-temporal). `mem.ingest_round(turn)` runs the full four-decision pipeline. Pick `ingest_round` for conversational data; pick `add` for structured writes where you've already done the conflict reasoning.\n\n### Sleep-time consolidation\n\n`mem.consolidate()` (`core.py:526`) re-extracts and synthesizes facts from recent episodes with a stronger model. Currently a Python-API-only call — no CLI command. Schedule it from your application (cron, systemd timer, ECS scheduled task) when you want fresher facts than the streaming extractor produces.\n\n### Reflection engine\n\n`attestor/consolidation/reflection.py` runs periodic synthesis across N episodes for one user. Outputs:\n\n- `stable_preferences` — patterns appearing in 3+ episodes\n- `stable_constraints` — rules the user repeatedly invokes\n- `changed_beliefs` — preferences that shifted (old → new, with explicit invalidate)\n- `contradictions_for_review` — flagged for **HUMAN REVIEW**, *not* auto-resolved\n\nThe \"do not auto-resolve\" stance is the load-bearing piece for regulated chat systems. The prompt is explicit (`reflection.py:35-66`): *\"Do NOT auto-resolve contradictions. Flag them for human review.\"*\n\n### Chain-of-Note reading\n\n```python\npack = mem.recall_as_pack(query=\"who runs engineering?\", context=ctx)\n# pack.memories : list of {id, content, validity_window, confidence, source_episode_id}\n# pack.prompt   : default Chain-of-Note prompt with NOTES → SYNTHESIS → CITE → ABSTAIN → CONFLICT structure\n```\n\nThe default prompt has explicit `ABSTAIN` and `CONFLICT` clauses — every frontier model defaults to confabulation otherwise.\n\n---\n\n## Multi-agent primitives\n\n### Six roles\n\n`AgentRole`: `ORCHESTRATOR`, `PLANNER`, `EXECUTOR`, `RESEARCHER`, `REVIEWER`, `MONITOR` (`attestor/context.py:49-56`). The role flows onto every memory's metadata for provenance. Access enforcement happens at the Postgres RLS layer (see §Tenant isolation).\n\n### AgentContext — handoff, scratchpad, trail\n\n```python\norchestrator = AgentContext.from_env(agent_id=\"orchestrator\", namespace=\"project:acme\")\nplanner      = orchestrator.as_agent(\"planner\",  role=AgentRole.PLANNER)\nexecutor     = planner.as_agent(\"executor\",      role=AgentRole.EXECUTOR)\n\n# Each child carries parent_agent_id + accumulating agent_trail.\n# All three share the same scratchpad: Dict[str, Any] for typed handoff data.\n```\n\n`as_agent()` creates a child context with `parent_agent_id`, full `agent_trail`, and a shared `scratchpad`. The trail accumulates — useful for proving \"this answer came from agent X who got it from agent Y.\"\n\n### Per-agent token budgets\n\n`AgentContext.token_budget` (default 20 000) is enforced — `recall()` packs results greedily until the budget is exhausted (`scorer.py:fit_to_budget`). `token_budget_used` accumulates across calls in a session.\n\n### Optional write quotas\n\n`mem.set_quota(user_id, daily_writes=...)` → enforced on `add` against the v4 `user_quotas` table (`core.py:592-621`). Optional; unset means unlimited.\n\n---\n\n## Security \u0026 Compliance\n\n### Row-Level Security\n\nCross-link to §Tenant isolation. RLS policies are the access-control surface; the Python layer trusts them. Set `attestor.current_user_id` per connection.\n\n### Provenance on every memory\n\nEvery memory carries `agent_id`, `session_id`, `source_episode_id`. The supersession chain (`superseded_by`) is preserved forever. Conversation episodes are stored verbatim, separate from the memories extracted from them — meaning you can always reconstruct *which conversation turn produced which fact*.\n\n### Deletion audit log\n\nHard deletes (e.g., GDPR purges) write a row to `deletion_audit` before the cascade — what was deleted, when, why, by whom. This is the carve-out for the otherwise-immutable schema.\n\n### GDPR — export and purge\n\n```python\nmem.export_user(external_id=\"user-42\")     # full data export (memories + episodes + sessions + projects)\nmem.purge_user(external_id=\"user-42\",      # cascading hard delete with audit trail\n               reason=\"GDPR right-to-erasure request 2026-04-27\")\nmem.deletion_audit_log(limit=100)          # forensic readback\n```\n\n`core.py:557-590`. v4 only. Returns / writes everything Subject Access requires for Art. 15 / Art. 17.\n\n### Optional: Ed25519 provenance signing\n\nEnable via config (`signing.enabled = true`). On every `add`, attestor signs the canonical payload `id || agent_id || t_created || content_hash` with an Ed25519 key. `mem.verify_memory(memory_id)` returns `bool` (`core.py:623-640`). Optional, off by default — turn on for adversarial-write contexts where you need cryptographic non-repudiation.\n\n---\n\n## Runtime topologies\n\nSame API across all three. Only configuration changes.\n\n| Mode | Shape | When to use |\n|------|-------|-------------|\n| **A — Embedded library** | `AgentMemory(config)` in-process; talks directly to Postgres + Neo4j | Single-process agents, scripts, notebooks |\n| **B — Sidecar** | `attestor api` on `localhost:8080`; language-agnostic HTTP client shares the same Postgres + Neo4j | Polyglot agents on one box (Python + TS + Go) |\n| **C — Shared service** | One Attestor service in front of an agent mesh (App Runner / Cloud Run / Container Apps) backed by managed Postgres + Neo4j | Production multi-agent platforms |\n\n```bash\nattestor api    --port 8080         # Mode B / C — Starlette ASGI REST (HTTP)\nattestor mcp    --path ~/.attestor  # MCP stdio server (zero-config; for Claude Desktop / Cursor / Windsurf)\nattestor serve  ~/.attestor         # MCP stdio server (positional-path variant; equivalent transport)\n```\n\n---\n\n## Backends\n\n| Backend | Document | Vector | Graph | Status |\n|---------|:--------:|:------:|:-----:|--------|\n| **Postgres + Neo4j** *(default)* | ✓ | pgvector | Neo4j + GDS | Production-ready |\n| **ArangoDB** | ✓ | ✓ | ✓ | Production-ready (one engine, all 3 roles) |\n| **AWS** | DynamoDB | OpenSearch Serverless | Neptune | Backend code + Terraform shipped |\n| **Azure** | Cosmos DB | Cosmos DiskANN | NetworkX (in-process) | Backend code shipped, Terraform forthcoming |\n| **GCP** | AlloyDB | AlloyDB ScaNN | AGE on AlloyDB | Backend code shipped, Terraform forthcoming |\n\nOverride the default via config:\n\n```toml\n# ~/.attestor.toml\nbackend = \"postgres+neo4j\"   # or \"arangodb\" | \"aws\" | \"azure\" | \"gcp\"\n```\n\nReference Terraform lives under `attestor/infra/`.\n\n---\n\n## Embeddings\n\nProvider auto-detect (`attestor/store/embeddings.py:get_embedding_provider`), in this order:\n\n1. **Local Ollama `bge-m3`** — 1024-D, 8K context — used when `http://localhost:11434` is reachable\n2. **Cloud-native** — Bedrock Titan / Vertex / Azure OpenAI when their SDK + creds are present\n3. **OpenAI `text-embedding-3-large`** (3072-D native; pin `OPENAI_EMBEDDING_DIMENSIONS=1024` for schema compat)\n4. **OpenRouter** — for federated runs\n\nLocal-first by design. Override:\n\n```bash\nexport ATTESTOR_DISABLE_LOCAL_EMBED=1            # skip the Ollama probe entirely\nexport ATTESTOR_EMBEDDING_PROVIDER=openai\nexport ATTESTOR_EMBEDDING_MODEL=text-embedding-3-large\n```\n\n---\n\n## CLI\n\n`attestor --help` lists everything. The most useful commands:\n\n| Command | Purpose |\n|---------|---------|\n| `attestor init` | Create a starter config |\n| `attestor setup local` | Generate Docker Compose for Postgres + Neo4j |\n| `attestor doctor` | Health-check every store + the retrieval pipeline |\n| `attestor add` / `recall` / `search` / `list` | CRUD-ish memory ops |\n| `attestor timeline` | Entity timeline (uses bi-temporal manager) |\n| `attestor stats` | Store statistics |\n| `attestor export` / `import` | JSON dump / restore |\n| `attestor compact` | Remove archived memories |\n| `attestor update` / `forget` | Mutate / archive a memory |\n| `attestor inspect` | Inspect raw database state |\n| `attestor api` | Start the Starlette REST API |\n| `attestor serve \u003cpath\u003e` | Start MCP stdio server (positional-path variant) |\n| `attestor mcp [--path …]` | Start MCP stdio server (zero-config; default for Claude Desktop / Cursor / Windsurf) |\n| `attestor ui` | Read-only browser UI for the store |\n| `attestor hook {session-start, post-tool-use, stop}` | Run a Claude Code lifecycle hook |\n| `attestor lme` / `locomo` / `mab` | Built-in benchmark runners (see §Evaluation) |\n\n---\n\n## MCP server\n\n`attestor mcp` (or `attestor serve \u003cpath\u003e`) exposes an MCP stdio server with eight tools:\n\n| Tool | Purpose |\n|------|---------|\n| `memory_add` | Write a memory with provenance |\n| `memory_get` | Fetch one memory by id |\n| `memory_recall` | Run the full retrieval pipeline |\n| `memory_search` | Filtered list (entity / category / time / namespace) |\n| `memory_forget` | Archive a memory by id |\n| `memory_timeline` | Chronology for an entity |\n| `memory_stats` | Store statistics |\n| `memory_health` | Per-role health snapshot — call this first when integrating |\n\nPlus MCP **resources** (memory listings) and **prompts** (canned recall prompts for IDE assistants).\n\n---\n\n## Hooks (Claude Code)\n\nThree lifecycle hooks ship in `attestor/hooks/`:\n\n- **`session_start`** — injects relevant memories into the session context based on cwd / repo\n- **`post_tool_use`** — auto-captures useful artifacts from `Write` / `Edit` / `Bash`\n- **`stop`** — writes a session summary on exit\n\nWire them up via the installer (next section) or by hand in `~/.claude/settings.json`.\n\n---\n\n## Install for Claude Code\n\nSingle instruction users can give Claude Code:\n\n```\ninstall attestor\n```\n\n(Or run `/install-attestor`.) The installer interviews you on:\n\n1. **Scope** — global (`~/.claude/.mcp.json`) vs project (`.mcp.json`)\n2. **Postgres connection** — local Docker, Neon, RDS, etc.\n3. **Neo4j connection** — local Docker, AuraDB, etc.\n4. **Backend override** — default `postgres+neo4j`, or `arangodb` / `aws` / `azure` / `gcp`\n5. **Embedding provider** — local Ollama (default), OpenAI, or cloud-native\n6. **Hooks** — whether to wire `session-start` / `post-tool-use` / `stop`\n7. **Namespace + default token budget**\n\nThen it installs `attestor` via pipx, writes the MCP config, optionally writes `settings.json` hooks, and runs `attestor doctor` to verify.\n\n---\n\n## Evaluation\n\n\u003e **Boundary statement.** The dual-LLM judge stack is a **benchmarking** mechanism, *not* the runtime contract. Recall in production is single-pipeline and deterministic. Multiple judges score answers in evaluation only — never in user-facing reads.\n\n| Runner | Source | Measures |\n|--------|--------|----------|\n| `attestor lme` | LongMemEval (Google's long-memory benchmark) | answer accuracy under long history, distillation, dual-judge cross-family |\n| `attestor locomo` | LoCoMo | conversational long-memory consistency |\n| `attestor mab` | MultiAgentBench | multi-agent coordination |\n| AbstentionBench (CI gate) | internal | when *not* to answer — known unknowns |\n| `scripts/lme_smoke_local.py` | dual-LLM smoke | quick install verification (see Quick Start §6) |\n\nThe smoke driver mirrors the canonical published-benchmark stack exactly. See `--help` for the full env-var / CLI-flag override matrix.\n\n---\n\n## Project layout\n\n```\nattestor/\n  core.py                  -- AgentMemory (main public API)\n  client.py                -- MemoryClient (HTTP drop-in for remote Attestor)\n  context.py               -- AgentContext, AgentRole, Visibility\n  models.py                -- Memory, RetrievalResult, ContextPack\n  cli.py                   -- attestor CLI entry point\n  api.py                   -- Starlette ASGI REST API\n  longmemeval.py           -- LongMemEval benchmark runner (dual-judge)\n  locomo.py                -- LoCoMo runner\n  doctor_v4.py             -- v4 schema + invariant validator\n  init_wizard.py           -- interactive install flow\n  store/\n    base.py                -- DocumentStore / VectorStore / GraphStore protocols\n    registry.py            -- backend selection\n    connection.py          -- config layering / env resolution\n    embeddings.py          -- provider auto-detect (Ollama / OpenAI / Bedrock / Vertex / Azure)\n    postgres_backend.py    -- pgvector (document + vector roles)\n    neo4j_backend.py       -- Neo4j + GDS (graph role)\n    arango_backend.py      -- all 3 roles in one\n    aws_backend.py         -- DynamoDB + OpenSearch Serverless + Neptune\n    azure_backend.py       -- Cosmos DB DiskANN + NetworkX\n    gcp_backend.py         -- AlloyDB pgvector + AGE + ScaNN\n    schema.sql             -- v4 Postgres schema (RLS, bi-temporal columns, content_tsv)\n  conversation/\n    ingest.py              -- ingest_round() pipeline\n  extraction/\n    round_extractor.py     -- 2-pass speaker-locked extraction\n    conflict_resolver.py   -- 4-decision contract (ADD/UPDATE/INVALIDATE/NOOP)\n    rule_based.py          -- deterministic fact extraction (no LLM)\n    prompts.py             -- shared prompt templates\n  consolidation/\n    consolidator.py        -- sleep-time re-extraction\n    reflection.py          -- cross-thread synthesis (stable patterns + flagged contradictions)\n  graph/\n    extractor.py           -- entity / relation extraction\n  retrieval/\n    orchestrator.py        -- 6-step semantic-first pipeline\n    tag_matcher.py\n    scorer.py              -- MMR, confidence decay, entity boost, fit-to-budget\n    trace.py               -- JSONL trace writer\n  temporal/\n    manager.py             -- timelines, supersession, contradiction detection, as_of replay\n  identity/\n    signing.py             -- Ed25519 provenance signing (optional)\n    defaults.py            -- SOLO mode auto-provisioning\n  mcp/\n    server.py              -- MCP server (tools, resources, prompts)\n  hooks/\n    session_start.py\n    post_tool_use.py\n    stop.py\n  ui/\n    app.py                 -- Starlette read-only viewer\n    static/, templates/    -- Evidence Board UI\n  utils/\n    config.py, tokens.py\n  infra/\n    local/                 -- Docker Compose (Postgres + Neo4j)\n    aws_arango/            -- Reference Terraform\ntests/                     -- Unit tests; live cloud tests env-gated\nevals/                     -- LongMemEval / LoCoMo / MultiAgentBench / AbstentionBench harnesses\ndocs/                      -- Architecture notes, ADRs\ncommands/                  -- /install-attestor, etc.\nscripts/                   -- lme_smoke_local.py, etc.\n```\n\n---\n\n## Development\n\n```bash\npoetry install\npoetry run pytest tests/ -q                          # unit tests, no external services needed\nATTESTOR_LIVE_PG=1 poetry run pytest tests/live -q   # live integration (env-gated)\n```\n\nStyle: `black` formatting, `isort` imports, `ruff` lint, `mypy` types. PEP 8, type-annotated signatures, dataclasses for DTOs. Many small files (200–400 lines typical, 800 max).\n\nConventions worth knowing:\n\n- Postgres is the source of truth. Neo4j is derived; rebuild it from Postgres if it drifts.\n- Non-fatal errors in vector / graph paths are caught and logged. The document path never silently breaks.\n- Configuration layering: env vars → `~/.attestor.toml` → in-code overrides.\n- Two write paths: `add()` for structured (lightweight rule-based supersession), `ingest_round()` for conversational (full 2-pass + 4-decision contract).\n\n---\n\n## Health check\n\nAlways call this first when integrating:\n\n```bash\nattestor doctor                  # CLI\n```\n\n```python\nmem = AgentMemory()\nprint(mem.health())              # Python API\n```\n\n```jsonc\n// MCP\n{ \"tool\": \"memory_health\" }\n```\n\nIt probes Document Store (Postgres), Vector Store (pgvector), Graph Store (Neo4j), and the retrieval pipeline. All four are required for the default topology — graph expansion is step 2 of the canonical pipeline, not an optional accelerator. Transient vector-probe failures surface in the `recall()` trace (`vector_error`) so callers can distinguish a degraded result from a clean one.\n\n---\n\n## Status \u0026 versioning\n\n- **Version:** 4.0.0a3 (alpha) — published to [PyPI](https://pypi.org/project/attestor/) and the [MCP Registry](https://registry.modelcontextprotocol.io/v0/servers?search=attestor) as `io.github.bolnet/attestor`\n- **v3 → v4:** greenfield rebuild on a v4-native Postgres schema with hard tenant isolation, bi-temporal facts, and a no-LLM retrieval critical path. **There is no automated migration.** v3 was alpha-only with no production users; drop your v3 DB and reinstall.\n- See [`CHANGELOG.md`](./CHANGELOG.md) for the full track-by-track changelog.\n\n---\n\n## License\n\nMIT. See [`LICENSE`](./LICENSE).\n\n\u003c!-- mcp-name: io.github.bolnet/attestor --\u003e\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbolnet%2Fattestor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbolnet%2Fattestor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbolnet%2Fattestor/lists"}