# Project export: Perseus Vault Codex

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Codex never forgets — persistent, encrypted, local-first memory for your Codex agent, in one pip install.
- Devpost: https://devpost.com/software/perseus-vault-codex
- GitHub: https://github.com/Perseus-Computing-LLC/perseus-vault-codex
- Video: https://www.youtube.com/embed/8WkXThyzyRE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Claude Opus 4.8 (5 commits)

## Devpost submission (written by the team)

### Overview

What it is Perseus Vault Codex is an MCP server that gives any OpenAI Codex agent persistent, encrypted, local-first memory. Install it and Codex gains five memory tools — perseus_remember, perseus_recall, perseus_forget, perseus_reflect, perseus_status — so it remembers your project's conventions, past decisions, and debugging context across every session. Under the hood it wraps Perseus Vault: a single 12 MB binary, fully local, AES-256-GCM encrypted at rest, with FTS5 keyword + hybrid recall and no API keys, no cloud dependency, and no telemetry.

### Inspiration

Every Codex session starts from zero. The agent re-learns your build commands, re-discovers your conventions, and re-derives the same architectural context you explained an hour ago. Memory is the missing primitive for coding agents — and the existing memory stores don't fit a developer's machine. mem0 is cloud-dependent, cognee is Python-only with no encryption at rest, Letta doesn't encrypt local storage, and Chroma is a vector DB rather than structured agent memory. None are single-binary, zero-infra, and encrypted. Developer memory — your unreleased code, your architecture, your secrets-adjacent context — is exactly the kind of data that should never leave the machine unencrypted. So we built the local, encrypted answer and wired it natively into Codex.

### What it does

Remembers across sessions. perseus_remember stores a fact, decision, convention, or gotcha, keyed so re-learning updates rather than duplicates. Recalls on demand. perseus_recall retrieves relevant context with FTS5 keyword + hybrid ranking. A new session picks up exactly where the last left off. Forgets cleanly. perseus_forget soft-deletes stale or wrong memories (recoverable). Reflects. perseus_reflect gathers grounding memories and uses your OpenAI/GPT-5.6 key to synthesize a cited insight — and degrades gracefully to returning the assembled context when no LLM is configured. Reports. perseus_status shows memory count, that encryption is active, and where the local DB lives. Zero config: on first run it auto-creates an encrypted vault at ~/.perseus-vault/codex/memory.db and generates the AES-256-GCM key itself. One command wires it into Codex: perseus-vault-codex-setup.

### How we built it

— and how Codex + GPT-5.6 were used Codex was used as an implementation and verification partner during Build Week. This final review session does not claim authorship of the pre-existing core; it read the complete implementation and test suite, then independently exercised and hardened it. Reviewed the real integration. Codex traced the Codex-facing JSON-RPC server, five-verb translation layer, zero-config setup, and the vendored client that starts the perseus-vault subprocess. Verified, rather than assumed. It ran all 32 tests against perseus-vault 2.17.0, ran the two-session demo, and confirmed a memory saved before a complete client teardown was recalled by a new client. Checked encryption at rest directly. It wrote a unique marker to the default ~/.perseus-vault/codex/memory.db, read the database as raw bytes, and confirmed that marker was absent before soft-deleting the audit memory. Hardened a transport edge case. On an unexpected vault stdout EOF, the client now tears down its unusable subprocess before raising. A regression test confirms the next call can auto-respawn instead of repeatedly receiving EOF. Measured and challenged the benchmarks. Codex ran the real encrypted-vault benchmarks and documented their limits: latency/recall numbers apply to the supplied synthetic workload; the token-savings percentage is scenario modeling, not a universal observed outcome. perseus_reflect can use the user's configured OpenAI-compatible model to synthesize recalled memories; it falls back to inspectable context when no LLM endpoint is configured. Built with: Python (zero runtime deps), the Model Context Protocol (JSON-RPC 2.0 over stdio), Perseus Vault (Rust single binary, SQLite + FTS5, AES-256-GCM), OpenAI Codex, GPT-5.6.

### Challenges we ran into

stdout is the protocol. In an MCP stdio server, a single stray print to stdout corrupts the JSON-RPC stream. We routed all logging to stderr and added tests that drive the full serve() loop over fake streams to catch regressions. Not blocking Codex at startup. Codex calls tools/list during startup; if we spawned the vault binary eagerly, a missing binary would hang the session. We made the vault start lazily on the first real tool call. Graceful LLM degradation. When the reflect LLM call fails, the vault returns an error as an ordinary text block. We detect that and fall back to context-only mode instead of surfacing an error string as an "answer." Collapsing 55+ tools to 5. The hard product call was restraint — a coding agent shouldn't reason about 55 memory tools. Choosing the right five verbs was the design. Accomplishments we're proud of One pip install + one setup command + zero config = a Codex agent with encrypted persistent memory. Encryption at rest, on by default, proven by a test that asserts memory plaintext never appears in the on-disk database. Zero runtime Python dependencies — the whole wrapper is self-contained. A real, non-trivial MCP integration with a 31-test suite, verified end-to-end against the real Perseus Vault binary. Measured benchmarks, not claims benchmarks/): recall at p50 8 ms / 5-of-5 recall@10 on a 10k-memory corpus, and a 72.5% context-token reduction over a 30-session horizon vs. re-priming each session — every figure measured against the real binary or labeled as a stated assumption.

### What's next

Auto-recall hooks so Codex pulls relevant memory into context without an explicit call. Team memory: shared encrypted vaults synced via the Vault's export/import. Publishing to the OpenAI MCP server registry.

## README (from the GitHub repository)

# Perseus Vault Codex

**Persistent, encrypted, local-first memory for OpenAI Codex agents.**

> Codex never forgets. Perseus Vault gives your Codex agent persistent encrypted
> memory — so it remembers your project conventions, past decisions, and
> debugging context across every session.

[![CI](https://github.com/Perseus-Computing-LLC/perseus-vault-codex/actions/workflows/ci.yml/badge.svg)](https://github.com/Perseus-Computing-LLC/perseus-vault-codex/actions/workflows/ci.yml)
License: MIT | Built for **OpenAI Build Week** — Developer Tools track

---

## The problem

Every Codex session starts from zero. The agent re-learns your build commands,
re-discovers your conventions, and re-derives the same architectural context you
explained yesterday. Memory is the missing primitive for coding agents.

Existing memory stores don't fit a developer's machine: **mem0** is
cloud-dependent, **cognee** is Python-only with no encryption at rest, **Letta**
manages memory but doesn't encrypt local storage, **Chroma** is a vector DB, not
structured agent memory. None are single-binary, zero-infra, and encrypted.

## The answer

`perseus-vault-codex` is a tiny MCP server that wraps [**Perseus Vault**](https://github.com/Perseus-Computing-LLC/perseus-vault)
— a single 12 MB binary, fully local, **AES-256-GCM encrypted at rest**, with
FTS5 keyword + hybrid recall and **no API keys, no cloud, no telemetry**. Install
it and any Codex session gains five memory tools:

| Tool | What it does |
|------|--------------|
| `perseus_remember` | Save a fact, decision, convention, or gotcha across sessions. |
| `perseus_recall` | Retrieve relevant past context (FTS5 + hybrid ranking). |
| `perseus_forget` | Remove a stale or wrong memory. |
| `perseus_reflect` | Synthesize an insight from stored memories (RAG via your OpenAI/GPT-5.6 key). |
| `perseus_status` | Store health: memory count, encryption state, DB location. |

## Install

```bash
# 1. Install the wrapper (zero Python dependencies)
pip install perseus-vault-codex            # from PyPI, or from source (below)

# 2. Install the Perseus Vault binary (single static binary, no deps) and put it on PATH
#    https://github.com/Perseus-Computing-LLC/perseus-vault/releases

# 3. Register it with Codex (non-destructive; backs up your config)
perseus-vault-codex-setup
```

**Install from source (works today, before the PyPI release):**

```bash
git clone https://github.com/Perseus-Computing-LLC/perseus-vault-codex
cd perseus-vault-codex
pip install -e .
perseus-vault-codex-setup
```

That's it. On first run the server auto-creates an encrypted vault at
`~/.perseus-vault/codex/memory.db` — **no config required**.

## Configure in Codex

`perseus-vault-codex-setup` writes this into `~/.codex/config.toml`:

```toml
[mcp_servers.perseus-vault]
command = "perseus-vault-codex"
args = []
```

Restart Codex (or start a new session) and the five `perseus_*` tools are
available. To enable `perseus_reflect` synthesis, expose your OpenAI key:

```toml
[mcp_servers.perseus-vault.env]
OPENAI_API_KEY = "sk-..."
OPENAI_MODEL = "gpt-5.6"
```

See [`examples/codex_config.toml`](examples/codex_config.toml) for every option.

## Example usage

Inside a Codex session:

```
you> Set up the project. First, check what you already know.

codex> [calls perseus_recall {"query": "project build and test conventions"}]
       I recall from a previous session:
       - This project formats with `ruff format` and lints with `ruff check`.
       - We chose SQLite + FTS5 over Postgres for the local dev store.
       Running `ruff check` before I start...

you> We just decided to switch auth to short-lived JWTs with refresh tokens in Redis.

codex> [calls perseus_remember {"category": "decision", "key": "auth",
        "text": "Auth uses short-lived JWTs; refresh tokens live in Redis."}]
       Remembered. I'll apply that convention going forward — including in
       future sessions.
```

Tomorrow, in a **brand-new session**, `perseus_recall "auth"` returns that
decision. The context survived.

### Try the demo

```bash
PERSEUS_VAULT_BIN=/path/to/perseus-vault python scripts/demo.py
```

It simulates two separate Codex sessions: session 1 learns three project facts
and tears the vault process down completely; session 2 — a fresh process —
recalls them, reflects on them, and reports encrypted status. Sample output is
in [`docs/`](docs/architecture.md).

## Architecture

```
Codex (GPT-5.6)  ──MCP stdio──▶  perseus-vault-codex  ──MCP stdio──▶  perseus-vault binary
                    5 tools        (this package)        55+ tools      SQLite+FTS5, AES-256-GCM
```

Two hops on purpose: Perseus Vault exposes 55+ low-level memory tools; this
package collapses them into five verbs a coding agent can reason about, and the
binary does the encrypted storage and retrieval. Full write-up:
[`docs/architecture.md`](docs/architecture.md).

## Benchmarks

Measured against the real `perseus-vault` binary (v2.17.0), encrypted at rest —
full methodology and reproducible harness in [`benchmarks/`](benchmarks/):

- **Recall is fast and accurate at scale.** Seeding 10,000 developer memories,
  recall runs at **p50 7 ms** (p95 27 ms) with **5/5 recall@10** on distinctive
  needle memories (1,000-memory corpus: p50 1.3 ms). The recall hot path — what a
  Codex agent hits every task — stays in single/low-double-digit milliseconds.
- **The engine scales to 1,000,000 memories.** A separate 2× H100 validation
  (run `#619`, [`results/scale_1m_2xh100.json`](benchmarks/results/scale_1m_2xh100.json))
  embedded ~1M memories (995,562 persisted, 0 errors) and hit **hybrid recall@5 =
  recall@10 = 1.00** over 2,000 semantic queries, at sub-second latency (p50
  479 ms). This is an engine-scale result on GPU, not the laptop path — reported
  separately and honestly (keyword-only recall is near-zero on that semantic
  workload; hybrid carries it).
- **Persistent memory cuts context tokens ~72%.** Over a 30-session horizon,
  recalling the top-k relevant memories per task uses **110,493 fewer tokens
  (72.5% reduction)** than re-priming each new session with the full project
  knowledge base — per-unit token costs measured with tiktoken against real vault
  recalls.

Every number is measured or explicitly labeled as a stated assumption; nothing is
hardcoded. Reproduce with `python benchmarks/bench_recall.py` and
`python benchmarks/bench_token_savings.py`.

## How Codex was used

Codex was used during Build Week as an implementation and verification partner.
In the final review session, it read the complete wrapper and its tests, ran the
suite against the real `perseus-vault 2.17.0` binary, exercised the two-session
demo, and checked a unique marker was absent from the raw default database file.
It also hardened stdout-EOF recovery in the subprocess client and added a
regression test. Those are review-session contributions; this README does not
attribute all pre-existing code to that session.

See [`SUBMISSION.md`](SUBMISSION.md) for the precise verification record and
benchmark caveats.

## Development

```bash
git clone https://github.com/Perseus-Computing-LLC/perseus-vault-codex
cd perseus-vault-codex
pip install -e ".[dev]"
pytest -q                                   # unit tests (no binary needed)
PERSEUS_VAULT_BIN=/path/to/perseus-vault pytest -q   # + integration tests
```

## About

Built by [Perseus Computing LLC](https://perseus.observer). Perseus Vault is the
only fully-local, encrypted memory store for AI agents, with existing
integrations for Haystack, LangChain, LlamaIndex, CrewAI, Pydantic AI, and
Google ADK. MIT licensed.


## Detected evidence (automated analysis)

Indexed codebase: 25 recognized source files, 117 KB.
- Python (language) — detected in the code
- Rust (language) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (34 of 34)

```
.gitattributes
.github/workflows/ci.yml
.gitignore
AGENTS.md
benchmarks/_corpus.py
benchmarks/bench_recall.py
benchmarks/bench_token_savings.py
benchmarks/README.md
benchmarks/results/recall.json
benchmarks/results/scale_1m_2xh100.json
benchmarks/results/token_savings.json
DEMO_SCRIPT.md
docs/architecture.md
examples/codex_config.toml
glama.json
LICENSE
pyproject.toml
README.md
scripts/demo.py
src/perseus_vault_codex/__init__.py
src/perseus_vault_codex/__main__.py
src/perseus_vault_codex/_vault_client.py
src/perseus_vault_codex/config.py
src/perseus_vault_codex/install.py
src/perseus_vault_codex/server.py
src/perseus_vault_codex/tools.py
SUBMISSION.md
tests/conftest.py
tests/test_config.py
tests/test_install.py
tests/test_integration.py
tests/test_server.py
tests/test_tools.py
tests/test_vault_client.py
```

### Dependencies

- pyproject.toml: pytest@>=7

### Recent commits (newest first)

- Update license formatting in README.md
- Update maintainer's GitHub username
- Add glama.json configuration file
- benchmarks: add verified 1M-memory scale row (2xH100 #619) + sync doc numbers
- harden JSON-RPC validation and document verification
- Add measured benchmarks: recall latency/accuracy + cross-session token savings
- docs: add install-from-source path (runnable before PyPI release)
- ci: put repo root on sys.path so tests.conftest resolves under bare pytest
- Perseus Vault Codex — encrypted persistent memory for Codex agents

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
# AGENTS.md — guidance for Codex working in this repo

This file is read by Codex to understand the project. It also documents how the
memory tools this package provides should be used by an agent.

## What this project is

`perseus-vault-codex` is an MCP stdio server that gives a Codex agent persistent,
encrypted, local-first memory by wrapping the [Perseus Vault](https://github.com/Perseus-Computing-LLC/perseus-vault)
binary. It exposes five tools: `perseus_remember`, `perseus_recall`,
`perseus_forget`, `perseus_reflect`, `perseus_status`.

## Using the memory tools (for any Codex agent with this server installed)

- **At the start of a task**, call `perseus_recall` with a short description of
  what you're about to do. Pull in past decisions, conventions, and gotchas.
- **When you learn something durable** — a build command, a code-style rule, an
  architectural decision, a non-obvious gotcha, a user preference — call
  `perseus_remember`. Use a stable `key` so re-learning updates rather than
  duplicates.
- **When a memory is wrong or stale**, `perseus_forget` it by key.
- **To synthesize across many memories**, use `perseus_reflect`.
- Memories persist across sessions and are encrypted at rest. Do not store
  secrets you wouldn't want in a local encrypted DB.

## Developing this package

- Python ≥ 3.9, zero runtime dependencies (the stdio transport is vendored in
  `src/perseus_vault_codex/_vault_client.py`).
- Run tests: `pytest -q`. Unit tests use a fake vault client and need no binary.
  Integration tests run only when `PERSEUS_VAULT_BIN` points at a real
  `perseus-vault` binary (or it's on PATH).
- **stdout is the JSON-RPC channel** — never `print()` to stdout in the server
  path; log to stderr via the `_log` helpers.
- The curated tool schemas live in `tools.py::TOOL_SCHEMAS`; keep the surface at
  five tools — that minimalism is the point.

```

### DEMO_SCRIPT.md

```markdown
# Demo Video Script — Perseus Vault Codex

**Target length:** under 3:00 · **Format:** screen recording + voiceover ·
**Upload:** YouTube (unlisted or public), paste URL into `SUBMISSION.md`.

The audio **must** cover how Codex + GPT-5.6 were used — that's a judging
criterion. Lines flagged **[JUDGING]** below carry that; don't cut them.

Timings are targets. Record the terminal at a readable font size. A full dry-run
of the on-screen commands is in the shot list at the bottom.

---

### 0:00–0:20 — Hook (talking head or title card over terminal)

> "Every Codex session starts from zero. It re-learns your build commands,
> re-discovers your conventions, re-derives the context you explained an hour
> ago. Memory is the missing primitive for coding agents. This is Perseus Vault
> Codex — persistent, encrypted, local-first memory for Codex, in one install."

### 0:20–0:45 — The problem, concretely

> "There are memory stores out there, but none fit a developer's machine. mem0
> is cloud-dependent. cognee has no encryption at rest. Chroma's a vector DB,
> not agent memory. Your unreleased code and architecture shouldn't leave your
> machine in the clear. Perseus Vault is the only fully-local, encrypted answer —
> a single 12-megabyte binary, AES-256 encrypted, no cloud, no telemetry."

### 0:45–1:15 — Install & configure (screen: terminal)

Show, narrating as you go:

```bash
pip install perseus-vault-codex
perseus-vault-codex-setup
```

> "One pip install — zero Python dependencies. Then one setup command. It writes
> the MCP server into my Codex config, non-destructively, and backs up the old
> one."

Show the stanza it added in `~/.codex/config.toml`:

```toml
[mcp_servers.perseus-vault]
command = "perseus-vault-codex"
```

> "That's it. No config file to write, no database to provision. On first run it
> creates an encrypted vault in my home directory and generates the key itself."

### 1:15–2:05 — The payoff: memory across sessions (screen: `scripts/demo.py`)

Run the demo (it simulates two separate Codex sessions):

```bash
PERSEUS_VAULT_BIN=/path/to/perseus-vault python scripts/demo.py
```

Narrate over the output:

> "Session one: the agent learns three things about my project — that we format
> with ruff, that we picked SQLite and FTS5 over Postgres, and a Windows path
> gotcha. It remembers each one. Then I tear the whole vault process down —
> nothing's held in memory."
>
> "Session two — a brand-new process. I ask how we format code, why SQLite, the
> Windows bug. Every answer comes straight back out of the encrypted store. The
> context survived. Codex never forgets."

Point at the status line:

> "And it's encrypted at rest — there's a test in the repo that proves the
> memory plaintext never touches the database file on disk."

### 2:05–2:40 — How Codex + GPT-5.6 built it **[JUDGING]**

> "I built this with Codex during Build Week, and Codex did the real work. From a
> single prompt describing five tools, Codex scaffolded the whole MCP
[truncated — 1633 more characters]
```

### pyproject.toml

```
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "perseus-vault-codex"
version = "0.1.0"
description = "Persistent, encrypted, local-first memory for OpenAI Codex agents — a 5-tool MCP server wrapping Perseus Vault."
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [{ name = "Thomas Connally", email = "perseus@perseus.observer" }]
keywords = ["codex", "mcp", "memory", "openai", "gpt-5.6", "agent", "encrypted", "local-first", "perseus", "vault"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Topic :: Software Development :: Libraries",
    "Topic :: Scientific/Engineering :: Artificial Intelligence",
]
# Zero runtime dependencies: the stdio transport is vendored, and the vault is a
# single external binary. Only the perseus-vault binary needs to be on PATH.
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=7"]

[project.urls]
Homepage = "https://perseus.observer"
Repository = "https://github.com/Perseus-Computing-LLC/perseus-vault-codex"
"Perseus Vault" = "https://github.com/Perseus-Computing-LLC/perseus-vault"

[project.scripts]
perseus-vault-codex = "perseus_vault_codex.server:main"
perseus-vault-codex-setup = "perseus_vault_codex.install:main"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]
# Put the repo root on sys.path so `from tests.conftest import ...` resolves
# under the bare `pytest` console script (which, unlike `python -m pytest`,
# does not add the CWD to sys.path).
pythonpath = ["."]

```

### src/perseus_vault_codex/server.py

```python
"""The Perseus Vault Codex MCP server.

A dependency-free MCP stdio server that Codex connects to. It speaks JSON-RPC
2.0 over newline-delimited stdin/stdout (the MCP stdio transport) and forwards
the five curated memory tools to a ``perseus-vault serve`` subprocess.

    Codex agent  ── MCP stdio ──▶  this server  ── MCP stdio ──▶  perseus-vault
       (GPT-5.6)                   (5 curated tools)              (55+ tools, encrypted)

Design notes
------------
* **stdout is sacred.** Only JSON-RPC responses go to stdout; all logging goes to
  stderr. A stray print to stdout corrupts the protocol.
* **Lazy vault start.** The vault subprocess is spawned on the first tool call,
  not at import, so ``tools/list`` (which Codex calls during startup) is instant
  and never blocks on a missing binary.
* **Errors are JSON-RPC errors,** never crashes: a failing tool returns an error
  object so the Codex session stays alive.
"""

from __future__ import annotations

import json
import sys
from typing import Any, Dict, Optional

from . import __version__
from ._vault_client import VaultClient, VaultError
from .config import VaultConfig, load_config
from .tools import TOOL_SCHEMAS, Tools

PROTOCOL_VERSION = "2024-11-05"
SERVER_NAME = "perseus-vault-codex"

# JSON-RPC error codes (subset of the spec we use).
PARSE_ERROR = -32700
INVALID_REQUEST = -32600
METHOD_NOT_FOUND = -32601
INTERNAL_ERROR = -32603


def _log(msg: str) -> None:
    print(f"[{SERVER_NAME}] {msg}", file=sys.stderr, flush=True)


class CodexMemoryServer:
    """Owns the stdio loop, the vault subprocess, and the tool dispatch."""

    def __init__(self, config: Optional[VaultConfig] = None) -> None:
        self._cfg = config or load_config()
        self._client: Optional[VaultClient] = None
        self._tools: Optional[Tools] = None

    # -- lazy vault wiring --------------------------------------------------

    def _ensure_vault(self) -> Tools:
        if self._tools is None:
            self._client = VaultClient(
                binary=self._cfg.binary,
                db_path=self._cfg.db_path,
                encryption_key=self._cfg.encryption_key,
                llm_endpoint=self._cfg.llm_endpoint,
                llm_api_key=self._cfg.llm_api_key,
                llm_model=self._cfg.llm_model,
            )
            self._tools = Tools(self._client, self._cfg)
            _log(
                f"vault ready — db={self._cfg.db_path} "
                f"encrypted={self._cfg.encrypted} reflect={self._cfg.reflect_enabled}"
            )
        return self._tools

    def close(self) -> None:
        if self._client is not None:
            self._client.close()

    # -- request handling ---------------------------------------------------

    def handle(self, req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """Handle one JSON-RPC request. Returns a response dict, or ``None`` for
        notifications (which take no reply)."""
        method = req.get("method")
        rid = req.get("id")
        params = req.get("params", {})
        if params is None:
            params = {}

        # Reject malformed JSON-RPC before dispatching.  In particular, a
        # scalar ``params`` used to reach _call_tool and become an opaque
        # internal error instead of the protocol-level Invalid Request response.
        if (
            req.get("jsonrpc") != "2.0"
            or not isinstance(method, str)
            or not isinstance(params, dict)
        ):
            return _err(None, INVALID_REQUEST, "Invalid JSON-RPC request")

        # Notifications have no id and expect no response.
        is_notification = "id" not in req

        try:
            if method == "initialize":
                return _ok(rid, self._initialize())
            if method == "notifications/initialized":
                return None
            if method == "ping":
                return _ok(rid, {})
            if method == "tools/list":
                return _ok(rid, {"tools": TOOL_SCHEMAS})
            if method == "tools/call":
                return _ok(rid, self._call_tool(params))
            if is_notification:
                return None
            return _err(rid, METHOD_NOT_FOUND, f"Method not found: {method}")
        except VaultError as exc:
            _log(f"vault error on {method}: {exc}")
            return _err(rid, INTERNAL_ERROR, str(exc))
        except Exception as exc:  # never let one bad call kill the session
            _log(f"unexpected error on {method}: {exc!r}")
            return _err(rid, INTERNAL_ERROR, f"{type(exc).__name__}: {exc}")

    def _initialize(self) -> Dict[str, Any]:
        return {
            "protocolVersion": PROTOCOL_VERSION,
            "capabilities": {"tools": {"listChanged": False}},
            "serverInfo": {"name": SERVER_NAME, "version": __version__},
            "instructions": (
                "Persistent encrypted memory for this Codex agent. Recall project "
                "context at the start of a task; remember durable facts, decisions, "
                "and conventions as you learn them; reflect to synthesize insights."
            ),
        }

    def _call_tool(self, params: Dict[str, Any]) -> Dict[str, Any]:
        name = params.get("name")
        arguments = params.get("arguments") or {}
        tools = self._ensure_vault()
        handler = tools.handler_for(name)
        result = handler(arguments)
        text = json.dumps(result, indent=2, ensure_ascii=False)
        # Return both the human/agent-readable text block and structured content.
        return {
            "content": [{"type": "text", "text": text}],
            "structuredContent": result,
            "isError": False,
        }

    # -- main loop ----------------------------------------------------------

    def serve(self, stdin=None, stdout=None) -> None:
        stdin = stdin or sys.stdin
        stdout = stdout or sys.stdout
        _log(f"v{__version__} started — waiting for Codex on
[truncated — 1488 more characters]
```

### tests/test_vault_client.py

```python
"""Regression tests for vault subprocess transport recovery."""

from __future__ import annotations

import io

import pytest

from perseus_vault_codex._vault_client import VaultClient, VaultError


class _ClosingProcess:
    """A process whose stdout closes while the process still appears live."""

    def __init__(self) -> None:
        self.stdin = io.StringIO()
        self.stdout = io.StringIO("")
        self.terminated = False

    def poll(self):
        return None

    def terminate(self) -> None:
        self.terminated = True

    def wait(self, timeout=None) -> int:
        return 0

    def kill(self) -> None:
        self.terminated = True


def test_closed_stdout_tears_down_process_for_next_call():
    """EOF must clear the process so a later call can auto-respawn it."""
    client = VaultClient(binary="perseus-vault", db_path="unused.db")
    proc = _ClosingProcess()
    client._proc = proc

    with pytest.raises(VaultError, match="closed stdout unexpectedly"):
        client._request("tools/list", {})

    assert proc.terminated is True
    assert client._proc is None

```

### tests/test_install.py

```python
"""Tests for the Codex config installer (non-destructive merge)."""

from __future__ import annotations

from pathlib import Path

from perseus_vault_codex.install import STANZA_HEADER, install, render_stanza


def test_render_stanza_contains_command():
    stanza = render_stanza("perseus-vault-codex", None)
    assert STANZA_HEADER in stanza
    assert 'command = "perseus-vault-codex"' in stanza


def test_render_stanza_pins_binary_when_not_on_path():
    stanza = render_stanza("perseus-vault-codex", "/opt/perseus/perseus-vault")
    assert "PERSEUS_VAULT_BIN" in stanza
    assert "/opt/perseus/perseus-vault" in stanza


def test_install_creates_config_when_absent(tmp_path):
    cfg = tmp_path / ".codex" / "config.toml"
    summary = install(cfg)
    assert cfg.exists()
    assert STANZA_HEADER in cfg.read_text(encoding="utf-8")
    assert "Configured Codex" in summary


def test_install_preserves_existing_config_and_backs_up(tmp_path):
    cfg = tmp_path / "config.toml"
    cfg.write_text('[foo]\nbar = "baz"\n', encoding="utf-8")
    summary = install(cfg)
    text = cfg.read_text(encoding="utf-8")
    assert '[foo]' in text  # preserved
    assert STANZA_HEADER in text  # appended
    assert "backup" in summary
    # A timestamped backup was written next to it.
    backups = list(tmp_path.glob("config.toml.bak-*"))
    assert len(backups) == 1


def test_install_is_idempotent(tmp_path):
    cfg = tmp_path / "config.toml"
    install(cfg)
    summary = install(cfg)
    assert "Already configured" in summary
    # Only one stanza header present.
    assert cfg.read_text(encoding="utf-8").count(STANZA_HEADER) == 1


def test_dry_run_writes_nothing(tmp_path):
    cfg = tmp_path / "config.toml"
    summary = install(cfg, dry_run=True)
    assert not cfg.exists()
    assert "[dry-run]" in summary

```

### tests/test_config.py

```python
"""Tests for zero-config auto-init resolution logic."""

from __future__ import annotations

import perseus_vault_codex.config as config


def test_find_binary_prefers_explicit():
    assert config.find_binary("/custom/perseus-vault") == "/custom/perseus-vault"


def test_find_binary_reads_env(monkeypatch):
    monkeypatch.setenv("PERSEUS_VAULT_BIN", "/env/perseus-vault")
    assert config.find_binary() == "/env/perseus-vault"


def test_resolve_llm_prefers_explicit_endpoint(monkeypatch):
    monkeypatch.setenv("PERSEUS_VAULT_LLM_ENDPOINT", "http://localhost:11434/api/chat")
    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    endpoint, _key, _model = config._resolve_llm()
    assert endpoint == "http://localhost:11434/api/chat"


def test_resolve_llm_defaults_to_openai_when_key_present(monkeypatch):
    monkeypatch.delenv("PERSEUS_VAULT_LLM_ENDPOINT", raising=False)
    monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
    monkeypatch.delenv("OPENAI_MODEL", raising=False)
    monkeypatch.setenv("OPENAI_API_KEY", "sk-abc")
    endpoint, key, model = config._resolve_llm()
    assert endpoint == "https://api.openai.com/v1/chat/completions"
    assert key == "sk-abc"
    assert model == "gpt-5.6"


def test_resolve_llm_none_when_nothing_configured(monkeypatch):
    monkeypatch.delenv("PERSEUS_VAULT_LLM_ENDPOINT", raising=False)
    monkeypatch.delenv("PERSEUS_VAULT_LLM_API_KEY", raising=False)
    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    assert config._resolve_llm() == (None, None, None)


def test_load_config_disables_encryption_when_env_set(monkeypatch, tmp_path):
    monkeypatch.setenv("PERSEUS_VAULT_CODEX_ENCRYPT", "0")
    monkeypatch.setenv("PERSEUS_VAULT_CODEX_DB", str(tmp_path / "m.db"))
    monkeypatch.setenv("PERSEUS_VAULT_BIN", "perseus-vault")
    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    monkeypatch.delenv("PERSEUS_VAULT_LLM_ENDPOINT", raising=False)
    cfg = config.load_config()
    assert cfg.encrypted is False
    assert cfg.db_path.endswith("m.db")

```

### tests/test_integration.py

```python
"""End-to-end integration test against the real ``perseus-vault`` binary.

Skipped unless a binary is discoverable (``PERSEUS_VAULT_BIN`` or on PATH). This
is the test that proves the wrapper actually drives the vault — spawning the
subprocess, storing an encrypted memory, and recalling it across a fresh client,
exactly as a new Codex session would.
"""

from __future__ import annotations

import shutil

import pytest

from perseus_vault_codex._vault_client import VaultClient
from perseus_vault_codex.config import find_binary, load_config
from perseus_vault_codex.tools import Tools


def _binary_available() -> bool:
    b = find_binary()
    return shutil.which(b) is not None or b not in ("perseus-vault", "perseus-vault.exe")


pytestmark = pytest.mark.skipif(
    not _binary_available(),
    reason="perseus-vault binary not found (set PERSEUS_VAULT_BIN to run integration test)",
)


def test_remember_recall_persist_across_sessions(tmp_path):
    cfg = load_config(db_path=str(tmp_path / "it.db"), encrypt=True)

    # Session 1: remember two project facts, then close the vault entirely.
    with VaultClient(
        binary=cfg.binary, db_path=cfg.db_path, encryption_key=cfg.encryption_key
    ) as v1:
        t1 = Tools(v1, cfg)
        t1.remember({"text": "This project runs tests with 'pytest -q'", "key": "tests",
                     "category": "convention"})
        t1.remember({"text": "Auth uses short-lived JWTs, refresh in Redis", "key": "auth",
                     "category": "decision"})

    # Session 2: a brand-new client/process — recall must survive.
    with VaultClient(
        binary=cfg.binary, db_path=cfg.db_path, encryption_key=cfg.encryption_key
    ) as v2:
        t2 = Tools(v2, cfg)
        hits = t2.recall({"query": "how do we run tests"})
        texts = " ".join(m["text"] for m in hits["memories"])
        assert "pytest" in texts

        status = t2.status({})
        assert status["encrypted_at_rest"] is True
        assert status["total_memories"] >= 2

        # Forget one and confirm it's gone.
        t2.forget({"key": "auth", "category": "decision"})
        again = t2.recall({"query": "JWT auth", "category": "decision"})
        assert again["count"] == 0


def test_encrypted_db_is_not_plaintext(tmp_path):
    """The on-disk database must not contain memory text in the clear."""
    cfg = load_config(db_path=str(tmp_path / "enc.db"), encrypt=True)
    if not cfg.encrypted:
        pytest.skip("key generation unavailable")
    secret = "SUPERSECRETCONVENTIONTOKEN12345"
    with VaultClient(
        binary=cfg.binary, db_path=cfg.db_path, encryption_key=cfg.encryption_key
    ) as v:
        Tools(v, cfg).remember({"text": secret, "key": "s", "category": "secret"})
    raw = (tmp_path / "enc.db").read_bytes()
    assert secret.encode() not in raw

```

### tests/conftest.py

```python
"""Shared test fixtures: a fake vault client so unit tests need no binary."""

from __future__ import annotations

import json
from typing import Any, Dict, List

import pytest

from perseus_vault_codex.config import VaultConfig


class FakeVaultClient:
    """In-memory stand-in for VaultClient that mimics the vault tool contract.

    It records every call and stores memories in a dict keyed by
    (category, key), so tool translation can be asserted end-to-end without
    spawning the real binary.
    """

    def __init__(self, *, llm_answer: str | None = None):
        self.calls: List[Dict[str, Any]] = []
        self.store: Dict[tuple, Dict[str, Any]] = {}
        self.llm_answer = llm_answer

    def tool(self, short: str) -> str:
        return f"perseus_vault_{short}"

    def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
        self.calls.append({"name": name, "arguments": arguments})
        short = name.replace("perseus_vault_", "")
        if short == "remember":
            key = (arguments["category"], arguments["key"])
            action = "updated" if key in self.store else "created"
            self.store[key] = json.loads(arguments["body_json"])
            return {"action": action, "category": key[0], "key": key[1], "id": "mem-abc"}
        if short == "recall":
            q = (arguments.get("query") or "").lower()
            items = []
            for (cat, key), body in self.store.items():
                if arguments.get("category") and arguments["category"] != cat:
                    continue
                text = body.get("content", "")
                if not q or any(w in text.lower() for w in q.split()):
                    items.append(
                        {"key": key, "category": cat, "body_json": json.dumps(body), "score": 0.9}
                    )
            return {"items": items[: arguments.get("limit", 5)]}
        if short == "forget":
            key = (arguments["category"], arguments["key"])
            archived = 1 if key in self.store else 0
            self.store.pop(key, None)
            return {"archived": archived}
        if short == "ask":
            if self.llm_answer is None:
                raise RuntimeError("no llm configured")
            return {"answer": self.llm_answer}
        if short == "stats":
            by_cat: Dict[str, int] = {}
            for (cat, _key) in self.store:
                by_cat[cat] = by_cat.get(cat, 0) + 1
            return {"total_entities": len(self.store), "by_category": by_cat}
        raise RuntimeError(f"unexpected tool {name}")

    def close(self) -> None:
        pass


@pytest.fixture
def fake_vault():
    return FakeVaultClient()


@pytest.fixture
def fake_config(tmp_path):
    return VaultConfig(
        binary="perseus-vault",
        db_path=str(tmp_path / "memory.db"),
        encryption_key=str(tmp_path / "vault.key"),
        llm_endpoint=None,
        llm_api_key=None,
        llm_model=None,
    )


@pytest.fixture
def fake_config_with_llm(tmp_path):
    return VaultConfig(
        binary="perseus-vault",
        db_path=str(tmp_path / "memory.db"),
        encryption_key=str(tmp_path / "vault.key"),
        llm_endpoint="https://api.openai.com/v1/chat/completions",
        llm_api_key="sk-test",
        llm_model="gpt-5.6",
    )

```

### scripts/demo.py

```python
#!/usr/bin/env python3
"""Perseus Vault Codex — end-to-end demo.

Simulates two separate Codex sessions to prove memory persists across them:

  Session 1  — the agent learns three project facts and remembers them.
  (vault process is fully torn down — nothing is held in RAM)
  Session 2  — a brand-new agent recalls those facts, then reflects on them.

Run it:
    PERSEUS_VAULT_BIN=/path/to/perseus-vault python scripts/demo.py

Set OPENAI_API_KEY as well to see `reflect` synthesize an answer with GPT-5.6;
without it, reflect returns the assembled memory context instead.
"""

from __future__ import annotations

import os
import sys
import tempfile

# Windows consoles default to cp1252; force UTF-8 so example text renders.
try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass

from perseus_vault_codex._vault_client import VaultClient
from perseus_vault_codex.config import load_config
from perseus_vault_codex.tools import Tools

RULE = "-" * 64


def banner(title: str) -> None:
    print(f"\n{RULE}\n  {title}\n{RULE}")


def session(cfg):
    """Open a fresh vault client (a new 'Codex session') bound to the same DB."""
    client = VaultClient(
        binary=cfg.binary,
        db_path=cfg.db_path,
        encryption_key=cfg.encryption_key,
        llm_endpoint=cfg.llm_endpoint,
        llm_api_key=cfg.llm_api_key,
        llm_model=cfg.llm_model,
    )
    return client, Tools(client, cfg)


def main() -> None:
    db = os.path.join(tempfile.mkdtemp(prefix="perseus-codex-demo-"), "memory.db")
    cfg = load_config(db_path=db, encrypt=True)

    print(f"Vault binary : {cfg.binary}")
    print(f"Database     : {cfg.db_path}")
    print(f"Encrypted    : {cfg.encrypted}  (AES-256-GCM at rest)")
    print(f"Reflect LLM  : {cfg.llm_model if cfg.reflect_enabled else 'not configured'}")

    # ---- Session 1: the agent learns and remembers ----------------------
    banner("SESSION 1  |  Codex learns your project")
    client, t = session(cfg)
    facts = [
        ("convention", "style", "This project formats with `ruff format` and lints with `ruff check`. Never commit unformatted code."),
        ("decision", "db", "We chose SQLite + FTS5 over Postgres for the local dev store — zero infra, single file."),
        ("gotcha", "windows-paths", "On Windows, always use pathlib; os.path.join with mixed separators breaks the test fixtures."),
    ]
    for cat, key, text in facts:
        res = t.remember({"text": text, "category": cat, "key": key})
        print(f"  remember  [{res['action']:>7}]  {cat}/{key}")
    print(f"\n  Stored {len(facts)} memories. Closing the session (vault process ends).")
    client.close()

    # ---- Session 2: a NEW agent recalls -----------------------------------
    banner("SESSION 2  |  A brand-new Codex session recalls everything")
    client, t = session(cfg)

    for q in ["how do we format code", "why sqlite", "windows path bug"]:
        hits = t.recall({"query": q, "limit": 1})
        top = hits["memories"][0]["text"] if hits["memories"] else "(nothing)"
        print(f"  recall  {q!r}\n     -> {top}\n")

    banner("REFLECT  |  Synthesize an insight from memory")
    reflection = t.reflect(
        {"query": "Summarize what you know about formatting, sqlite, and windows here"}
    )
    print(f"  mode: {reflection['mode']}")
    print(f"  {reflection['answer']}")
    if reflection.get("context"):
        print("\n  Grounding memories:")
        print("  " + reflection["context"].replace("\n", "\n  "))

    banner("STATUS")
    st = t.status({})
    print(f"  total memories   : {st['total_memories']}")
    print(f"  by category      : {st['by_category']}")
    print(f"  encrypted at rest: {st['encrypted_at_rest']}")
    print(f"  engine           : {st['engine']}")
    client.close()

    print("\n✅  Memory survived a full session teardown. Codex never forgets.\n")


if __name__ == "__main__":
    main()

```

[14 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]