@abianbiya/lean-memory0.3.0
Project-isolated memory for Pi: explicit global facts, per-worktree notes and tasks, scoped keyword search, and stable context snapshots. Uses Jay Zeng's pi-memory helpers.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
Lean Memory for Pi
Project memory by default. Global memory by explicit choice.
@abianbiya/lean-memory is a standalone Pi extension. A brand-new project sees
only your small global memory, not another project's tasks, logs or decisions.
Built on ideas and line-preserving scratchpad / entry-aware deletion helpers from pi-memory by Jay Zeng (MIT). Version 0.2 owns its scoped storage, search and snapshot lifecycle; it does not run upstream's shared memory engine. Full credit and upstream license are in THIRD_PARTY_NOTICES.md.
Scope rules
| Data | Default scope | Automatic injection |
|---|---|---|
Facts (MEMORY.md) |
Current project | Small global + current-project facts |
| Scratchpad | Current project | Only open tasks in global/current project |
| Daily logs | Current project | Never; retrieve when relevant |
| Search | Current project + global | Only when called |
| Forget / restore | Current project | Recovery stays in its originating scope |
Use scope: "global" deliberately for universal preferences/safeguards, such as
"never use my personal browser profile." Repository commands, environment details,
and unfinished work belong to project memory. No cross-project search API is
exposed. These are tool-level boundaries, not an OS sandbox: ordinary filesystem
tools still have the user's permissions.
Project identity
The canonical Git working-tree root, resolved through symlinks, is hashed with SHA-256. Nested folders share a project's memory; repositories with the same name do not. Separate worktrees have separate facts, tasks and logs. Nothing is keyed by branch name, remote URL or folder basename. Renaming/moving a root creates a new identity; existing memory stays archived until explicitly migrated.
For non-Git projects, set an absolute LEAN_MEMORY_PROJECT_ROOT in the harness
launch environment. The current directory must be inside that root. No project
config is read or executed. If no project is identified (including Git failure or
a bare repository), injection/search fall back to global-only; default project
read/write/task/recovery calls fail clearly rather than silently writing globally.
Install
pi install npm:@abianbiya/lean-memory
Or try it without installing:
pi -e npm:@abianbiya/lean-memory
For development against a local checkout (run npm install in this package
directory first):
pi install /absolute/path/to/skills/lean-memory-pi
No qmd, embeddings, external service or API key is required for memory.
Pi supplies the SDK peer dependencies. Upstream pi-memory@0.4.2 is pinned and
bundled for its helper functions; its original copyright/license is included.
Runtime requirements: Pi 0.85.1 or newer, which provides the
@earendil-works/pi-coding-agent and @earendil-works/pi-ai peer dependencies.
main points at the TypeScript extension entry that Pi loads; this package is not
intended to be imported as a Node module. The only installed runtime dependency is
pi-memory@0.4.2 for its helper functions, and it is bundled in the tarball.
Disable any separately loaded pi-memory extension and remove/disable the old
extensions/lean-memory/ adapter copy to prevent duplicate tool registrations:
{
"packages": [
{ "source": "npm:pi-memory", "extensions": [] },
"npm:@abianbiya/lean-memory"
]
}
Merge these with existing settings, then /reload. Installing only Lean Memory
on a clean profile does not require a separate pi-memory package entry.
Storage and configuration
Private memory stays outside repositories by default:
~/.pi/agent/memory/
global/
MEMORY.md
SCRATCHPAD.md
REVIEW.json # optional last-reviewed timestamp; never injected or searched
daily/YYYY-MM-DD.md
recovery/<uuid>.json
projects/<sha256-of-canonical-working-tree-root>/
PROJECT.json # optional display name; never injected or searched
REVIEW.json # optional last-reviewed timestamp; never injected or searched
MEMORY.md
SCRATCHPAD.md
daily/YYYY-MM-DD.md
topics/*.md # optional manually maintained notes
recovery/<uuid>.json
| Environment variable | Meaning |
|---|---|
LEAN_MEMORY_DIR |
Absolute storage root; default ~/.pi/agent/memory |
LEAN_MEMORY_PROJECT_ROOT |
Optional explicit absolute project root, useful without Git |
Use memory_status to inspect the active project root/hash and exact directories.
Legacy PI_MEMORY_* variables are not used; Lean Memory does not mutate them or
share upstream's process-global storage state. Configure environment before
launch/reload. Storage is shared across profiles using the same root, separated
by canonical working tree. On the first project memory write, PROJECT.json stores
the sanitized repository-root basename for the browser label; it is never injected or
searched. Existing scopes are not backfilled and receive a name only after a later
project write. Memory is local/private and never packaged or committed.
Team-shared instructions belong in project AGENTS.md, not private memory.
Tools
Seven familiar tool names remain, but scopes and behavior are explicit:
memory_write: project by default;scope: globalopts into global. Facts may append/overwrite; daily logs append only. Overwrite creates a full-file backup.memory_read: project by default; facts, scratchpad, daily log or scoped file list. Character offsets and bounded limits allow paging (maximum 12,000 chars).scratchpad: add/done/undo/clear_done/list in one scope. Clear-done backs up the original. Unknown prose and sub-notes survive checklist mutations.memory_search: defaultscope: bothmeans current project + global only;projectorglobalcan narrow it. Local keyword matching requires all query terms. Returns up to 10 scoped filenames/snippets, default 3.memory_forget: removes matching entries and saves recovery before mutation.memory_restore: restores missing entries, idempotently preserving later writes. A recovery ID from project A cannot be restored in B or global memory.memory_status: identities, directories, search boundaries and snapshot policy.
Examples:
memory_write(target="long_term", content="Tests run inside the app container.")
scratchpad(action="add", text="Verify concurrency before declaring completion.")
memory_write(scope="global", target="long_term", content="Use isolated browsers.")
memory_search(query="container tests", limit=3)
memory_read(target="daily", date="2026-09-15", offset=0, limit=4000)
Browsing: /memory
/memory opens a read-only picker for the active scope — open tasks, facts and file
lists for this project and for global memory, each row showing its count or size.
Selecting a row shows its contents, then returns to the list; dismissing the picker
closes it. It writes nothing and reads disk directly, so it shows what another session
or an external editor has already saved. This picker is limited to the active project
and global memory; use the explicitly opened HTML viewer below for all-project browsing.
Without an interactive UI (print/JSON mode) the same inventory is emitted as one text
notification instead of dialogs.
/memory html — browser view
Opens a simple local browser UI to browse global memory and every stored project
(the active project shows its basename; unnamed saved scopes use readable numbered
labels), including Markdown files such as daily logs and topics. Markdown headings,
lists, emphasis, quotes, links and code are rendered safely; raw HTML remains text.
Its Prompt context view separates the latest exact
Lean-Memory-added prompt segment from a fresh project simulation, with character
counts and a rough characters / 4 token estimate. The simulation includes only
facts and open tasks; daily/topic files are not auto-injected. The base Pi prompt is
never shown. /memory close stops the viewer.
- Loopback only. The socket binds
127.0.0.1on an OS-assigned port — never a wildcard address, so other machines cannot reach it. - Token-gated. A per-run random token is required (
/?token=…, compared in constant time). Requests without it get403. The port is shown in the status line; the token is not, and is not logged. - Read-only, constrained endpoints.
/serves a static shell;/data,/file, and/contextreturn inventory, one allow-listed memory file, or a context preview. Request values are checked against discovered project IDs and eligible file lists; no arbitrary path is accepted and no write endpoint exists. Cross-project inspection is limited to this explicitly opened, token-gated viewer; memory tools and search keep their existing scope rules. - Injection-proof. The shell carries no memory content; the Markdown view builds
DOM nodes and uses
textContent, so raw HTML such as<script>or<img onerror>…displays as literal text and cannot execute. A CSP ofdefault-src 'none'blocks external loads. - Live. The page polls every 2s, so writes from tools appear without a reload. It keeps the project it was opened from marked active even if the working directory later changes; actual injection shows the most recently captured prompt addition. Project simulations read the current disk snapshot and are labeled as simulations.
- Ephemeral. The server is unref'd: it never holds the pi process open, dies with
the session, and does not survive a restart. Re-running
/memory htmlreplaces it.
Auto-launching the browser only happens in TUI mode. On a remote or headless host the
URL is announced instead, so nothing opens on the wrong machine. In print/JSON mode the
browser view is unavailable and /memory prints a listing.
The HTML viewer includes eligible topic Markdown contents. Unrelated legacy archives are not part of the viewer or search, even if the old qmd collection still indexes them.
/memory review — periodic review
On the first interactive use of non-empty global or current-project facts/open tasks,
Lean Memory reminds you if that scope has never been reviewed or its last review was
at least 30 days ago. It checks only global plus the active project; it never scans
other projects. /memory review asks the assistant for evidence-based keep/update/
consider-removing suggestions. That user-initiated review turn has no tools and cannot
change memory. After reviewing the recommendations, run /memory review done to save
the review date for the scopes included. Daily logs/topics are outside this review.
There are no background model calls, automatic edits, or automatic deletions.
Stable context and limits
Snapshots refresh on session/reload/cwd change, successful compaction, and facts
or scratchpad mutations through tools. The ambient prompt is cached per project
identity and stamped with an invalidation generation, so interleaved turns for two
projects cannot share one snapshot, and a snapshot that started before a write
cannot repopulate the cache once that write lands. Daily writes do not invalidate ambient
context. External edits and other sessions' writes become ambient after /reload
or compaction; explicit read/search always reads disk. No automatic per-prompt
search, exit-summary LLM call or copying of mixed recent logs takes place.
Keep global facts under 2,000 characters. Injection ceilings per scope: facts 2,400 characters, open tasks 800, plus headings/omission labels. Whole lines are kept; omission is visible. Keep critical safeguards near the beginning and curate files instead of relying on truncation. A fresh project needs no filler context.
Files are limited to 1 MiB. Search scans at most 1,000 directory entries per scope, three nested directory levels and 8 MiB of text across selected scopes. It skips recovery/hidden files and symlinks, bounds snippets to 1,000 characters, and labels scan truncation. Oversized/unreadable files fail visibly, not as "no memories."
Writes use per-scope inter-process locks and atomic file replacement; recovery
is written before deletion. A lock waits up to five seconds, then fails without
stealing it. After a crashed writer, verify no writer remains before manually
removing that scope's .lock directory. Do not edit the same files concurrently
with tools that ignore this lock. Symlinks inside scoped storage are refused for
reads/writes and skipped by search. This does not protect against a hostile OS
user concurrently replacing directory components.
Upgrading from pi-memory / Lean Memory 0.1
This release intentionally does not automatically migrate mixed old memories.
Old root-level MEMORY.md, SCRATCHPAD.md, daily/, topics/ and recovery records
remain unchanged but are not injected or searched. An empty new scope is normal.
Back up the old memory directory.
Curate truly universal preferences into
global/MEMORY.md; omit project lists.Resolve each project's directory offline. The identity is the SHA-256 of the canonical, symlink-resolved working-tree root with no trailing newline, so the hashed string must match
fs.realpathbyte for byte:root=$(cd /path/to/repo && pwd -P) # canonical root, newline stripped printf '%s' "$root" | shasum -a 256 | cut -d' ' -f1 # GNU/Linux: sha256sum | cut -d' ' -f1That digest is the directory name under
~/.pi/agent/memory/projects/. Confirm it againstmemory_statusrun from inside that repository before copying anything: a mismatch means you would populate a directory the extension never reads.Copy only clearly attributable notes into
projects/<digest>/topics/<name>.md(or facts/tasks into that project'sMEMORY.md/SCRATCHPAD.md). Keep unfinished tasks open. Migration is a copy, not a move: leave the originals in place as the archive.Verify from that project:
memory_read target="list"shows the file and amemory_searchfor its contents returns it. Then verify from an unrelated project that the same search returns nothing.Leave cross-repository or ambiguous notes archived until a single owning root is confirmed. A note that spans repositories (several projects, one tooling topic) has no correct destination yet; do not bulk-route by filename. Migrate one project per verification, not one file per guess. Ambiguous daily logs stay archived as they are: read them with normal filesystem tools when you need that history, and copy across only entries you can attribute to a confirmed root.
/reload, then re-check that another project cannot read or search those notes.
Old recovery IDs require the old implementation/archive; they are not silently reinterpreted. Full-file overwrite/clear-done backups are for manual inspection and merging, not automatic restore that could overwrite subsequent work.
ponytail: this release uses bounded local keyword search, not semantic/deep search. The upgrade path is a scope-filtered index with isolation tests; reusing the old shared qmd collection would defeat the boundary. Worktree-shared facts, automatic archival and automatic identity migration are also intentionally absent.
Development / release checks
npm run typecheck
npm test
npm pack --dry-run --ignore-scripts
Tests create disposable Git repositories and memory directories; they never commit, touch real user memory, run migrations, query models, or invoke qmd. They cover two-project isolation, global-only behavior, canonical identity, scope-bound recovery, symlink/path validation, budgets and concurrent processes.
The owner's checkout reuses installed SDK/dev tools via ignored node_modules
links and an unchanged copy of the pinned upstream package. No machine paths are
included in the published source/manifest. To publish after review, run npm publish --access public; prepublishOnly runs typecheck and tests. This is documentation,
not an indication that the package has been published.
Rollback and license
Remove this package entry and re-enable upstream pi-memory, then /reload. The
new scoped files remain on disk. Upstream may resume injecting its old mixed
memory and searching its old collection, so rollback also changes isolation.
This extension is MIT licensed. Jay Zeng's upstream helpers retain their original MIT copyright; see THIRD_PARTY_NOTICES.md.