Project-scoped Markdown memory for the Pi coding agent — durable state, reports, and research as an auxiliary channel, independent from task-management systems.
Pi session
│
├─ session_start ──▶ snapshot memory context
├─ each turn ──────▶ inject: 5 newest state + 5 newest event records
│ (non-sensitive, maxTokens budget)
│ modes: message-append | system-prompt | off
│
├─ tools — the whole lifecycle in 4
│ memory_search ── scan content/metadata; no query = paged list + merge hints
│ memory_read ──── @id or path → full | summary | knowledge
│ memory_write ─── create | overwrite (+diff) | merge via supersedes
│ memory_delete ── remove one record + reconcile metadata
│
└─ storage — per Git workspace (slug = git-root basename), local-only
~/.pi/memory-md/projects/<slug>/
├── records/ state.*.md event.*.md ← source of truth
├── .catalog.json slim rebuildable index (derived)
└── .concepts.json concepts + alias map (derived)
state= still-current facts;event= append-only reports. Writes are identity-addressed:memory_write(path="events/foo.md", kind="event")storesrecords/event.foo.md(legacystate//events/paths remain readable).- Cross-session updates: when another session (or any external write) changes project memory, the next turn appends a compact
+ ~ −notice listing changed ids (message-append mode). Append-only, so the system prompt and cached prefix stay byte-identical. - No init tool, no git layer: the first
memory_writecreatesrecords/and the written record only; reads never create a project directory. Renaming a project folder means movingprojects/<slug>/by hand.
pi install git:github.com/sting8k/pi-workspace-memoryLocal development does not need node_modules — runtime frontmatter support is built in:
pi -e /absolute/path/to/pi-workspace-memory/index.tsSTART: memory_search({ query: "task keywords", searchIn: "all" })
READ: memory_read({ path: "@event.or-state-id", view: "knowledge" })
END: memory_write({ path: "events/<topic>.md", kind: "event", summary, claims or facts })
Default to event; use state only for knowledge still true tomorrow. Update state in place instead of dated snapshots. Write durable findings only.
memory_write generates compact Markdown from structured fields — summary, concepts, claims, facts (JSON scalars/arrays; nested objects flatten to dotted keys), relations (@id links to existing records in the same project; unknown or self targets are rejected), notes (loaded only in full view). One machine-checkable facts block per record:
<!-- memory:facts:v1 -->
runtime.supported_modes = ["git", "npm", "local"]
relation.follow_up -> @event.next-investigation
<!-- /memory:facts -->- Field limits — a write is rejected when its
summaryexceeds 300 chars or itsdescriptionexceeds 160; longer text belongs inclaims,facts,notes, orcontent. Only the input of that write is checked, so already-stored long values stay readable. States hold current truth (overwrite in place); events record milestones, decisions, or results, not per-action logs. - Overwrite diff — replacing an existing record returns a compact line diff; fresh creates keep the plain response.
- Sensitive —
sensitive: trueblocks auto-injection; secret-shaped values are flagged with a warning, not rejected. Detection follows gitleaks-style rules (provider token prefixes such asghp_/sk-ant-/AKIA, private key blocks, credentials in URLs, andkey = <high-entropy value>assignments); words like "password" or "ssh" alone do not count, so credentials written in prose need an explicitsensitive: true. The flag is recomputed on overwrite. - Supersedes (manual compaction) —
supersedes: ["@a", "@b"]validates every ID before touching disk, writes the distilled record, then marks each targetsupersededBy. Hiding is derived at read time: a record stays hidden only while its superseder exists, deleting the superseder resurrects it, and writing to a hidden record clears the marker. - Pre-write dedup (state creates only) — an ID-family match (
-v2,-final, date suffix, …) routes to an overwrite; concept containment (one set contains the other) or a similar ID (all distinctive ID tokens of one appear in the other, ignoring the project name and tokens most states share, e.g.arenavsarena-meta) rejects with a hint naming up to 3 similar records.forceCreate: trueor a non-emptysupersedesbypasses both. Events are never deduped. - Dated state IDs refused — dates belong to events; the error suggests
kind: "event"orforceCreate: true.
memory_search hides superseded records by default (includeSuperseded: true to list them); list mode (no query) shows records newest first, limit (default 50, max 200) per page with a ready-to-copy next-page call, and on page 1 appends up to 5 cluster warnings — state records sharing a concept with 4+ members, never events — each with a ready-to-copy merge call. memory_read reads hidden records and appends a supersede note. In full and knowledge views it also lists "Referenced by": live records whose relations point at this one, grouped by relation name (up to 20). The catalog rebuilds entirely from frontmatter; no migration needed.
~/.pi/agent/settings.json:
{
"pi-workspace-memory": {
"enabled": true,
"localPath": "~/.pi/memory-md",
"injection": "message-append"
}
}injection controls how memory reaches the agent:
message-append(default): on the first turn, a hidden message carries the project memory list; later turns append a compact+ ~ −notice only when another session changed memory. The system prompt never changes, so provider prompt caching keeps working. The list itself is a session-start snapshot;/memory-refreshre-sends it.system-prompt: the list is rebuilt from disk every turn and appended to the system prompt, so mid-session writes show up immediately, at the cost of prompt-cache misses whenever memory changes. No cross-session notices (the list is already current).off: nothing is injected; the tools and commands still work, and the agent finds memory only throughmemory_search/memory_read.
The legacy pi-memory-md key is still honored as a fallback.
npm test
npx tsc --noEmit
npx biome check <changed-files>MIT