Metadata-Version: 2.3
Name: sessionmemory
Version: 0.3.1
Summary: Durable memory for coding agents, one folder of searchable pages per project.
Author: Nathaniel Landau
Author-email: Nathaniel Landau <github@natelandau.com>
Requires-Dist: fastembed>=0.8.0
Requires-Dist: nclutils>=3.4.4
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: sqlite-vec>=0.1.9
Requires-Dist: tomli-w>=1.2.0
Requires-Dist: typer>=0.27.2
Requires-Python: >=3.13, <3.15
Description-Content-Type: text/markdown

# sessionmemory

Durable memory for coding agents, one folder of searchable pages per project.

An agent starts every session knowing nothing about the last one. Yesterday's session
worked around a trap, rejected the obvious approach for a reason, and found the one flag
that makes a library behave. All of it ends with the session, and the next one works it
out again from nothing. `sessionmemory` keeps what a project learned and hands it back
when the next session starts. Each project keeps its own memory, and no page is ever
shared between projects.

| Part                       | What it is                                                                     |
| -------------------------- | ------------------------------------------------------------------------------ |
| The vault                  | Your knowledge as markdown pages, one folder per project                       |
| The `sessionmemory` plugin | Claude Code hooks that feed a session at its start and record it at its end    |
| The `sessionmemory` CLI    | Searches pages by meaning and creates them. Everything else is a file you edit |

The pages follow the memoryfield format by Cal Paterson, described in
[his article](https://calpaterson.com/memoryfields.html) and defined in
[the memoryfield spec](https://github.com/calpaterson/memoryfield-spec). Each project's
`learnings/` folder is one field in that format: a flat directory of markdown pages beside one
vector index file. You can export that folder, share it, or read it with any tool that
speaks the format.

## Requirements

- [uv](https://docs.astral.sh/uv/). It installs the CLI, and the plugin runs its hooks
  through it.
- Python 3.13 or 3.14. `uv` downloads one when none is installed.
- git. The vault is a git repository, and a project is registered by its git remote.
- Claude Code, to run the plugin. The CLI works on its own without it.

The first search downloads the `nomic-embed-text-v1.5` embedding model, about 520MB, and
caches it under `~/.cache/sessionmemory/models`. Nothing else touches the network.

## Install

Installation is three steps: the CLI, the plugin, and a vault for them to share.

### 1. Install the CLI

The CLI is published on PyPI. Install it as a tool, which puts `sessionmemory` on your
`PATH`:

```bash
uv tool install sessionmemory
```

To upgrade it later, run `uv tool upgrade sessionmemory`.

### 2. Install the Claude Code plugin

In Claude Code, add this repository as a marketplace and install the plugin from it:

```
/plugin marketplace add natelandau/sessionmemory
/plugin install sessionmemory@sessionmemory
```

The hooks run the `sessionmemory` from step 1 when its version is at or past the
plugin's own. Keep the two in step: after `/plugin update sessionmemory@sessionmemory`,
run `uv tool upgrade sessionmemory` as well. A plugin newer than the tool falls back to
a copy of the CLI it carries, in a Python environment of its own that the first such
session builds.

### 3. Create a vault

The vault is its own directory. Make it a git repository of its own, so your pages and
this code do not share a history. Then initialize it:

```bash
mkdir -p ~/repos/my-vault
cd ~/repos/my-vault
git init
sessionmemory init ~/repos/my-vault
```

`sessionmemory init` writes the three files a vault needs. A marker in `_system/vault.toml`
identifies the directory as a vault. A `.gitignore` keeps the derived index out of your
history. A README explains the layout to whoever opens the vault later. `sessionmemory init`
never overwrites a file, so you can run it again safely.

Then tell the CLI and the hooks where the vault is. Both read the same two places, in the
same order: the `SESSIONMEMORY_VAULT` environment variable, then `vault.root` in
`~/.claude/sessionmemory.toml`. Record the root in the file, since a session launched
from a GUI or an IDE does not read your shell profile:

```toml
[vault]
root = "~/repos/my-vault"
```

The variable wins when both are set, so exporting it in one shell points that shell at a
different vault without touching the file. The
[plugin documentation](docs/plugin.md#settings) lists every other key that file accepts.

Until a directory holds the marker that `sessionmemory init` writes, every command refuses
to touch it. The refusal protects you. If the root points at your home directory by
mistake, the first page written scatters a `projects/` tree into it.

Nothing commits the vault on a timer. The plugin commits it when a session starts and
again when a session ends, so a page reaches git within the session that wrote it.
Pushing that history to a remote stays yours to do.

> **Note:** To bring an existing directory of notes under the CLI, run
> `sessionmemory init --force ~/repos/my-vault` once. `--force` means only that the
> directory already has contents. Nothing existing is overwritten.

## Register a project

A project gets memory when its repository is registered. With the plugin installed, the
first session you open in a git repository registers it. The session begins with one
line that names the slug:

```
This repository was registered with the vault as project 'invoice-api'.
```

The slug comes from the git remote, or from the directory name when the repository has
no remote. There is nothing else to choose: no tags, no scope, no note type.

Only a git repository is registered for you. A slug is permanent once pages carry it. A
session opened in your home directory or a scratch folder must not leave a project named
after it in the vault. To register a directory outside git, or to choose the slug
yourself, run the command once:

```bash
cd ~/repos/invoice-api
sessionmemory project --register --cwd .
```

```
✓ registered 'invoice-api'
  └─ root: ~/repos/invoice-api
```

From then on, a session that starts in a registered repository receives that project's
memory. A session that ends or compacts hands its transcript to a background pass, which
records what was worth keeping.

## Search and write pages

The CLI does two things. It finds pages by meaning, and it creates pages. Reading and
editing a page is a job for your editor or your agent's own tools.

```bash
sessionmemory search "why does the same stripe event arrive twice" --limit 2 --cwd .
```

```
~/repos/my-vault/projects/invoice-api/learnings/stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
  Stripe retries a webhook for 72 hours, so the handler must be idempotent
  Stripe redelivers an unacknowledged webhook for up to 72 hours, so the handler records the event id and ignores a repeat.

~/repos/my-vault/projects/invoice-api/learnings/the-nightly-reconciliation-job-must-start-after-the-02-00-bank-feed.md
  The nightly reconciliation job must start after the 02:00 bank feed
  The bank feed lands at 02:00 UTC; a reconciliation run before it reports every open invoice as unpaid.
```

A result is a path, a title, and a summary. A paraphrase finds the page, because search
ranks by meaning and not by words in common. A query that nothing answers returns no
results rather than the nearest pages. Pass `--read` to print every hit in full.

```bash
sessionmemory new learning \
  --title "Stripe retries a webhook for 72 hours, so the handler must be idempotent" \
  --summary "Stripe redelivers an unacknowledged webhook for up to 72 hours, so the handler records the event id and ignores a repeat." \
  --cwd .
```

```
✓ created stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
  └─ ~/repos/my-vault/projects/invoice-api/learnings/stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
```

The vault path is shortened to `~` here; the command prints absolute paths.

The command writes the frontmatter and prints the path. Write the body into that file,
or pass it with `--body-file`. The title is what every future session sees at its start,
and the summary is what a search result shows. Both state the fact and not the topic.

## What a session sees

`sessionmemory inject` prints the block a session starts with. This is the block for a
project holding four learnings, one spec, one plan, and two open backlog items:

```
## Using this vault

Durable memory for this project lives in a vault of markdown pages. Nothing below is
loaded for you: the titles are what the vault holds, and each is one `sessionmemory search`
away. The project's folder has `learnings/` and `logs/`, searched by meaning, beside
`specs/`, `plans/`, and `backlog.md`, which are ordinary files you Read and Edit.
`sessionmemory project --json` prints every path.

  - Before assuming nothing was written down, search: `sessionmemory search "<words>"`
    prints each hit's path, title, and summary, and `--read` prints every hit's whole
    page in one call. A paraphrase still matches. No hits means nothing is recorded,
    not that the query needs loosening.
  - Past sessions, one page each: `sessionmemory search "<words>" --logs`.
  - Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
    (feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
    `- [ ] [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add, tick, or
    delete lines directly. If the file is missing, create it with a `# Backlog` heading.
  - Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file
    and prints its path. Edit it directly after that.
  - Learnings are captured at session end, not by you mid-session. When the user asks
    to keep one now: `sessionmemory new learning --title "..." --summary "..." --cwd .`
    creates the page and prints the path to write prose into. Title and summary state
    the fact, not the topic. Keep a page under 8KB; more detail is another page.

## What this project knows

  - Invoice numbers come from a Postgres sequence, never from max(id) plus one
  - pytest-asyncio needs asyncio_mode = auto or every async test is skipped
  - Stripe retries a webhook for 72 hours, so the handler must be idempotent
  - The nightly reconciliation job must start after the 02:00 bank feed

## Open work

  2 open backlog items
  spec: Export invoices as UBL 2.1 XML
  plan: Move PDF rendering to a worker queue
```

A page body never enters that block, so its cost grows with the number of pages and not
with their length. The titles say what exists. `sessionmemory search` returns what they say.

## Documentation

| Page                                     | What it covers                                               |
| ---------------------------------------- | ------------------------------------------------------------ |
| [Concepts](docs/concepts.md)             | Pages, fields, the index, and the layout of a vault          |
| [CLI reference](docs/cli.md)             | Every command, its options, and its output                   |
| [The Claude Code plugin](docs/plugin.md) | The hooks, the sweep, every setting, and the slash commands  |
| [Vault health](docs/vault-health.md)     | What `sessionmemory doctor` reports, and what to do about it |

## Development

```bash
git clone https://github.com/natelandau/sessionmemory
cd sessionmemory
uv sync                  # install dependencies
uv run duty lint         # ruff, ty, typos, yamllint, shellcheck, prek
uv run duty test         # pytest with coverage
```

`CLAUDE.md` records the conventions this project holds itself to.

## License

MIT. See [LICENSE](LICENSE).
