Metadata-Version: 2.5
Name: memware
Version: 0.8.0
Summary: Memory for AI agents that only remembers the latest truth: a bi-temporal belief ledger plus a transcript index, in one SQLite file.
Project-URL: Homepage, https://github.com/ericwalisko/memware
Project-URL: Issues, https://github.com/ericwalisko/memware/issues
Project-URL: Changelog, https://github.com/ericwalisko/memware/blob/main/CHANGELOG.md
Author: ericwalisko
License-Expression: MIT
License-File: LICENSE
Keywords: agent-memory,bitemporal,claude-code,fts5,hermes,llm,mcp,sqlite
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: pyyaml>=6; extra == 'dev'
Requires-Dist: ruff==0.16.5; extra == 'dev'
Requires-Dist: shtab>=1.7; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp>=1.2; extra == 'mcp'
Provides-Extra: shell
Requires-Dist: shtab>=1.7; extra == 'shell'
Description-Content-Type: text/markdown

# memware

**Memory for AI agents that only remembers the latest truth.**

memware is one SQLite file with two stores:

- **turns** — immutable evidence. Every prompt and answer from past sessions,
  split into ~400-token passages and indexed with FTS5. Recall ranks passages and
  quotes only the matching ones; reading a session back returns whole turns.
  BM25 × recency × use, no model in the loop.
- **beliefs** — a bi-temporal ledger of facts. A new value for the same
  `(subject, relation)` **supersedes** the old one. Recall only ever returns the
  currently valid belief; history is kept for audit and never reaches a prompt.

No daemon, no vector database, and no model call at capture or read time unless you switch on
the optional [relevance filter](#optional-a-relevance-filter-for-prompt-time-injection). A 30-day
corpus of a busy coding agent — 18k turns, 40k passages — indexes in about
fourteen seconds into ~120 MB.

```text
$ memware sync ~/.claude/projects --harness claude-code
{"added": 14348, "files": 1475}

$ memware assert "api" "listens on port" "8443" --source "session 3f2a, turn 41"
{"outcome": "superseded", "belief_id": 2, "incumbent_id": 1}

$ memware recall "which port does the api use" --what beliefs
api listens on port 8443            # 8080 is in the ledger, retired, and never surfaces
```

## Why

Agent memory systems that rewrite what they remember degrade: continuous
LLM consolidation can push utility *below* having no memory at all
([Useful Memories Become Faulty When Continuously Updated by LLMs](https://arxiv.org/abs/2605.12978)).
And embeddings cannot tell a *contradicted* fact from a *rephrased* one — AUROC 0.59 —
so vector stores serve stale facts 15–40% of the time on evolving knowledge
([Temporal Validity in Retrieval Memory](https://arxiv.org/abs/2606.26511)).

memware borrows four mechanisms from human memory research and keeps them deliberately small:

| mechanism | in the brain | in memware |
|---|---|---|
| evidence ≠ belief | hippocampus vs neocortex (complementary learning systems) | `turn` table is append-only; `belief` table is separate |
| update on surprise | reconsolidation driven by prediction error | `memware assert` at the moment an agent notices a conflict |
| only the latest understanding | reconsolidated traces overwrite in place | deterministic supersession keyed on `(subject, relation)`, ordered by **event time** |
| need-probability recall | Anderson & Schooler 1991 / ACT-R activation | `bm25 × (1+age)^-d × (1 + w·ln(1+uses))` |

Full rationale and citations: [docs/design.md](docs/design.md).

## Install

The Claude Code plugin's hooks call `memware` as a bare command, so the CLI must be on
the `PATH` your shell uses — **install it as a tool, not into a project virtualenv**:

```bash
uv tool install "memware[mcp]"     # recommended
# or
pipx install "memware[mcp]"
```

Then confirm the shim resolves (if this prints nothing, the hooks will silently do nothing):

```bash
memware --version
which memware
```

<details>
<summary>Plain <code>pip install</code></summary>

`pip install "memware[mcp]"` works for library and CLI use, but a plain pip install into a
project or conda environment usually leaves `memware` off the PATH that Claude Code's hooks
run under — use `uv tool` or `pipx` (above) for the plugin, or install into an environment
that is always active. `memware` (core) omits the MCP server; drop `[mcp]` only if you do
not want the MCP tools.

</details>

## Use it from Claude Code

```bash
claude plugin marketplace add ericwalisko/memware
claude plugin install memware@memware
claude mcp add -s user memware -- memware-mcp   # optional tools; -s user = every project, not just this dir
```

**Backfill your existing sessions** (optional, once). The plugin only captures new
sessions; index the transcripts already on disk so recall works over past work from day one:

```bash
memware backfill                 # indexes ~/.claude/projects (idempotent; ~5 s for a month)
```

Prefer a guided first run? `memware setup` walks through the backfill and backups together, asks
whether `derive` may run automatically, and prints the operating guidance. Run it on a fresh install
or after an upgrade. `memware setup --yes` accepts the defaults non-interactively and never
switches derive on.

The *belief ledger* starts empty and is not backfilled — beliefs are derived, not stored in
transcripts. It fills as you work (via the `remember` tool) and through `memware derive`,
which mines the indexed transcripts for durable facts — see [Deriving beliefs](#deriving-beliefs).
Transcript recall is what backfill gives you immediately, and it is where most of the value is.

Requires the `memware` CLI on your `PATH` (see [Install](#install)). Hooks:
a `SessionStart` hook catches up any session whose `SessionEnd` was skipped (some environments force-kill Claude Code — a worktree manager may `SIGKILL` it — and a kill cannot run `SessionEnd`); a second one injects `memware digest`, a short block with this project's recent sessions and beliefs and a line pointing at `recall`; `SessionEnd`/`PreCompact` sync the transcript into the index; an optional `UserPromptSubmit`
hook injects the handful of beliefs whose subject the prompt names, each with the date it was
recorded (beliefs only — transcript search is on demand through the MCP tools). Neither block
injects a derived measurement, moving version or status, or a version the project's manifest
overrules; `memware beliefs --stale` lists what they leave out. Set `MEMWARE_DB` to move the
store. Start a session with `MEMWARE_NO_CAPTURE=1` to keep it out of the index and the backup
mirror: the hooks list its transcript, and every sync and backup skips what is listed. A session
that runs no memware hook cannot be recognised that way. See
[docs/integrations.md](docs/integrations.md) and [docs/keeping-memory-clean.md](docs/keeping-memory-clean.md).

## Deriving beliefs

```bash
memware derive --plan     # no network: every excerpt a run would send, and where
memware derive            # dry run: sends the excerpts to the model, prints the facts, files nothing
memware derive --apply    # files them; runs again later from where it stopped
```

`derive` reads every turn from an interactive session indexed since its last run, has a model
turn the sentences that look like facts into `(subject, relation, value)` triples, and files only
the triples that pass a deterministic check: every word of the value must appear in the
excerpt, so a model cannot introduce a fact the evidence does not contain. Derived beliefs
carry reliability 0.5, below anything you stated yourself, so a contradiction lands in
`memware review` rather than on top of your belief.

The default provider is the Claude Code CLI on your own subscription (`claude -p`, Haiku),
so there is nothing to configure. `--provider openai` sends the extraction to any
OpenAI-compatible endpoint instead (`OPENAI_BASE_URL` / `OPENAI_MODEL` / `OPENAI_API_KEY`).

**Interactive sessions only, by default.** Claude Code records what started each session
(`entrypoint`: `cli` interactive, `sdk-cli` for `claude -p`), and derive skips the turns of
`claude -p` and Agent SDK runs. On a machine that runs agent lanes those are most of the
transcripts, and the eval scaffolding among them reads like fact. They stay indexed and
recallable; they just never become beliefs. If your headless runs hold decisions you want in
the ledger, opt them back in with `memware config derive.sources all`. A turn with no recorded
entrypoint (a transcript from before Claude Code wrote the field, or another harness) is read as
interactive, so nothing the filter cannot label is dropped. `memware derive --plan` prints the
setting and how many new turns each setting would read, and `memware stats` counts sessions,
turns and beliefs by entrypoint and names the project directories holding the most sessions.

A dry run is not offline: it sends the excerpts to that provider and skips only the write.
`--plan` is the view that sends nothing. It lists every excerpt a run would send with its
source pointer, then the session, excerpt, character and model-call counts and the
destination, and it works before `claude` or `OPENAI_*` is set up. Read it before you turn
derive on for transcripts that must not leave the machine.

You do not need an always-on machine. The plugin can run it for you on session start, at
most once a day. `memware setup` is where you switch that on: it shows where the excerpts go
before it asks. `memware config derive.auto true` sets the same switch directly. Other
options — a macOS LaunchAgent that catches up after sleep, a systemd timer with
`Persistent=true`, plain cron — are in [docs/scheduling.md](docs/scheduling.md).

## Use it from Hermes Agent

`integrations/hermes/memware/` is a memory-provider plugin built on Hermes's
`MemoryProvider` ABC — prompt-time belief prefetch, non-blocking turn capture, and
`memware_recall` / `memware_remember` tools — sharing one store with Claude Code.

## The supersession rule

```text
same key, same value   → reinforce (reliability rises, use is counted)
same key, newer value  → supersede: incumbent gets valid_to = new.valid_from
same key, older value  → filed as history; the timeline stays consistent
weaker challenger      → parked as a candidate and sent to review
```

Ordering is decided by `valid_from` (when the evidence says it became true), never by
insertion order — so a backfill converges to the same state in any order, twice, or in
batches. Three policies: `auto` (last writer by event time), `gate_conflicts`
(default: a less reliable challenger goes to review), `await_confirmation`.

## Recall is keyword search; the agent supplies the meaning

The index is FTS5/BM25 — fast, model-free, and literal. The `recall` tool therefore takes
**several phrasings** and fuses them by reciprocal rank, so a tool-calling agent puts its
own reasoning into retrieval at call time (synonyms, related concepts, the literal value it
expects), the same way it would issue a few grep or web-search queries:

```text
recall(queries=["which port does the api listen on", "api port", "8443", "gateway listen port"])
```

Byte-identical hits collapse to a single slot, so a prompt captured on many days — a scheduled job's own preamble, say — never crowds out distinct evidence; the turns stay in the store and a session still reads back whole.

Prompt-time injection (the hooks) stays deterministic and only injects beliefs whose
*subject* the prompt names. The optional filter below can drop some of those; it never adds one.

## Optional: a relevance filter for prompt-time injection

**Off by default.** Nothing in this section happens until you switch it on, and with it off memware
makes no network call and injects exactly what it did before the filter existed.

The prompt hook picks beliefs by keyword, and a shared word is not relevance. A prompt about an
incident report also gets a weekly report's file path, and a prompt that says "draft a short note"
gets a short story's title. memware can ask the System One model ("Jev") from
[TypeSafe](https://typesafe.ai) whether each candidate bears on the prompt, and inject only the ones
that clear a threshold. This is the only model call memware can make at read time, so you have to
opt in to it.

**What leaves your machine when it is on:** for each prompt the Claude Code hook sees, and each
turn the Hermes provider prefetches for, memware sends the prompt text (cut to 2,000 characters)
and up to 20 candidate beliefs, each as `subject relation: value`. They go over HTTPS to
`api.typesafe.ai` with your API key. It sends no session id, path, transcript or date. What
TypeSafe does with the data is set by [its terms](https://docs.typesafe.ai/legal).

Two kinds of turn are never sent:

- **A turn nobody typed:** a background task's notification, or a hook that fires inside a
  subagent.
- **A session memware keeps out of its store:** `MEMWARE_NO_CAPTURE=1`, the no-capture list, a
  `capture.exclude` glob, or an ignore marker in the prompt.

So `memware exclude --add '*/<project-dir>/*' --apply` keeps every prompt from that project on the
machine. It also keeps that project out of memware's index, since that is what the glob is for. If
some of your work must not leave the machine, exclude it that way before you turn the filter on,
or leave the filter off.

```bash
echo 'TYPESAFE_API_KEY=<your key>' >> ~/.memware/.env   # or export it where the hooks run
memware config relevance.mode shadow     # make the call and log it; injection unchanged
memware config relevance.mode filter     # once the log says the threshold is right
memware config relevance.mode off        # no calls at all (the default)
```

| mode | request per prompt | what is injected | log |
|---|---|---|---|
| `off` | none | memware's own top k | none |
| `shadow` | one | unchanged | one line per candidate |
| `filter` | one | the candidates at or above the threshold, most probable first, at most k | one line per candidate |

Any other value reads as `off`, so a typo never switches it on. The other settings, each set with
`memware config relevance.<name> VALUE`:

- `threshold` (0.5)
- `pool` (20 candidates, taken from memware's own ranking, so a relevant fact ranked seventh can
  replace a lexical hit)
- `timeout_s` (1.5, at most 5)
- `model` (`jev-1.13.0`, pinned rather than `jev-latest` because a threshold is tuned against one
  version)

The Hermes provider reads the same switch.

**It fails open.** memware makes one request with no retry, under a hard deadline. If it has no
key, the request times out, the server returns an HTTP error or redirect, or the reply is not one
probability per candidate, the hook injects exactly what it would with the filter off. Measured
end to end on a synthetic ledger with a full pool of 20 candidates, over 100 prompts:

| hook | p50 | p95 |
|---|---|---|
| off | 84 ms | 99 ms |
| filter | 553 ms | 753 ms |

One of the 100 calls hit the 1.5 s deadline and fell back to the unfiltered output.

**Cost:** a full pool is about 3,200 input tokens per prompt. At jev-1.13's list price of $0.042
per million input tokens (output is free), that is about $0.00014 per prompt.
`~/.memware/relevance-usage.jsonl` records each answered request: its tokens, cost and latency,
and no text.

**Calibrating:** the default threshold of 0.5 has not been calibrated against your ledger. Shadow
mode writes `~/.memware/relevance-log.jsonl`, with one line per prompt and candidate. Each line
carries:

- `pair_id`: the prompt's hash and the belief id, so the same pair is labelled once
- memware's `rank` for the candidate, and `today`: whether memware injected it
- `p`: the probability the model returned
- `chosen`: whether filter mode would have injected it
- `prompt` and `fact`: the texts that were sent

Label a few dozen pairs as relevant or not, and pick the threshold that keeps what you need.
**The log holds the text of your prompts,** so delete it when you are done. `memware nuke` removes
both files; `memware scan` and `prune` do not read them.

The model answers with probabilities and never with text. The injected block therefore holds only
beliefs from your own ledger that passed memware's subject and staleness gates. A prompt or a
stored fact written to steer the model can do no more than move a candidate that was already in
the pool.

## Backups and the wipe trap

Transcripts are deleted by the OS after ~30 days, so an aged session lives only in the store —
**back it up, and never wipe-and-re-backfill** (backfill only re-indexes transcripts still on
disk). memware guards this: migrations snapshot first, and `backfill` warns if a backup is
larger than the store. Once a destination is set, backups happen **automatically at session boundaries** — the
`SessionStart` hook takes a throttled snapshot (at most once every ~20h), and a clean
`SessionEnd` does too. No cron; immune to a laptop sleeping through a scheduled time, and — because
the start hook always runs — also to a session being force-killed (a worktree/pane manager that
`SIGKILL`s Claude Code never runs `SessionEnd`).

```bash
memware setup                              # guided: index sessions, pick a folder, take a first backup
memware backup                             # tiered snapshot (1/3/7/14-day) + transcript mirror
memware restore --latest                   # after a wipe, restore — do not re-backfill
memware nuke                               # delete everything, typed-confirmation guarded
```

Full guide: [docs/backup.md](docs/backup.md).

## Keeping evaluations out of the evidence

Full guide: [docs/keeping-memory-clean.md](docs/keeping-memory-clean.md).

Headless runs write transcripts too, unless you pass `--no-session-persistence` to `claude -p`.
Set `MEMWARE_NO_CAPTURE=1` in any run you do not want indexed: the plugin's hooks list its
transcript so no sync indexes it and no backup mirrors it, and the Hermes provider captures
nothing. That needs a memware hook to run in the session, so also put `[memware-eval]` in
evaluation prompts, and use `memware-eval --corpus ROOT --db scratch.db
--beliefs-from ~/.memware/memware.db` to judge retrieval against a store that excludes them.
`memware prune --containing TEXT` shows the runs that already slipped in and the beliefs derived from them, and `--apply` un-indexes the runs and retracts those beliefs; copies already
mirrored to a backup folder have to be deleted there by hand. For a pasted secret, run `memware prune --turns-containing --apply` from a plain terminal: it asks for the value without echoing it and removes it from the store file too, and `memware scan --backups` counts every place it is left, the transcripts memware does not index included ([removal runbook](docs/keeping-memory-clean.md#removing-a-value-a-token-a-password)). For a durable filter that every sync and backup honours — including runs that predate a marker — list content signatures in `~/.memware/ignore-markers.txt` (or `MEMWARE_IGNORE_MARKERS`); any transcript whose head contains one is never indexed or mirrored.

A generator that runs from its own working directory can be excluded by path, whatever its environment: `memware exclude --add '*/<project-dir>/*'` previews a `capture.exclude` glob and `--apply` writes it ([docs/keeping-memory-clean.md](docs/keeping-memory-clean.md#layer-3--path-exclusions-the-machine-remembers)).

## Reviewing contested supersessions

memware does not ship a UI. It ships a contract — `ReviewBackend` with `publish()` and
`collect()` — plus two implementations: JSONL outbox/inbox files and a plain HTTP
endpoint. Wire it to whatever you already use to make decisions.

```bash
memware review sync                       # outbox ~/.memware/review-outbox.jsonl
echo '{"review_id": 7, "decision": "approve"}' >> ~/.memware/review-inbox.jsonl
memware review sync                       # applied
```

## Evaluation

`memware-eval` scores retrieval against a question set: does the right evidence
surface, and does the stale value stay hidden? It needs no model, so results are
reproducible. The protocol for end-to-end comparisons — agent alone vs agent + memware —
is in [docs/eval.md](docs/eval.md).

## Editor and shell integration

memware ships no editor plugins — `--plain` (tab-separated, id-first) and `--json` are the
integration surface, and everything is a copy-paste recipe on top of them. Shell completions
come from `memware completions zsh|bash|fish` (needs the `[shell]` extra:
`uv tool install "memware[mcp,shell]"`). `--plain` pipes cleanly to `fzf`/`awk`/`cut`:

```bash
memware recall "which port does the api use" --plain | fzf --delimiter='\t' --with-nth=10
```

Emacs, Vim, Neovim, an `$EDITOR` bulk-edit round-trip, and completion install steps are in
[docs/editor-integration.md](docs/editor-integration.md).

## Accessibility

memware emits no colour at all (so `NO_COLOR` is honoured by construction), and no information
is ever carried by colour. Default output is screen-reader-friendly — labeled, one field per
line, blank line between records; `--plain` and `--json` are the stable machine formats; and
`--ascii` (auto-on in a non-UTF-8 locale) avoids glyphs a screen reader or terminal might
mangle. Full statement: [docs/accessibility.md](docs/accessibility.md).

## Status

Alpha. The schema may change before 1.0; the ledger semantics will not.

## License

MIT. See [LICENSE](LICENSE).
