- JavaScript 58.3%
- TypeScript 41.7%
| .pi | ||
| agent | ||
| docs | ||
| scripts | ||
| stateful-memory | ||
| tests | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
Pi Stateful Memory Extension
This package adds long-term statefulness to Pi via an extension. It maintains a persistent persona (stateful-memory/SOUL.md), per-session memory logs, pinned facts (FACTS.md), orientation context (WAKE.md), and topic-based knowledge addenda (stateful-memory/persona_topics/).
Documentation
- Project State (current behavior, config defaults, validation): docs/PROJECT_STATE.md
- Plan (four-pillar roadmap): docs/PLAN.md
The layout here mirrors the global install layout: agent/ maps to ~/.pi/agent, and stateful-memory/ maps to ~/.pi/stateful-memory. In the monorepo, everything lives under /persist/pi/.
What's Included
agent/extensions/stateful-memory/: extension implementation (memory store, retrieval, topic routing).stateful-memory/: persona files, topic addenda, and the topic routing index.stateful-memory/SOUL.md,STYLE.md,REGISTER.md,SLEEP.md: persona source files (tracked).stateful-memory/persona_topics/,stateful-memory/PERSONALITY_MATRIX.md: topic addenda + routing table (tracked).FACTS.md,WAKE.md,dreams/: not here — these are live runtime state in~/.pi/stateful-memory/, written by the sleep cycle, not tracked in the repo.
agent/extensions/delegate/: fractal delegation — spawns focused sub-sessions for long-running tasks and returns summaries as tool results. Two files:index.ts— registers thedelegatetool, enforces depth limitfork-runner.ts— full fork lifecycle: session creation, depth registry, summary extraction, manualsession_shutdownfor memory durability See DELEGATE.md for full details.
agent/extensions/: other Pi extensions (SSH, force-tools, web-search, etc.).scripts/: smoke tests for memory and topic routing.tests/: unit tests.
Architecture Diagram (High-Level)
User Prompt
│
▼
Trunk Session (depth 0)
│
├─ before_agent_start
│ ├─ System Prompt = Base + Memory Context + Topic Addenda
│ │ Memory Context order:
│ │ Persona (SOUL + STYLE + REGISTER + SLEEP)
│ │ Current Context (WAKE.md)
│ │ Pinned Facts (FACTS.md)
│ │ Recent Themes (last 5 memory entries)
│ │ Recollections (4 summaries + 4 explicit entries)
│ └─ Topic Addenda: hybrid-scored, persisted across turns
│
├─ Tools
│ ├─ remember / remember_session / recall
│ ├─ list_topics / load_topic
│ │
│ └─ delegate (task, context?)
│ │ Spawns a fork session — same identity + memory, clean context.
│ │ Depth tracked in globalThis.__delegateForkDepths (survives
│ │ jiti module reloads). Hard limit: 3 levels from trunk.
│ │
│ ▼
│ Fork Session (depth 1)
│ ├─ Full persona + memory system (same agentDir)
│ ├─ Seeded with task + explicit context only
│ ├─ Can call delegate again (depth 2, then 3, then blocked)
│ ├─ session_shutdown fired manually → summary written to
│ │ memory/sessions/*.md (fork work is recallable)
│ └─ Returns TASK_COMPLETE: summary to calling session
│
├─ Session lifecycle hooks
│ ├─ session_shutdown / session_before_switch / session_before_fork
│ └─ Memory summary → memory/sessions/*.md
│ (fires for trunk + forks — all work is recallable)
│
└─ /sleep command
├─ Phase 0: pre-sleep summary (main thread, synchronous)
├─ Phase 1: WAKE.md fork — reads all session summaries; writes WAKE.md
│ (uses its own fork runner in memory-sleep.js, not the delegate tool)
├─ Phase 2: FACTS.md fork — reads summaries + FACTS.md; rewrites FACTS.md
├─ Phase 3: Dreams fork — reads recent summaries + topics; writes dream log
└─ ctx.newSession() → fresh session with updated WAKE.md + FACTS.md in context
Running With Pi
The extension is loaded globally via ~/.pi/agent/extensions/stateful-memory, which is a bind mount from /persist/pi/agent/extensions/stateful-memory (configured in the NixOS monika module).
To load explicitly from the monorepo during tests:
cd /persist/pi
pi -e agent/extensions/stateful-memory/extension.js
Configuration
The live config is at /persist/pi/agent/stateful-memory.json. It uses absolute paths so it works regardless of which directory Pi is launched from. See docs/PROJECT_STATE.md for all defaults and environment overrides.
Global Install Layout (NixOS)
On the monika host, bind mounts wire the monorepo into the live Pi install:
/persist/pi/agent/extensions/stateful-memory→~/.pi/agent/extensions/stateful-memory/persist/pi/stateful-memory/→~/.pi/stateful-memory/FACTS.mdandWAKE.mdare not symlinked — they are real files in~/.pi/stateful-memory/written by the sleep cycle. Do not commit them to the repo.
Tools and Commands
Tools:
remember: store facts to session memory (target: "memory") or directly to FACTS.md (target: "profile").remember_session: force a session summary now.recall: two-phase memory retrieval — LLM planner selects files, second LLM call synthesises a response.list_topics: list available topic addenda and their trigger keywords.load_topic: load a full topic addendum by id.delegate: spawn a focused sub-session for a task. Takestask(required) andcontext(optional — excerpt relevant facts from the current conversation). The fork runs with full identity and memory access but a clean context window. Returns the fork'sTASK_COMPLETE:summary. Max depth 3 from trunk. Use for work that would dominate the current context: reading many files, extended research, multi-step implementation. Short actions are better done directly.
Slash commands:
/sleep: run the sleep cycle — pre-sleep summary, then three fork phases (WAKE.md, FACTS.md, dreams), then open a fresh session. Seedocs/PROJECT_STATE.mdfor full details.
Delegate Extension
The delegate tool and the sleep cycle's fork runner share the same core pattern but are separate implementations:
delegate tool |
Sleep fork runner | |
|---|---|---|
| File | delegate/fork-runner.ts |
stateful-memory/memory-sleep.js |
| Called by | Agent (tool call) | /sleep command handler |
| Task prompt | Structured focused-task message | Phase-specific (WAKE / FACTS / dreams) |
| Depth tracking | globalThis.__delegateForkDepths |
Not tracked (sleep forks don't nest) |
| Timeout | 10 min | 15 min |
| Session output dir | sessions/forks/ |
sessions/forks/ |
Why separate implementations? The sleep fork runner was written to be self-contained — importing only from @mariozechner/pi-coding-agent — to avoid any risk of cross-file import breakage under jiti's real-path resolution. Now that both live in the same repo, sharing is more feasible, but it hasn't been done yet.
The globalThis depth registry: jiti loads extensions with moduleCache: false, so each fork session gets a fresh module instance. A module-level Map would reset on every load, breaking depth enforcement. Storing the registry on globalThis instead means it survives across all module loads in the process, regardless of how they were loaded.
The session_shutdown trick: dispose() on a Pi session only disconnects listeners — it does not fire lifecycle events. To get stateful-memory to write a session summary for a fork (making its work recallable), both fork runners explicitly call session_shutdown on the fork's _extensionRunner before disposing. This is accessing a TypeScript private field compiled to a regular JS property; if a Pi SDK upgrade breaks it, fork sessions degrade gracefully to explicit-remember-only durability.
force-tools dependency: The delegate tool must be present in agent/extensions/force-tools.ts's active tool list. force-tools.ts calls setActiveTools() on every session start and controls which tools are available. If delegate is missing from that list, it will be silently dropped.
Tests
cd /persist/pi
npm test
npm run smoke:memory
npm run smoke:topics
See docs/PROJECT_STATE.md for validation steps and smoke test expectations.