{"id":49369678,"url":"https://github.com/bolnet/attestor","last_synced_at":"2026-05-26T18:01:01.909Z","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-05-26T18:01:01.879Z","avatar_url":"https://github.com/bolnet.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Attestor\n\n**Cut your agent's token burn 21×. Two API calls.**\n\nFull-context replay re-reads the whole conversation every turn — input tokens that grow O(n²) and a bill that compounds with every session. Attestor retrieves only what's needed: flat ~200 tokens per call, 21× fewer input tokens by turn 100, 100% recall — measured across six models, open and closed.\n\n```python\nawait attestor.add(namespace, content)          # when new information arrives\nfacts = await attestor.recall(namespace, query) # ~200 flat tokens, always\n```\n\nSelf-hosted, deterministic retrieval, zero LLM in the critical path. The memory layer for agent teams that need shared, tenant-isolated memory with bi-temporal replay and an auditable supersession chain.\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\u003e **Using Claude Code?** Paste this repo's link and say *\"install attestor\"* — Claude follows the repo's install guide to scan your machine, bring up the backends, install the package, and wire the MCP server + hooks (it asks you for credentials when it needs them). See **[Install for Claude Code](#install-for-claude-code)**.\n\u003e\n\u003e ```\n\u003e https://github.com/bolnet/attestor  — install attestor to claude code\n\u003e ```\n\n| | |\n|---|---|\n| **Version** | `4.0.0` (stable; 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\u003e Designed and built by **[Surendra Singh](https://www.linkedin.com/in/singhsurendra/)** — building auditable infrastructure for multi-agent AI, with fifteen years of production-systems discipline brought to the memory layer. Companion projects: [`claude-finance`](https://github.com/bolnet/finance) (Claude-powered financial analytics) · [`private-equity`](https://bolnet.github.io/private-equity/) (PE × AI workshop). [Reach out](https://www.linkedin.com/in/singhsurendra/) if you're hiring senior IC for AI infrastructure.\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\n**The token math:** Full-context replay is O(n²) — every turn re-reads the whole history. Attestor replaces that with O(n) targeted retrieval. Per-call context stays flat at ~200 tokens whether the agent is on turn 1 or turn 100. One Claude Opus 4 session at 100 turns: $24.15 → $1.24. Verify it yourself with [context-clock](https://github.com/bolnet/context-clock).\n\n| Turn | Full-context replay | Attestor | Reduction |\n|---|---|---|---|\n| t24 | growing | ~200 tok | 5.6× |\n| t50 | growing | ~200 tok | 11× |\n| t100 | 8,709 tok/call | ~200 tok | **21.5×** |\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**Or pull the container** (introspection-grade image, single layer over `python:3.12-slim`, currently `linux/amd64`):\n\n```bash\ndocker pull ghcr.io/bolnet/attestor:latest      # recommended — anonymous pull, mirrored to all registries below\n```\n\nSame image is mirrored to:\n\n| Registry          | Pull address                                                            |\n| ----------------- | ----------------------------------------------------------------------- |\n| GHCR              | `ghcr.io/bolnet/attestor:latest`                                        |\n| Docker Hub        | `bolnet2025/attestor:latest`                                            |\n| Quay              | `quay.io/bolnet/attestor:latest`                                        |\n| AWS ECR Public    | `public.ecr.aws/m6h5j7o3/attestor:latest`                               |\n| GCP AR            | `us-central1-docker.pkg.dev/coral-marker-452616-n4/attestor/attestor:latest` |\n\n(An internal Azure ACR mirror exists at `memwright.azurecr.io/attestor` but is private — Azure customers should use `az acr import` from one of the public registries above.)\n\nThe image's default entrypoint is `attestor mcp` (MCP server over stdio). For full production use, point the container at an external Postgres + Neo4j via env vars (or compose them with `attestor/infra/local/docker-compose.yml`); override the entrypoint to run `attestor doctor`, `attestor api`, etc.\n\n### 2. Bring up local Postgres + Pinecone + Neo4j\n\n```bash\nattestor setup local                                       # writes attestor/infra/local/docker-compose.yml\n\n# Vector role: Pinecone Local is a SEPARATE container (not in the compose file) — start it first.\ndocker run -d --name pinecone-local -e PORT=5080 -e PINECONE_HOST=localhost \\\n  -p 5080-5090:5080-5090 --platform linux/amd64 \\\n  ghcr.io/pinecone-io/pinecone-local:latest\n\n# Compose loads its env file from the compose file's own directory, so pass the\n# repo-root .env explicitly (it holds PINECONE_API_KEY / VOYAGE_API_KEY / etc).\ndocker compose --env-file .env -f attestor/infra/local/docker-compose.yml up -d --build\n```\n\n\u003e **New here? Read [docs/LOCAL_DOCKER_SETUP.md](docs/LOCAL_DOCKER_SETUP.md)** for the full step-by-step\n\u003e container walkthrough, the exact verify command, and a Troubleshooting section covering the common\n\u003e first-run gotchas (missing `--env-file`, Pinecone Local not running, and the host-vs-container\n\u003e `localhost:5080` nuance).\n\nThe default stack ships **three containers** (one per storage role):\n\n| Container | Role | Port | Purpose |\n|---|---|---|---|\n| Postgres 16 | Document | `5432` | Source of truth — content, tags, entity, ts, provenance, RLS-isolated by `user_id` |\n| **Pinecone Local** | Vector | `5080-5089` | Dense embeddings, free per-namespace isolation, plain gRPC (no HTTPS) |\n| Neo4j 5 + GDS | Graph | `7687` | Entity nodes + typed edges, PageRank / BFS / Leiden |\n\n`pgvector` remains in the Postgres schema as an opt-in fallback for single-process / self-contained deploys, but the **default vector role is Pinecone** as of 2026-04-29 — it delivered the +10pp LME-S temporal-reasoning lift in the most recent bench-up.\n\n### 3. Configure the embedder\n\nThe default embedder is **Pinecone Inference `llama-text-embed-v2`** (NVIDIA-hosted, 1024-D) — one vendor for embedder + storage, free Starter tier (5M tokens/month per organization, see [§ Cost \u0026 runtime guide](#cost--runtime-guide)). It is declared in `configs/attestor.yaml` (`stack.embedder`, the single source of truth); set `PINECONE_API_KEY` in `.env` and the loader picks it up.\n\n```bash\necho \"PINECONE_API_KEY=pcsk_…\" \u003e\u003e .env       # cloud key for the embedder; storage can stay local\n```\n\n**Alternative providers** (override via `ATTESTOR_EMBEDDING_PROVIDER` / `ATTESTOR_EMBEDDING_MODEL`):\n- `voyage` — Voyage AI `voyage-4` (1024-D, paid)\n- `openai` — `text-embedding-3-small` (1024-D via Matryoshka)\n- `ollama` — `bge-m3` local-first (free, requires `ollama pull bge-m3`)\n\n### 4. Verify (mandatory)\n\n```bash\nattestor doctor\n```\n\nAll four checks must be green for the default install: **Document Store** (Postgres), **Vector Store** (Pinecone Local or Cloud), **Graph Store** (Neo4j), **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 come from `configs/attestor.yaml`: Pinecone Inference `llama-text-embed-v2` (1024-D) embedder + Pinecone vector store, `openai/gpt-5.5` answerer, dual judges (`openai/gpt-5.5` + `anthropic/claude-sonnet-4-6`), `parallel=2`.\n\n```bash\nset -a \u0026\u0026 source .env \u0026\u0026 set +a   # OPENROUTER_API_KEY, PINECONE_API_KEY, NEO4J_PASSWORD\n.venv/bin/python scripts/lme_smoke_local.py --n 2 --yes\n```\n\nEvery model and parameter comes from YAML — see [§ Benchmarking](#benchmarking) below for the full bench harness.\n\n---\n\n## Benchmarking\n\nEvery benchmark — smoke, single slice, full sweep, synthetic supersession — reads its knobs from two YAMLs:\n\n| File | What lives there |\n|---|---|\n| `configs/attestor.yaml` | Stack — embedder, models, retrieval features, DBs, registries, clouds |\n| `configs/bench.yaml` | Bench-only — variants, category iteration order, target scores, output paths |\n\nThe two files **must have disjoint keys**. The CI test `tests/test_config_no_duplicate_keys.py` enforces this; the bench loader (`attestor.bench_config.get_bench`) crashes on overlap. If you want a one-off override (different model for one bench run), use an env var or CLI flag — never duplicate the key in `bench.yaml`.\n\n### What LongMemEval is\n\n[**LongMemEval**](https://arxiv.org/abs/2410.10813) (Wu et al., 2024 — published at ICLR '25) is the canonical benchmark for memory-augmented chat assistants. It measures whether an AI system can correctly answer questions that require recalling facts from long, multi-session conversation histories — the exact scenario Attestor is built for.\n\n**500 questions, 6 reasoning categories, 3 haystack sizes.** Same questions across all three sizes; only the noise around the answer-bearing session changes:\n\n| Variant | Tokens / Q | Sessions | What it measures |\n|---|---|---|---|\n| **`oracle`** | ~3-15k | 1-3 gold | **Reasoning ceiling** — what the answerer can do with perfect retrieval. If you score low here, your prompt or LLM is broken (retrieval can't help). |\n| **`s`** (Standard / Small) | ~115k | ~50 | **Public leaderboard** — the canonical comparison. Fits in a single Claude/GPT context window, so Attestor's retrieval is benchmarked against the \"just stuff everything into long context\" baseline. |\n| **`m`** (Plus / Medium) | ~1M+ | ~500 | **Pure retrieval** — too big for any context window. Memory layer is forced; no long-context shortcut available. |\n\n**LME-S is the headline number to beat.** A memory layer that scores within 5% of a long-context baseline at 30× lower token cost is the marketing pitch.\n\n**The 6 reasoning categories** (cleaned LME-S, 500 questions total — note: no `abstention` slice in the cleaned split, which the synthetic [supersession suite](#synthetic-supersession-suite--python--m-evalsknowledge_updates) covers):\n\n| Category | N | What it tests |\n|---|---|---|\n| `multi-session` | 133 | Fact spans across multiple sessions — must track an entity over time |\n| `temporal-reasoning` | 133 | Date arithmetic (\"two weeks ago\", \"before X\") — *Attestor's bi-temporal layer is built for this slice* |\n| `knowledge-update` | 78 | **Supersession** — newer fact must beat older fact when both exist |\n| `single-session-user` | 70 | One session, fact stated by the user |\n| `single-session-assistant` | 56 | One session, fact stated by the assistant |\n| `single-session-preference` | 30 | One session, user preference |\n\n**Why this benchmark for Attestor:** the `temporal-reasoning` and `knowledge-update` slices directly exercise features that distinguish Attestor from a vanilla RAG: bi-temporal recall, supersession-on-contradiction, event-time vs transaction-time disambiguation. A high score on those slices is the regulated-AI / audit / compliance pitch.\n\nFor the published Attestor numbers, see `docs/bench/` — bench artifacts persist as `lme-{variant}-{category}-{date}.{report,summary}.json`. The `Reporting` section below shows how to render them as a table.\n\n### Download the LongMemEval dataset (one-time, before any bench run)\n\nAll `lme_*.sh` scripts use the cleaned LongMemEval split published on HuggingFace by [xiaowu0162/longmemeval-cleaned](https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned). It auto-downloads on first use, but you'll want to know what's happening.\n\n**Cache location** (created on first call):\n\n```\n~/.cache/attestor/longmemeval/\n```\n\n(Or `$XDG_CACHE_HOME/attestor/longmemeval/` if you set XDG_CACHE_HOME.)\n\n**Variants and on-disk sizes:**\n\n| Variant | Filename | Size | Tokens / Q | Use |\n|---|---|---|---|---|\n| `oracle` | `longmemeval_oracle.json` | ~5 MB | ~3-15k | Reasoning ceiling — cheapest smoke |\n| `s` | `longmemeval_s_cleaned.json` | ~250 MB | ~115k | **Public leaderboard** (canonical) |\n| `m` | `longmemeval_m_cleaned.json` | ~2 GB | ~1M+ | Forces retrieval (no long-context shortcut) |\n\n#### Option A — auto-download (recommended)\n\nJust run any bench command. The first call downloads and caches; every subsequent call reads from disk:\n\n```bash\n# Will download longmemeval_oracle.json (~5 MB) the first time\n.venv/bin/python scripts/lme_smoke_local.py --n 2 --yes --variant oracle\n\n# Will download longmemeval_s_cleaned.json (~250 MB) the first time\nscripts/bench/lme_run.sh knowledge-update\n```\n\nYou only pay the download cost once per variant. Internet flake during the first run? Delete the partial file in the cache dir and rerun.\n\n#### Option B — pre-warm the cache (offline / CI)\n\nPre-fetch every variant you plan to use before the bench day:\n\n```bash\n.venv/bin/python -c \"\nfrom attestor.longmemeval import load_or_download\nfor v in ('oracle', 's', 'm'):\n    samples = load_or_download(variant=v)\n    print(f'{v}: {len(samples)} samples')\n\"\n```\n\nExpected output:\n\n```\noracle: 500 samples\ns: 500 samples\nm: 500 samples\n```\n\n#### Option C — manual download (firewalled environments)\n\nIf your runner can't reach `huggingface.co`, fetch the files on a connected machine and drop them into the cache dir manually:\n\n```bash\nmkdir -p ~/.cache/attestor/longmemeval\ncd ~/.cache/attestor/longmemeval\n\n# pick the variants you need\ncurl -L -o longmemeval_oracle.json \\\n    https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_oracle.json\n\ncurl -L -o longmemeval_s_cleaned.json \\\n    https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json\n\ncurl -L -o longmemeval_m_cleaned.json \\\n    https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_m_cleaned.json\n```\n\nThe bench harness checks for these filenames exactly — don't rename them.\n\n#### Verify the dataset is loadable\n\nAfter download (auto or manual), confirm the loader picks it up cleanly:\n\n```bash\n.venv/bin/python -c \"\nfrom attestor.longmemeval import load_or_download\nfrom collections import Counter\nsamples = load_or_download(variant='s')\ncnt = Counter(s.question_type for s in samples)\nprint(f'Loaded {len(samples)} samples')\nfor cat, n in sorted(cnt.items(), key=lambda x: -x[1]):\n    print(f'  {cat}: {n}')\n\"\n```\n\nExpected for the cleaned `s` variant (500 questions, 6 categories — note: **no abstention slice in the cleaned split**):\n\n```\nLoaded 500 samples\n  multi-session: 133\n  temporal-reasoning: 133\n  knowledge-update: 78\n  single-session-user: 70\n  single-session-assistant: 56\n  single-session-preference: 30\n```\n\nIf counts don't match, the file is truncated — re-download.\n\n### Quick smoke (≤ 1 minute, ≤ $0.10)\n\nConfirm the pipeline runs end-to-end before committing or running anything bigger:\n\n```bash\n.venv/bin/python scripts/lme_smoke_local.py --n 2 --yes --variant oracle\n```\n\n`oracle` is the cheapest variant (gold sessions only, no distractor haystack). Schema is reapplied automatically; pass `--skip-schema` if you want to keep a populated DB between runs.\n\n### Single category — `scripts/bench/lme_run.sh`\n\n```bash\n# all 6 categories, current variant from bench.yaml (default: s)\nscripts/bench/lme_run.sh\n\n# one slice — full\nscripts/bench/lme_run.sh knowledge-update\n\n# one slice — capped at N samples (smoke)\nscripts/bench/lme_run.sh knowledge-update 10\n\n# one slice on a different variant (oracle = cheapest, m = ~1M tokens)\nscripts/bench/lme_run.sh knowledge-update \"\" oracle\n```\n\nValid `--category` values: `single-session-user`, `single-session-assistant`, `single-session-preference`, `multi-session`, `temporal-reasoning`, `knowledge-update`. See [What LongMemEval is](#what-longmemeval-is) above for sample counts and what each category tests.\n\nEach run persists two files:\n\n```\ndocs/bench/lme-{variant}-{category}-{YYYYMMDD}.report.json   # full LMERunReport\ndocs/bench/lme-{variant}-{category}-{YYYYMMDD}.summary.json  # BenchmarkSummary\n```\n\n### Full sweep — `scripts/bench/lme_all.sh`\n\nIterates `bench.yaml`'s `lme.categories` list in order. Adding/removing slices is a YAML edit, not a script edit:\n\n```bash\n# All 6 slices, current variant\nscripts/bench/lme_all.sh\n\n# All 6 slices, capped at 10 samples each (smoke)\nscripts/bench/lme_all.sh 10\n\n# All 6 slices on Oracle variant\nscripts/bench/lme_all.sh \"\" oracle\n```\n\nIf one slice fails, the script logs it and moves on to the next.\n\n### Reporting — `scripts/bench/lme_report.py`\n\nAggregates every `docs/bench/lme-*.summary.json` into one markdown table; picks the most-recent file per `(variant, category)`:\n\n```bash\n.venv/bin/python scripts/bench/lme_report.py                       # latest-per-slice\n.venv/bin/python scripts/bench/lme_report.py --variant s           # filter to LME-S\n.venv/bin/python scripts/bench/lme_report.py \\\n    --markdown-out docs/bench/LME-S.md                             # also write file\n.venv/bin/python scripts/bench/lme_report.py --trend               # progression over time\n```\n\nDefault mode (latest-per-slice):\n\n```\n| Variant | Category | Score | N | Date | Answer | Judges |\n| ------- | -------- | -----:| -:| ---- | ------ | ------ |\n| s | knowledge-update | 87.5% | 78 | 20260429 | openai/gpt-5.4-mini | openai/gpt-5.5, anthropic/claude-sonnet-4-6 |\n```\n\nTrend mode (`--trend`) reads `docs/bench/trend.csv` — one row appended per bench run (auto-populated by `lme_run.sh`) — and shows progression with a `Δ` column:\n\n```\n| Variant | Category | Date | N | Score | Δ | SHA | Features | Run |\n| ------- | -------- | ---- | -:| -----:| -:| --- | -------- | --- |\n| s | knowledge-update | 20260429 | 78 | 80.0% |       | a126e7a |               | bench |\n| s | knowledge-update | 20260430 | 78 | 88.0% | +8.0  | badcf1b | multi_query   | bench |\n| s | knowledge-update | 20260501 | 78 | 91.5% | +3.5  | xxxxxxx | multi_query,hyde | bench |\n```\n\nThe `Features` column records exactly which retrieval/answerer flags were enabled per run, so you can see at a glance which knob produced which lift.\n\n### Retrieval + answerer feature flags\n\nFive orthogonal features land via `configs/attestor.yaml` boolean flips. All disabled by default — pick one per bench run, measure the lift, decide which to ship enabled.\n\n| Flag | What it does | Lift | Cost overhead |\n|---|---|---|---|\n| `retrieval.multi_query` | rewrite question into N paraphrases, RRF-merge N+1 vector lanes | +6-10% (lit.); regressed −10pp on LME-S temporal smoke | 1 small LLM call + N extra vector searches per recall |\n| `retrieval.hyde` | event-descriptive hypothetical-document embedding (temperature=0) — embed it as a parallel vector lane | **+10pp measured** on LME-S temporal-reasoning (30q smoke, 70%→80%→96.7% with BM25 hybrid) | 1 small LLM call + 1 extra vector search per recall |\n| `retrieval.temporal_prefilter` | regex-detect \"two weeks ago\" etc; narrow event-time window before vector | +1.5% (lit.); 0pp on LME-S interrogative-anchor questions | Free (regex-only, no LLM) |\n| `self_consistency` | answerer draws K=5 samples at temperature, elects consensus | +3-6% (lit.) | 5× answerer cost |\n| `critique_revise` | answer → critique → conditional revise | +3-5% (lit.) | ~3× answerer worst case |\n\n`multi_query` and `hyde` are mutually exclusive in this release (multi_query wins if both flags are on with a logged warning). `self_consistency` and `critique_revise` are similarly mutually exclusive on the answerer side. Combinations across the two sides (e.g. `hyde + self_consistency`) are fine.\n\n**HyDE v2 prompt** (`attestor/retrieval/hyde.py`) — generates an event-descriptive snippet rather than an answer-shape response, so the embedding lands close to source-shape conversation turns instead of question-shape queries. This is the lever that produced the +10pp measured lift on LME-S temporal-reasoning. `temperature=0` is pinned so re-runs are deterministic.\n\n**Honest negative results documented above** — `multi_query` and `temporal_prefilter` did NOT generalize from their literature numbers on the LME-S temporal-reasoning slice. `multi_query` paraphrases stay in question-shape and RRF dilutes marginal hits; `temporal_prefilter` heuristic anchors don't help interrogative-form questions (\"how many days ago…\"). HyDE was the right tool. Per-feature methodology + diagnostic artifacts in `docs/bench/pinecone-lme-temporal-diagnostic-{baseline,mq3,hyde,hyde-bm25}-20260429.json`.\n\n**Cross-vector-DB diagnostic harness** — `experiments/pinecone_lme_temporal_diagnostic.py` runs retrieval-only LME-S diagnostics against Pinecone Local with `--baseline` / `--multi-query` / `--hyde` / `--bm25-hybrid` / `--temporal-prefilter` / `--category` flags. No answerer, no judge — pure recall@K ceiling. `--skip-ingest` reuses populated namespaces for fast retrieval-flag iteration (~60s for 30q vs ~50min with fresh ingest).\n\n**To benchmark a single feature:** flip its `enabled: true` in `configs/attestor.yaml`, run the bench slice, compare against a same-day baseline run with everything off. The trend table will show the delta in the `Δ` column.\n\n### Synthetic supersession suite — `python -m evals.knowledge_updates`\n\n50 hand-curated cases, 10 contradiction categories × 5 each (numeric, categorical, temporal, preference, entity, locational, intent, relational, count, status_binary). Each case ingests two sessions (Session 1 states a fact, Session 5 contradicts it) and asks a question that should resolve to the **newer** fact. Metric: % of cases where retrieval surfaces the new fact as top-1.\n\n```bash\n# All 50 cases — ~5 min, ~$0.50 worth of embedding calls\n.venv/bin/python -m evals.knowledge_updates\n\n# Smoke — first 5 cases\n.venv/bin/python -m evals.knowledge_updates --limit 5\n\n# Custom fixtures\n.venv/bin/python -m evals.knowledge_updates --fixtures my_cases.json\n```\n\nOutputs:\n\n```\ndocs/bench/knowledge-updates-{YYYYMMDD}.report.json   # per-case verdicts (new_wins | stale_wins | miss | ambiguous)\ndocs/bench/knowledge-updates-{YYYYMMDD}.summary.json  # aggregate score + per-category breakdown\n```\n\nTarget score (configurable in `bench.yaml`): **92%** new_wins. Below that, the supersession-confidence-decay weight in `attestor/retrieval/scorer.py` needs tuning.\n\n### Cost \u0026 runtime guide\n\nApproximate, at `reasoning_effort=high` for answerer + judge, `parallel=2`, OpenRouter pricing:\n\n| Run | N | Wall time | Cost |\n|---|---|---|---|\n| Quick smoke | 2 oracle | ~1 min | \u003c $0.10 |\n| `knowledge-update` slice | 78 | ~30-60 min | ~$3-5 |\n| `temporal-reasoning` slice | 133 | ~50-100 min | ~$5-8 |\n| Full LME-S 500q | 500 | ~75-180 min | ~$20-30 |\n| Synthetic supersession | 50 | ~5 min | ~$0.50 (embeddings only) |\n\nTo cut costs, edit `configs/attestor.yaml`'s `models.reasoning_effort.{answerer,judge}` from `high` → `medium` or `low`.\n\n### Configuration cheat sheet — `configs/bench.yaml`\n\n```yaml\nbench:\n  lme:\n    variant: s                    # s | m | oracle\n    cache_dir: ~/.cache/attestor/lme\n    output_dir: docs/bench\n    sample_limit: null            # null = full dataset; int = truncate\n    category: null                # null = all 7; or single slice name\n    categories: [...]             # iteration order for lme_all.sh\n    variants_to_run: [...]        # for full size matrix\n\n  knowledge_updates:\n    fixtures_path: evals/knowledge_updates/fixtures.json\n    n_cases: 50\n    target_score: 0.92\n    categories: [numeric, categorical, ...]\n\n  report:\n    headline_slice: abstention\n    trend_csv: docs/bench/trend.csv\n    markdown_path: docs/bench/LME-S.md\n```\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** — Pinecone cosine, k=50 (pgvector remains as opt-in fallback for self-contained deploys)\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### Async retrieval — lower latency without weakening audit\n\nIndependent recall steps run concurrently via `asyncio.gather`, but **none of the eight audit invariants are relaxed.** You don't trade trust for speed — you get both.\n\n| Async step | Latency win | Audit invariant preserved |\n|---|---|---|\n| HyDE LLM call ‖ original-question vector embed | −33 % on HyDE-enabled recalls (~600 ms → ~400 ms in the simulated unit-test) | **A7** — generator pins `temperature=0.0`, same prompt + same model = same hypothetical = same RRF order. Async amplifies non-determinism risk if T \u003e 0; we explicitly pin T=0. |\n| Per-lane vector searches in parallel (HyDE / multi-query) | proportional to N (≈ N × per-lane → max-per-lane) | RRF over the lanes is deterministic given identical inputs — gather order does **not** corrupt rank positions (`test_multi_query_async_preserves_RRF_order`). |\n| Self-consistency K-fanout (answerer side) | 5× on K=5 sampling | Vote consensus is order-independent; answerer-side change, doesn't touch the document store. |\n| Vector ‖ BM25 ‖ graph candidate-fetch | −20 % on baseline recalls | **A2** `recall_started_at` ceiling — every cross-store read carries the same monotonic timestamp captured at recall start. Concurrent writes that land mid-recall are simply not visible. |\n| Graph BFS ‖ Postgres doc-fetch | −50 ms typical | Same ceiling. |\n\n**Write-side stays sync.** All `add()`, `update()`, supersession writes are explicitly **non-goals** for the async refactor — the audit chain depends on serial write ordering and the bi-temporal `t_created` order must be linearizable per row. Async is read-side only.\n\n**Trace stays reconstructable.** Every event carries `recall_id` + monotonic `seq` + optional `parent_event_id`, so the audit dashboard renders concurrent recalls as a tree of events rather than a stream — `(recall_id, seq)` reconstructs causal order from the JSONL log.\n\n**Same `recall(as_of=X)` replay guarantee.** A past recall remains byte-for-byte reproducible from the bi-temporal columns + `deletion_audit` + the trace JSONL — async parallelism doesn't change what gets read, only when. The load-bearing test (`tests/test_as_of_replay.py`) is in the regression gate of every async PR.\n\nFull design + audit-invariant matrix: [`docs/plans/async-retrieval/PLAN.md`](docs/plans/async-retrieval/PLAN.md). Convention: every async PR ships with an audit-preservation argument and the matching invariant test (`tests/async_retrieval/test_audit_invariants_under_async.py`) GREEN before merge.\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 | **Pinecone** (Local Docker / Cloud) | 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. **Pinecone vectors and Neo4j graph are derived state, both rebuildable from Postgres** — but both are required for the canonical install: vector cosine is step 1 of the retrieval pipeline, graph expansion is step 2, 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`recall()` returns a list. `recall_as_pack()` returns a **typed retrieval envelope** an agent can actually reason about — every field a Chain-of-Note flow needs to cite, abstain, or pick the right validity window when memories conflict:\n\n```python\npack = mem.recall_as_pack(query=\"who runs engineering?\", context=ctx)\n\nfor entry in pack.memories:\n    print(entry.id,                    # cite this in the answer\n          entry.confidence,            # weight or abstain\n          entry.valid_from,            # bi-temporal window for conflict resolution\n          entry.valid_until,\n          entry.source_episode_id)     # provenance back to the round it came from\n\nagent.send(pack.render_prompt())       # Chain-of-Note prompt, memories interpolated as JSON\n```\n\n`ContextPack` is `frozen=True`, hashable, JSON-serializable. It drops cleanly into a tool call. The 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 is two-layer:**\n\n- **AgentContext layer** — `ROLE_PERMISSIONS` matrix gates writes / forgets per role. Matrix: `ORCHESTRATOR = R+W+F`; `PLANNER` / `EXECUTOR` / `RESEARCHER` = `R+W`; `REVIEWER` / `MONITOR` = `R` only. `read_only=True` is an independent kill switch.\n- **Postgres RLS layer** — row-level filter on `user_id` (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\n**The one instruction.** Open Claude Code, paste this repo's URL, and say **\"install attestor\"**:\n\n```\nhttps://github.com/bolnet/attestor  — install attestor to claude code\n```\n\nClaude Code reads this repo's install guide ([`docs/INSTALL.md` → Chapter 00](docs/INSTALL.md#chapter-00--install-via-claude-code-recommended)), then **scans your machine first, looks up current docs via Context7, and installs** — it brings up the three backend containers, installs the `attestor` package, wires the MCP server + hooks, and verifies. It assumes you start with nothing installed.\n\n**One install path — the guided wizard** ([`commands/install-attestor.md`](commands/install-attestor.md)). Whatever you type, Claude reads this repo and runs the wizard end-to-end: scan → package → Postgres + Pinecone + Neo4j backends → MCP server + hooks → `attestor doctor`. Four ways to launch it — type any of:\n\n| # | Way | Type this |\n|---|-----|-----------|\n| 1 | Plugin (recommended) | `/plugin install attestor` |\n| 2 | Command | `/install-attestor` |\n| 3 | Repo URL | `github.com/bolnet/attestor` |\n| 4 | Natural language | `install attestor` |\n\n\u003e All four launch the same guided wizard. First-time plugin use needs `/plugin marketplace add bolnet/attestor` once. The plugin also auto-wires the MCP server + hooks; the wizard handles package + backends + verify.\n\n**Memory is isolated per project automatically** — each working directory (git root, else cwd) is its own hard-isolated tenant, so projects never share memory. No namespace to configure.\n\nThe backends come up as three clearly-named Docker instances, one per storage role:\n\n| Container | Type | Storage role |\n|---|---|---|\n| `attestor-postgres` | Postgres 16 + pgvector | Document — source of truth |\n| `attestor-pinecone` | Pinecone Local | Vector — embeddings |\n| `attestor-neo4j` | Neo4j 5 + GDS | Graph — PageRank / BFS |\n\n\u003e These are the names the Chapter 00 cold-start installer uses (public images). The legacy `attestor/infra/local/docker-compose.yml` dev stack uses different names (`attestor-pg-local`, `attestor-neo4j-local`, Pinecone started separately) — see [`docs/INSTALL.md`](docs/INSTALL.md) Chapter 01.\n\n---\n\n## Install as a Skill (2026 agent SDKs)\n\nAttestor ships with a canonical `SKILL.md` at [`skills/attestor-memory/SKILL.md`](skills/attestor-memory/SKILL.md). Both Anthropic (`skills-2025-10-02`) and OpenAI's Responses API converged on this format — a markdown file with YAML frontmatter — for distributing reusable agent expertise. The wheel ships the SKILL.md, so every 2026-grade harness can auto-discover it after a single `pip install attestor`.\n\nThe skill teaches the agent the six core primitives (`recall`, `add`, `timeline`, `current_facts`, `forget`, `audit`) plus the v4 enterprise surface (bi-temporal `as_of` replay, RBAC roles, namespace isolation, provenance signing, GDPR export / purge). Every code example references methods that actually exist on `attestor.AgentMemory`, and a CI test (`tests/test_skill_md.py`) keeps the SKILL.md from drifting from the live API.\n\nTo pin the contract in your own host:\n\n```bash\npip install attestor\npython -c \"import attestor, importlib.resources as r; print(r.files('attestor'))\"   # confirm wheel installed\n# Point your agent harness at the bundled SKILL.md or read it directly:\npython -c \"from pathlib import Path; import attestor; \\\n  print((Path(attestor.__file__).parent.parent / 'skills' / 'attestor-memory' / 'SKILL.md').read_text())\"\n```\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.0 (stable) — 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`. `pip install attestor` returns 4.0.0 (no `--pre` flag needed).\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\u003c!-- attestor-provenance: a41a7f112fe6d6e0 — original repository: https://github.com/bolnet/attestor (Surendra Singh, initial commit d06f954b on 2026-03-07). If this exact comment appears unmodified in another project's source, that project copied this file. --\u003e\n\n","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"}