@augmem/cortext-pi-extension
Cortext memory for the pi coding agent: per-session-isolated durable memory, live per-turn recall, zero-LLM-call compaction, and a streaming interrupt gate.
Package details
Install @augmem/cortext-pi-extension from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@augmem/cortext-pi-extension- Package
@augmem/cortext-pi-extension- Version
0.1.1- Published
- Aug 18, 2026
- Downloads
- 317/mo · 13/wk
- Author
- gwillen
- License
- Apache-2.0
- Types
- extension
- Size
- 103.7 KB
- Dependencies
- 2 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./dist/index.js"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Cortext for Pi
Living memory for the pi coding agent — and compaction
for free. Every message is written to a native, on-device memory engine
(@augmem/cortext) as the conversation
happens, and every turn re-queries that memory live against the current
prompt. So when the context window fills up, nothing has to be summarized —
nothing was being thrown away in the first place.
A port of @augmem/cortext-openclaw-plugin
to pi's extension surface: same memory loop, same architecture, same test and
proof standards — on the events the harness actually emits.
# install from npm (listed in the pi package gallery, pi.dev/packages)
pi install npm:@augmem/cortext-pi-extension
# then restart pi
# dev install from source instead
git clone https://github.com/augmem/cortext-pi-extension ~/.pi/agent/extensions/cortext
cd ~/.pi/agent/extensions/cortext && npm install && npm run build
# then restart pi — the extension is auto-discovered
# (equivalently: pi -e /abs/path/to/dist/index.js for a single run)
Why
- Recall on every turn, not just after compaction. Each assembly queries memory live with the current prompt — what gets injected is relevant to what's being asked right now, and a correction made this turn is reflected on the next (no cross-turn cache, no frozen digest). Recall runs on two surfaces: the system prompt (per user prompt) and a hidden per-LLM-call message (so mid-run tool loops get fresh recall too). Mid-turn, a streaming gate watches the model's reasoning and answer as they stream and can stage recall for the next assembly.
- Compaction with zero LLM calls. Pi's native compaction pays a
summarizer every time the window fills and hopes the summary kept what
you'll need. Cortext compaction just moves a window: the extension returns
its own
CompactionEntry(bridge note + exchange-aligned cut,fromHook: true) and no summarizer LLM call is made — verified live (the compaction response carries nousage). Archived content comes back through query-relevant recall every turn. - Measured, not vibes. The live bench (
bench/live.mjs) runs a realpisession over RPC and proves the loop end-to-end: after an extension compaction archives the exchange containing a nonsense-token codename, the transcript shows the codename exists only in the archived prefix, and the next turn answers it from memory injection alone. Run it:npm run test:integration. - Fast enough to forget it's there. Median 2.74 ms per-message durable
ingest (mean 2.78 ms, p95 3.21 ms; N=200, warm store, 200-char messages,
node bench/latency.mjs), fully offline after a one-time model download, no per-turn network, no API keys. - Isolated by default. One SQLite store per conversation — a shared project agent can't leak one user's facts to another. Verified live with positive and negative controls (a different session cannot read the fact; the same scope does).
- The transcript is never mutated. Compaction is a pi-native entry that pi appends to the session file — the extension only chooses the cut and supplies the bridge summary. No destructive surgery.
The loop
flowchart LR
subgraph turn["every turn"]
M[message_end<br/>user / assistant / toolResult] -->|ingest, durable| S[(Cortext store<br/>on-device SQLite)]
P[current prompt] -->|before_agent_start + context| R{{live recall<br/>query-relevant LTM<br/>+ working memory}}
S --> R
R -->|memory block<br/>system prompt + hidden msg| CTX[model context]
G[message_update gate<br/>watches text + thinking deltas] -.->|stage recall| CTX
end
B[compact] -->|session_before_compact:<br/>exchange-aligned cut, 0 LLM calls| W[pi CompactionEntry<br/>verbatim tail rides along<br/>store keeps everything]
W --> R
Recall runs from turn one; compaction only changes how much verbatim tail rides along — the memory side never changes.
Compaction
Cortext answers pi's session_before_compact with its own compaction —
pi's own summarizer is never called. Because every message is already in the
durable store (message_end ingest), the returned CompactionEntry just
picks an exchange-aligned cut (a user-message entry, so the kept tail is a
self-contained exchange — never a tool result orphaned from its call) and
carries a bridge note in summary plus its provenance in details
(engine: "cortext", mode, dropped count). pi then natively rebuilds the
model context from that entry — summary + entries from firstKeptEntryId
onward — so the archived prefix simply stops riding along, and it keeps
coming back through query-relevant recall.
This is where the port diverges from the openclaw plugin in one deliberate
way: pi owns the kept window natively (its buildContextEntries reconstructs
from the compaction entry), so the openclaw plugin's own anchor/state
self-healing machinery was not ported — there is no second shadow window
to keep consistent.
Two modes (compactionMode):
hybrid(default) — keep the system prompt + memory injection + a verbatim tail of the lastprotectTailmessages, walked back to a user-message boundary so the tail is a self-contained exchange.full— keep the system prompt + memory injection only; the verbatim window shrinks to the current exchange. Maximum savings — memory IS the context.
If there is nothing before the protected window to archive, the extension
returns undefined and pi's default compaction applies — memory never
vetoed a compaction it had no content behind.
How it plugs in
Four pi surfaces (verified against the installed pi 0.84.2 dist/ types,
not docs — see src/pi.d.ts):
- Ingest —
message_end(user, assistant, toolResult). Tool-call arguments are rendered as[tool call] name {args}truncated at 2,000 chars in the durable record (the command matters; a 100 KB patch payload shouldn't dominate the store); tool results are ingested in full. - Recall —
before_agent_startappends a fenced memory block to the (chained) system prompt per user prompt;context(fires before every LLM call) appends adisplay: falsecustom message carrying only memories not already in that run's system-prompt block, plus — once a compaction entry is in context — the live working-memory snapshot deduplicated against the kept window. Both drain recall the streaming gate staged, under the same scope key. - Compaction —
session_before_compactreturns the extension compaction (zero LLM calls) orundefined(see above). - Streaming gate —
message_updatefeedstext_delta/thinking_deltathrough Cortext's interrupt gate. Onshould_interrupt/at_boundarythe recalled memory is staged on a bounded bus keyed by scope, for the next assembly. Observe-only — see limits.
Install & configure
pi has no per-extension config surface (verified against the installed
0.84.2 Settings types), so config resolves in order:
CORTEX_PI_CONFIGenv var (JSON object) — wins~/.pi/agent/cortext/config.json- defaults (a malformed source is skipped, never fatal: memory must not break the agent)
Resolution happens once when the extension registers — there is no config watching; restart pi to apply changes.
| Key | Default | Meaning |
|---|---|---|
dbPath |
cortext |
Directory (under ~/.pi/agent/cortext) for stores; '.'/'..' fall back to cortext (traversal guard) |
memoryScope |
session |
Isolation boundary: session / agent / global |
focus |
0.45 |
F knob: retrieval breadth vs precision |
sensitivity |
0.5 |
S knob: affective relaxation of the gate |
stability |
0.5 |
T knob: gate refractory + boundary pacing |
recallLimit |
12 |
max memories injected per assembly |
interruptGate |
true |
run the streaming gate |
ingestReasoning |
true |
feed thinking deltas, not just answer text |
autoConsolidate |
true |
consolidate on compaction |
compactionMode |
hybrid |
hybrid: memory + verbatim tail; full: memory only |
protectTail |
6 |
hybrid: trailing messages kept verbatim (exchange-aligned) |
Requirements: Node.js ≥ 18; a platform with a @augmem/cortext native
prebuild (installed as a dependency). On first use Cortext downloads its
local model assets once; after that, memory runs fully offline.
Isolation
Cortext keeps one SQLite store per isolation scope — source ids are
metadata within a store, so distinct scopes are distinct databases (staged
gate memory is keyed by the same scope, so it can't cross the boundary
either). memoryScope:
session(default) — one store per session (ctx.sessionManager.getSessionId()). Safe when an agent serves multiple people: memory never crosses conversations.agent— one store per project (safe(basename(cwd)),mainwhen absent) — memory persists across that project's sessions. Use only for single-user projects. Note the identity is the cwd basename: two projects with the same folder name share a store (documented trade-off; pi has no stable project id in the extension context).global— a single shared store.
Both session isolation (positive + negative controls) and scope-key
determinism are asserted by the unit suite; live session isolation — with a
nonsense-token needle a model can't guess — is asserted by
bench/live.mjs.
Design and limits
- Recalled memory is untrusted input. A prior turn could have ingested a prompt-injection payload. Before injection, recalled text is stripped of data-fence breakouts and fake system markers, and wrapped in a block that explicitly labels it as reference data, not instructions. This is a mitigation, not a guarantee.
- Fine-grained recall is a work in progress. Retrieval reliably brings back conversationally salient facts (decisions, named results, user statements); one-off identifiers buried in bulk tool output are hit or miss (same state as the openclaw plugin and the underlying engine — see its release notes for needle-probe trends).
- The gate cannot splice into a live decode, and cannot request a
re-pass. Pi's agent event stream is observe-only and has no
before_agent_finalize-equivalent hook, so on interrupt the extension stages recall for the next assembly and that is the whole re-pass story. The openclaw gateway'srevisebehavior (theforceRepasskey) has no pi counterpart and is deliberately out of scope here. - Tool-call arguments are truncated at 2,000 chars in the durable record; tool results are ingested in full.
- The engine's consolidation hint is deliberately not acted on at ingest. Compact-time consolidation is the sufficient, safe cadence (openclaw parity; measured retrieval is identical with or without the hint).
- pi owns the window. After a Cortext compaction, the kept window is whatever pi reconstructs from the entry; the extension does not (and cannot) re-window on every assembly the way the openclaw context engine did. The per-LLM-call recall surface compensates: once windowed, the working-memory snapshot rides along with every recall call.
- An engine that can't open degrades its scope, not the agent. If the
native store cannot be opened for a scope (missing store dir, corrupt DB
file, binding rejection), that scope degrades to recall-less passthrough
with a one-time
cortext store errorlog line per scope — the agent keeps running, and memory for that scope stays off for the rest of the pi process lifetime: there is no automatic retry, so restarting pi re-attempts the store open. - Telemetry is spans only, and fully offline. Control and failure paths
emit OpenTelemetry spans via
@opentelemetry/api(a no-op proxy when no collector/SDK is present), so the extension stays fully offline with no per-turn network and no new diagnostics dependencies beyond the OTEL API; span attributes are bounded enums/numbers only.
Verified
Every release passes, against a real pi 0.84.2 installation and the
shipped engine, before publish:
- Unit + types —
npm run typecheckandnpm test(83 tests, native engine, offline): config resolution, store/scope-key determinism, engine ingest/recall/consolidate, compaction cut logic (exchange alignment, previous-boundary floor, fallback), stream-gate segmentation/staging, bus bounding, plus pi-surface tests for the handler set (ingest mapping, recall injection shapes, zero-LLM compaction result, shutdown flush).scripts/verify-entry.mjsasserts the factory registers exactly the expected handlers against a stubExtensionAPI. - Live loop —
npm run test:integration(bench/live.mjs) drives a realpi --mode rpcsession in an isolated temp project (own--session-dir,--no-extensions,--approve'd project-level compaction settings, uniqueCORTEX_PI_CONFIGdbPath — which also proves the env config path) and asserts, per control:- compaction-zero —
compactyields an extension-providedCompactionEntry(details.engine="cortext",fromHook: true) with nousage(zero summarizer LLM calls) - compaction-window — the needle entry is provably before
firstKeptEntryIdon the branch (archived, exchange-aligned cut) - recall-post-compaction — the next turn answers the needle, which exists only in the archived prefix (memory injection is the only source)
- isolation-session / isolation-store — a different live session (and a fresh handle on its store) cannot read the fact; the same scope can (positive control)
- ingest-durable — a fresh handle on the same SQLite DB recalls the needle (cross-process durability)
- gate-observe / ingest-mechanism — the streaming gate observed
assistant messages and processed turns with zero handler errors
(interrupt/boundary fires are state-dependent; observed fires are
reported as evidence, e.g.
gate fires=48)
- compaction-zero —
Verified live twice in a row (both 8/8), with
qwen3.8/Qwen3.8-27B at thinking=low, pi 0.84.2:
PASS compaction-zero (extension CompactionEntry, no summarizer LLM call)
PASS compaction-window (needle provably archived, exchange-aligned cut)
PASS recall-post-compaction (archived-only needle answered from memory)
PASS isolation-session (different session cannot read the fact, live model check)
PASS ingest-durable (fresh handle on S1 store recalls the needle)
PASS isolation-store (fresh handle on S2 store does NOT recall the needle)
PASS gate-observe (streaming gate observed, no handler errors)
PASS ingest-mechanism (extension active, recall injected, no handler errors)
bench: 8/8 controls passed
(The live bench needs a configured model for the pi session; it makes real LLM calls but no compaction summary call.)
Develop
npm install
npm run build # tsc → dist/
npm run typecheck
npm test # build + unit tests (native engine; fast, offline)
npm run test:integration # live bench against the installed pi
npm test includes a build-reproducibility gate (npm run build:verify
diffs a fresh tsc emit against dist/), a dependency-inventory gate
(npm run notice:verify fails when a package.json runtime dependency is
not named in NOTICE), and a test-count gate
(npm run testcount:verify fails when the README Verified test count
drifts from tests/*.test.mjs); a GitHub Actions CI workflow
runs typecheck, the full test chain, and npm audit.
The engine-backed tests need the one-time on-device model (a pinned,
sha256-verified 135 MB AIST GGUF plus tokenizer, fetched from Hugging
Face and cached in the platform cache dir; CORTEXT_MODEL_CACHE_DIR
overrides the location). On a fresh machine run
node scripts/ci-warm-model.mjs once — CI does the same with a
sha-keyed runner cache.
src/pi.d.ts is transcribed from the installed
@earendil-works/pi-coding-agent package's dist/ types — if pi's
extension surface changes, the transcription is re-verified against the real
package, not docs.