Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- Added repository-scoped memory under `.pi/agent/memory/` alongside user-wide
memory under `~/.pi/agent/memory/`. Repository paths resolve from the nearest
Git root, or from the session working directory outside Git.
- Memory tools now accept user/repository scopes. Natural-language clues such as
“remember this for this repo” select repository memory when the tool omits an
explicit scope; neutral requests continue to default to user memory.
- Context injection loads and labels both scopes, with repository memory taking
priority. `memory_search` searches both scopes by default through separate qmd
collections, and `memory_status` reports both inventories and collections.

### Changed

- New recovery records include their memory scope so repository deletions restore
to the correct location. Existing recovery records remain readable.
- `PI_MEMORY_DIR` now explicitly overrides only the user-wide memory directory;
repository memory remains at `.pi/agent/memory/`.

## [0.4.2] — 2026-08-10

### Added
Expand Down
85 changes: 53 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,31 @@

Thanks to https://github.com/skyfallsin/pi-mem for inspiration.

Your coding agent forgets everything between sessions. pi-memory gives it a memory: durable facts and decisions, a running daily log, and a scratchpad of things to come back to — all as plain markdown files you can read, edit, and commit. With optional [qmd](https://github.com/tobi/qmd) it also gets keyword, semantic, and hybrid **search** across everything it has ever remembered.
Your coding agent forgets everything between sessions. pi-memory gives it a memory: durable facts and decisions, a running daily log, and a scratchpad of things to come back to. User-wide memory lives under `~/.pi/agent/memory/`; repository-specific memory lives under `.pi/agent/memory/` in the repository root. Both use plain Markdown you can inspect and edit, and repository memory can be committed with the project. With optional [qmd](https://github.com/tobi/qmd), pi can search both scopes with keyword, semantic, and hybrid **search**.

## What it feels like

```text
# Session 1
you ▸ I always use pnpm in this repo, never npm. Remember that.
pi ▸ Got it — saved to long-term memory. (writes MEMORY.md)
pi ▸ Got it — saved to repository memory. (writes .pi/agent/memory/MEMORY.md)

# …days later, brand new session…
you ▸ add prettier as a dev dependency
pi ▸ pnpm add -D prettier
(recalled your package-manager preference from memory — no reminder needed)
```

Everything lives in `~/.pi/agent/memory/` as markdown, so you can also just `cat` it:
You can inspect either scope directly:

```bash
$ cat ~/.pi/agent/memory/MEMORY.md
# Repository-specific memory
$ cat .pi/agent/memory/MEMORY.md
<!-- 2026-06-07 10:12:03 [a1b2c3d4] -->
#preference [[package-manager]] Always use pnpm in this repo, never npm.

# User-wide memory
$ cat ~/.pi/agent/memory/MEMORY.md
```

## Installation
Expand All @@ -54,15 +58,15 @@ npm install -g @tobilu/qmd # no Bun required
bun install -g https://github.com/tobi/qmd # ensure ~/.bun/bin is on PATH
```

When qmd is present, the extension **automatically creates** the `pi-memory`
collection and path contexts on the next session start — no manual step. Run
`memory_status` any time to confirm qmd, the collection, and embeddings are ready.
When qmd is present, the extension **automatically creates** the user-wide
`pi-memory` collection and a uniquely named collection for the current repository
when repository memory exists. Run `memory_status` any time to confirm qmd, both
collections, and embeddings are ready.

Semantic/deep modes need vector embeddings; the extension keeps them current
automatically (`qmd embed` runs in the background at session start and after
writes). The very first embed downloads the embedding model, so semantic search
may take a minute to come online on a fresh install. To set the collection up
by hand:
may take a minute to come online on a fresh install. To set up the user-wide collection by hand:

```bash
qmd collection add ~/.pi/agent/memory --name pi-memory
Expand All @@ -77,13 +81,13 @@ Without qmd, the core tools still work fully — only `memory_search` and select

| Tool | Description |
|------|-------------|
| `memory_write` | Write to MEMORY.md (long-term) or daily log |
| `memory_forget` | Delete matching entries and create a durable recovery record |
| `memory_write` | Write to user or repository MEMORY.md (long-term) or a daily log |
| `memory_forget` | Delete matching entries in either scope and create a durable recovery record |
| `memory_restore` | Restore a deletion using the recovery ID returned by `memory_forget` |
| `memory_read` | Read any memory file or list daily logs |
| `scratchpad` | Add/done/undo/clear/list checklist items |
| `memory_search` | Search across all memory files (requires qmd) |
| `memory_status` | Health check: where files live, qmd/collection/embeddings state, active config |
| `memory_read` | Read a user or repository memory file, or list daily logs |
| `scratchpad` | Add/done/undo/clear/list user or repository checklist items |
| `memory_search` | Search user memory, repository memory, or both (requires qmd) |
| `memory_status` | Health check for both scopes, qmd collections, embeddings, and active config |

### memory_search modes

Expand All @@ -95,32 +99,49 @@ Without qmd, the core tools still work fully — only `memory_search` and select

If the first search doesn't find what you need, try rephrasing or switching modes.

## Memory scopes

Most memory tools accept `scope: "user" | "repo"`:

- **`user`** stores cross-repository preferences and facts in `~/.pi/agent/memory/`.
- **`repo`** stores project-specific decisions and context in `.pi/agent/memory/` at the nearest Git repository root. Outside Git, the session working directory is used.

When `scope` is omitted, the extension uses clues in the active request. Phrases such as “remember this for this repo,” “current project,” or “repository-specific” select repository memory. “Globally,” “across all repositories,” and similar phrases select user memory. Neutral requests default to user memory for backward compatibility. An explicit tool argument always wins.

`memory_search` defaults to `scope: "all"`, while `memory_restore` looks for the recovery ID in both scopes.

## File layout

```
~/.pi/agent/memory/
MEMORY.md # Curated long-term memory
SCRATCHPAD.md # Checklist of things to fix/remember
Both scopes use the same layout:

```text
<scope-root>/
MEMORY.md # Curated long-term memory
SCRATCHPAD.md # Checklist of things to fix/remember
daily/
2026-02-15.md # Daily append-only log
2026-02-15.md # Daily append-only log
2026-02-14.md
...
recovery/
<recovery-id>.json # Complete payload and restore state for a memory_forget deletion
<recovery-id>.json # Complete payload, scope, and restore state
```

The user scope root is `~/.pi/agent/memory/`. The repository scope root is
`.pi/agent/memory/` under the nearest Git root.

## How it works

### Context injection

Before every agent turn, the following are injected into the system prompt (in priority order):
Before every agent turn, memory is injected into the system prompt in this order:

1. **Open scratchpad items** (up to 2K chars)
2. **Today's daily log** (up to 3K chars, tail)
3. **MEMORY.md** (up to 4K chars, middle-truncated)
4. **Yesterday's daily log** (up to 3K chars, tail — lowest priority, trimmed first)
1. Repository then user **open scratchpad items** (up to 2K chars per scope)
2. Repository then user **today's daily log** (up to 3K chars per scope, tail)
3. Relevant qmd results in per-turn mode
4. Repository then user **MEMORY.md** (up to 4K chars per scope, middle-truncated)
5. Repository then user **yesterday's daily log** (up to 3K chars per scope, tail)

Total injection is capped at 16K chars.
Repository sections are labeled separately from user-wide sections. Total injection remains capped at 16K chars.

### KV cache-stable snapshot (default)

Expand Down Expand Up @@ -176,7 +197,7 @@ This ensures in-progress context survives compaction and is visible in the next
- **Persistence**: Memory files are plain markdown on disk — readable, editable, and git-friendly.
- **Recoverable deletion**: `memory_forget` stores complete deleted entries under `recovery/` before changing memory and returns a recovery ID that `memory_restore` can use. Recovery JSON is outside qmd's `**/*.md` index.
- **Tool response previews**: Write/scratchpad tools return size-capped previews instead of full file contents.
- **qmd auto-setup**: On first session start with qmd available, the extension creates the collection and path contexts automatically.
- **qmd auto-setup**: On session start, the extension creates the user collection and, when repository memory exists, the current repository's uniquely named collection and path contexts.
- **qmd re-indexing**: After every write, a debounced `qmd update` runs in the background (fire-and-forget, non-blocking) unless disabled via `PI_MEMORY_QMD_UPDATE`.
- **qmd embeddings**: Vector embeddings for semantic/deep search are kept current automatically — `qmd embed` (incremental) runs in the background after each re-index and as a catch-up at session start. Disabled along with re-indexing via `PI_MEMORY_QMD_UPDATE`.
- **Graceful degradation**: If qmd is not installed, core tools work fine. `memory_search` returns install instructions.
Expand All @@ -185,7 +206,7 @@ This ensures in-progress context survives compaction and is visible in the next

| Variable | Values | Default | Description |
|----------|--------|---------|-------------|
| `PI_MEMORY_DIR` | path | `~/.pi/agent/memory` | Override the memory storage directory |
| `PI_MEMORY_DIR` | path | `~/.pi/agent/memory` | Override the user-wide memory directory; repository memory remains at `.pi/agent/memory/` |
| `PI_MEMORY_SNAPSHOT` | `stable`, `per-turn` | `stable` | `stable` snapshots memory at checkpoints for KV cache stability; `per-turn` rebuilds every turn (legacy behavior) |
| `PI_MEMORY_QMD_UPDATE` | `background`, `manual`, `off` | `background` | Controls automatic `qmd update` + `qmd embed` after writes |
| `PI_MEMORY_QMD_SEARCH_TIMEOUT_MS` | positive integer (milliseconds) | `60000` | Sets the timeout for explicit `memory_search` qmd queries |
Expand All @@ -204,7 +225,7 @@ Run the `memory_status` tool first — it reports most of these at a glance.
| `memory_search` says qmd is required | qmd not installed or not on `PATH` | Install qmd (`npm install -g @tobilu/qmd`); if installed via Bun, ensure `~/.bun/bin` is on `PATH` |
| Search returns nothing for terms you know exist | Index is stale | A background `qmd update` runs after writes; if disabled (`PI_MEMORY_QMD_UPDATE=off`), run `qmd update` manually |
| “need embeddings” on semantic/deep search | Vectors not built yet | Embedding starts automatically in the background — retry shortly. If `PI_MEMORY_QMD_UPDATE` is `manual`/`off`, run `qmd embed` yourself |
| Collection `pi-memory` missing | Auto-setup didn't run (qmd installed mid-session) | Run any `memory_search` (auto-creates it) or `qmd collection add ~/.pi/agent/memory --name pi-memory` |
| A user or repository collection is missing | Auto-setup did not run, or qmd was installed mid-session | Run `memory_search` (auto-creates the required collection) and confirm with `memory_status` |
| qmd works in the shell but not from pi on Windows | Broken `.cmd`/`.ps1` shims | The extension bypasses them by invoking qmd's JS entry with `node`; make sure the npm global `node_modules` dir is on `PATH` |
| Memory isn't being injected after a write | Cache-stable snapshot only refreshes at checkpoints | Long-term writes refresh next turn; for daily/scratchpad use `memory_read`, or set `PI_MEMORY_SNAPSHOT=per-turn` |

Expand Down Expand Up @@ -245,8 +266,8 @@ This is a single-file extension (`index.ts`). No build step required — pi load
# Test with pi directly
pi -p -e ./index.ts "remember: I prefer dark mode"

# Verify memory was written
cat ~/.pi/agent/memory/MEMORY.md
# Verify repository-specific memory was written
cat .pi/agent/memory/MEMORY.md
```

## Publishing (maintainers)
Expand Down
79 changes: 39 additions & 40 deletions design.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ Three principles guided the design:

**1. Files are the index.**
No separate metadata store, no extraction pipeline, no sync to keep in
agreement. Memory lives in `~/.pi/agent/memory/` as markdown files. qmd
(a full-text + vector search tool) indexes them directly. `git diff` shows
what changed. `cat` shows what's stored.
agreement. User-wide memory lives in `~/.pi/agent/memory/`; repository memory
lives in `.pi/agent/memory/` at the repository root. qmd (a full-text + vector
search tool) indexes both scopes directly. `git diff` shows repository-memory
changes. `cat` shows what's stored in either scope.

**2. Injection should be selective, not exhaustive.**
The previous design injected ALL of MEMORY.md every turn, truncating from
Expand Down Expand Up @@ -79,11 +80,10 @@ falls back to the previous behavior. No feature is critical-path.
| - format top 3 results
| |
| 2. buildMemoryContext(searchResults)
| - read scratchpad |
| - read today's daily |
| - read repo + user scratchpads
| - read repo + user daily logs
| - include search results
| - read MEMORY.md |
| - read yesterday's daily
| - read repo + user MEMORY.md
| - truncate to 16K |
| |
| 3. Append to system prompt
Expand All @@ -109,24 +109,21 @@ falls back to the previous behavior. No feature is critical-path.

### Injection Priority

Context budget is 16K chars. Sections are built in priority order; when the total
exceeds the budget, content is trimmed from the end (yesterday goes first):

```
Priority Section Budget Truncation
-------- ------- ------ ----------
1 (high) Open scratchpad items 2.0K from start
2 Today's daily log 3.0K from end (tail)
3 qmd search results 2.5K from start
4 MEMORY.md (long-term) 4.0K from middle
5 (low) Yesterday's daily log 3.0K from end (tail)
------
14.5K (individual caps)
16.0K (total cap)
Context budget is 16K chars. Each file type keeps its existing per-section cap,
but repository sections precede user-wide sections:

```text
Priority Section Budget Truncation
-------- ------- ------ ----------
1 (high) Repository, then user scratchpad 2.0K each from start
2 Repository, then user today's log 3.0K each from end (tail)
3 qmd search results 2.5K from start
4 Repository, then user MEMORY.md 4.0K each from middle
5 (low) Repository, then user yesterday 3.0K each from end (tail)
```

The gap between individual caps (14.5K) and total cap (16K) provides headroom
for section headers and separator lines.
The 16K total cap remains authoritative when the per-section caps add up to more
than the available context.

### Why This Order

Expand All @@ -147,7 +144,8 @@ the oldest context and most likely to be stale.
|
+-- sanitize: strip control chars, limit to 200 chars
+-- check: qmd available? collection exists?
+-- qmd search "what database should we use?" -n 3 -c pi-memory
+-- qmd search "what database should we use?" -n 3
| -c pi-memory -c pi-memory-repo-<hash>
+-- timeout: 3 seconds (Promise.race)
+-- format: markdown snippets with file paths
|
Expand Down Expand Up @@ -220,21 +218,21 @@ commands. Now:
| no --> show install instructions, stop
| yes --> continue
|
+-- checkCollection("pi-memory") — does collection exist?
| yes --> done
| no --> setupQmdCollection()
| |
| +-- qmd collection add ~/.pi/agent/memory --name pi-memory
| +-- qmd context add /daily "Daily work logs" -c pi-memory
| +-- qmd context add / "Long-term memory" -c pi-memory
| |
| +-- any step fails? log and continue (not critical)
+-- ensure user collection `pi-memory`
| +-- qmd collection add ~/.pi/agent/memory --name pi-memory
|
+-- repository memory exists?
| no --> skip repository setup
| yes --> ensure `pi-memory-repo-<path-hash>` for .pi/agent/memory
|
+-- add /daily and / path contexts to each collection (best effort)
|
done
```

The same auto-setup runs inside the `memory_search` tool if the collection is
missing at search time, covering the case where qmd was installed mid-session.
The same auto-setup runs inside `memory_search`. Searches use repeated `-c`
arguments to search user and repository collections together, while explicit
scope arguments can restrict the search.

## What We Chose Not to Build

Expand All @@ -249,9 +247,10 @@ relationship modeling, and query translation — all failure-prone. Wiki-links l
`[[database-choice]]` achieve cross-referencing through content, searchable
without any graph infrastructure.

**No multiple collections.** One qmd collection with path contexts (`/daily` vs
`/`) is sufficient. Splitting into per-topic collections would require routing
logic to decide which collection to search.
**No per-topic collections.** qmd uses one user-wide collection and one
collection for each repository that has memory. This split enforces storage
scope without adding topic-routing logic; `/daily` and `/` path contexts still
distinguish daily logs from long-term memory within each collection.

**No semantic search for injection.** Keyword search (BM25) runs in ~30ms.
Semantic search (vector) takes ~2s. For injection that runs every turn, latency
Expand Down Expand Up @@ -419,5 +418,5 @@ complex architectures — at least for retrieval tasks. Our testing verifies the
mechanics work. The open question is whether selective injection meaningfully
improves recall in practice, and the eval infrastructure exists to answer it.

Total implementation: ~1,100 lines of TypeScript in a single file. Zero
dependencies beyond the pi runtime and optional qmd.
The implementation remains a single TypeScript file with no runtime dependencies
beyond pi and optional qmd.
Loading