pgmem
Concepts

Pitfalls & footguns

The footgun register — how not to hurt yourself.

pgmem gives you fine control over an agent's memory, and fine control means sharp edges. This is the register of the ways integrators actually hurt themselves, grouped by area, each with the fix. Skim it before you ship.

Memory hygiene

Storing current state as memory. Copying a value another system owns ("subscription is cancelled", "balance is $0") into memory makes it a stale shadow copy the moment that system changes. Fix: apply the events-vs-state litmus test — store the event, fetch the state live.

add_episode(content=...) is ungoverned extraction. It runs the LLM over raw text and will happily mint a state edge from a state-laden payload. Fix: assert events through external_facts (no extraction, carries provenance), and keep current-state text out of the extraction content.

A derived projection is not immediately refreshed. The claim ledger is the source of truth; vector, graph, and narrative projections are rebuilt asynchronously after session.end() or by the projection worker. A newly observed claim is available to the session immediately, but a derived projection may lag. Fix: read active claims through the session or claim-recall surface when freshness matters, and treat projections as eventually consistent. See Trust & memory formation.

Authority and evidence are load-bearing. If every observation is presented as an agent inference, the default claim policy correctly treats it as weaker than a validated application or tool result. Fix: record the real Authority, attach evidence where available, and let the claim policy decide whether to create, corroborate, supersede, or hold a claim.

Latency & the critical path

Doing memory work on the response path. add_episode, compaction, and projection refresh are heavy (LLM + embeddings). Running them while the user waits stalls every turn. Fix: only compile_context (pure indexed reads) belongs on the turn; run everything else off-turn. See How a turn flows.

injected_context_provider runs on the turn. A provider that calls an API puts that network round-trip on the response path. Fix: fetch live state concurrently in your own turn code and pass it via the per-call injected_context= argument; reserve the provider for fast, pre-warmed lookups.

Lifecycle & state

Reusing an old session as memory. resume() is for picking up a session interrupted mid-flight (a crashed worker), not for "continuing" a past conversation. Reusing an old execution tree resurrects stale reasoning. Fix: create a fresh session per conversation and let retrieval bring back the saga and claims. See The five-tier model.

Expecting overwrite semantics. pgmem is bi-temporal: a correction creates a new validity interval, it does not mutate the old fact in place. Fix: read facts with their time bounds in mind; "the latest" is a query, not the only row. See The bi-temporal model.

compaction_triggered is not compaction. compile_context returning compaction_triggered: true (or dropped: ["T1"]) just means the budget fitter trimmed recent turns for this one prompt — nothing durable happened. Fix: treat it as a signal to call compact_async() off-turn; that is the durable operation.

Mechanics

Confusing mem.search with session.compile_context. They are different entry points: search is raw Tier 4 graph retrieval; compile_context is the full multi-tier prompt assembly under a budget. Fix: use compile_context to build a turn's context; reach for search only for direct graph queries.

The token budget counts content only. compile_context's budget measures message text — not provider chat framing or tool-call overhead. Fix: reserve headroom in your token_budget for the provider's framing.

Forgetting group_id. Every memory operation is scoped by group_id — it is the tenant boundary. Mixing or omitting it leaks memory across tenants. Fix: thread the correct group_id through every call; session child data inherits it transitively from the session handle.

Wrong embedding dimension. The schema is fixed at vector(1536). An embedder of a different dimension fails at construction. Fix: use a 1536-dim embedder (or configure the matching expected_embed_dim).

On this page