Skip to main content

File-based persistent memory for AI agents. Zero dependencies.

Project description

Parsica-Memory

Persistent, intelligent memory for AI agents. Memory that picks up where you left off, anywhere. Zero dependencies. Pure Python. File-backed.

PyPI version Python 3.9+ License: Apache 2.0


What Is This?

AI agents forget everything between sessions. parsica-memory fixes that. It gives any agent a persistent, searchable memory store that:

  • Persists across sessions, servers, platforms, spawns, and restarts
  • Retrieves the right memories through a 12-layer BM25+ search engine
  • Decays old memories so signal stays high over time
  • Enriches memories via optional LLM metadata at write time for much better recall
  • Shares knowledge across multi-agent teams
  • Learns from mistakes, facts, and procedures with specialized memory types
  • Cross-session recall - facts and decisions surface across sessions automatically. Pulls x amount (configurable) of top ranked memories and recent memories from shared memory store across any session, server, or platform.
  • Talk on Telegram and pick back up on Discord.
  • Core features require no vector database, no API keys, and no external services. Optional enrichment can use an LLM provider or local compatible endpoint.

Full benchmarks and methodology

LOCOMO 2000-question benchmark Published benchmark details live on the benchmarks page above. The README benchmark summary below is a local package-level snapshot measured on a Mac Mini M4 with 9,896 live memories.


How to Read This README

This package has four layers. Knowing which one you need saves time:

  1. MemoryManager - the batteries-included wrapper for most applications. Easiest path.
  2. MemorySystem - the low-level engine with full control over ingest, search, graph, tiers, and maintenance.
  3. CLI - operational/admin commands for initializing, inspecting, rebuilding, and serving a workspace.
  4. Advanced internals - the 12-layer search engine, sharding, graph intelligence, shared pools, WAL, recency, and enrichment pipeline.

If you just want to ship something fast, start with MemoryManager. If you want full control, use MemorySystem. If you want to inspect or serve a store, use the CLI.


Quick Start

pip install parsica-memory

Using Parsica with OpenClaw

Parsica Memory ships with a native OpenClaw plugin. This gives your OpenClaw agent persistent memory across all sessions, channels, and platforms -- no extra setup beyond copying a folder.

What's included

The openclaw-plugin/ directory in this repo contains everything you need:

  • index.js -- the OpenClaw plugin entry point. Handles registration, command routing (/prune, /memory, /search), and lifecycle hooks (before_agent_start, agent_end).
  • pipeline_bridge.py -- the Python bridge process that runs Parsica Memory's retrieval engine, enrichment pipeline, and memory ingestion. Communicates with index.js via JSON-over-stdin/stdout.

Installation

  1. Install the Python library:
pip install parsica-memory
  1. Copy the plugin to your OpenClaw extensions:
cp -r openclaw-plugin/ ~/.openclaw/extensions/parsica-memory/
  1. Register the plugin in your openclaw.json:

Add this to your plugins.entries section:

{
  "plugins": {
    "entries": {
      "parsica-memory": {
        "path": "~/.openclaw/extensions/parsica-memory",
        "enabled": true,
        "config": {
          "memoryPath": "~/.openclaw/workspace/parsica_memory_store",
          "searchLimit": 5
        }
      }
    }
  }
}

Enrichment is on by default and uses your gateway's LLM provider automatically. No extra API key needed unless you want to use a different provider for enrichment (see Advanced enrichment configuration below).

  1. Restart your OpenClaw gateway.

  2. Verify it's working:

After restart, send any message to your agent. Then check:

/prune sessions

If Parsica is running, you'll see a response listing your sessions. You can also check:

/search test

to confirm retrieval is working.

That's it. Parsica will now:

  • Ingest every conversation turn into persistent memory
  • Enrich each memory with tags, summaries, and search keywords via your gateway's LLM
  • Recall relevant memories before each response (injected into context via before_agent_start)
  • Cross-session recall -- memories from one channel surface in another when relevant
  • Cross-platform recall -- talk on Telegram, pick back up on Discord
  • Recency recall -- recent cross-channel memories are injected automatically

Configuration options

Option Default What it does
memoryPath ./parsica_memory_store Where the memory store lives on disk
searchLimit 5 How many memories to inject per turn
recencyEnabled true Inject recent cross-channel memories
recencyWindow 6.0 Hours of recency to consider
recencyLimit 5 Max recent memories to inject
crossSessionRecall semantic Cross-session recall mode: semantic, all, or none

Note on memory isolation: If you run multiple users on the same OpenClaw instance and need per-user memory isolation, use separate memoryPath values for each user. By default, all sessions share one memory store, which is what enables cross-session and cross-platform recall.

Plugin commands

Once installed, these commands are available in your OpenClaw chat:

  • /search <query> -- search the memory store
  • /prune sessions -- clean up stale sessions
  • /prune small|medium|large -- prune old/low-value memories (dry-run first, add confirm to apply)
  • /enrichment on -- enable LLM enrichment at ingest time
  • /enrichment off -- disable enrichment
  • /enrichment status -- show enrichment state, model, and coverage
  • /decompose on -- enable fact decomposition at ingest time
  • /decompose off -- disable decomposition (default)
  • /decompose status -- show decomposition state
  • /decompose run -- retroactively decompose existing memories

Enrichment

Enrichment is one of Parsica's most important features. When enabled (on by default), every ingested memory is processed by an LLM to generate:

  • Tags -- topical labels for better retrieval matching
  • Enriched summary -- a concise summary that surfaces the core facts
  • Search keywords -- additional terms that bridge vocabulary gaps between how you stored something and how you'll search for it later

Why enrichment matters:

We benchmarked enrichment coverage against recall quality on a live 18,000-memory store:

Coverage R@1 R@5 MRR
20% enriched 37.6% 55.6% 0.459
60% enriched 48.8% 62.4% 0.549
100% enriched 56.0% 67.2% 0.609

R@1 nearly doubles from 20% to 100% coverage. MRR climbs from 0.459 to 0.609. This is one of the largest single levers in the system.

How it works out of the box:

Enrichment uses your gateway's default LLM provider automatically. No separate API key is needed. Any model works. Haiku is recommended for cost efficiency.

Toggle it live:

/enrichment off     # disable without editing config
/enrichment on      # re-enable
/enrichment status  # check current state + coverage

Advanced enrichment configuration

For power users who want more control:

{
  "config": {
    "enrichModel": "anthropic/claude-haiku-4-5-20251001",
    "anthropicApiKey": "sk-ant-...",
    "openaiApiKey": "sk-...",
    "googleApiKey": "AIza..."
  }
}

You can provide API keys from multiple providers. Parsica will use the key that matches the model you specify. For security, consider using OpenClaw's auth-profiles system instead of putting keys directly in your config file.

Retroactive enrichment with multiple keys:

If you have an existing memory store and want to enrich it in bulk, use the CLI harness with multiple API keys:

parsica enrich --workspace ./store
parsica enrich --workspace ./store --keys key1,key2,key3,key4,key5
parsica enrich --workspace ./store --coverage

Multiple keys are used in round-robin sequence, distributing load across providers and reducing rate-limit pressure on large stores. True parallel execution is planned for a future release.

Fact decomposition

Decomposition breaks memories into atomic facts, which can improve evidence recall and detail extraction for certain workloads.

Decomposition is off by default. This is intentional. Decomposition is powerful but opinionated -- it changes how memories are stored and ranked. For some workloads (technical memory, project tracking, coding assistants), it's a clear win. For others (conversational assistants, journaling), it can fragment context in ways that hurt top-1 precision.

Toggle it live:

/decompose on       # enable at ingest time
/decompose off      # disable (default)
/decompose status   # check current state
/decompose run      # retroactively decompose existing memories

When to use decomposition:

  • Your agent needs to recall specific facts from long conversations
  • You care more about evidence recall than conversational context
  • Your store contains dense, multi-fact memories that don't surface well as whole units

When to leave it off:

  • Your agent is conversational and benefits from full-context memories
  • You want memories to feel natural, not clinical
  • You're not sure yet -- start without it, measure, then decide

This is what Parsica means by "tunable memory quality." Every lever is visible, every tradeoff is documented, and every control point is accessible.

What you get with zero dependencies

Even without enrichment, the core BM25+ retrieval engine provides:

  • 12-layer retrieval with phrase matching, rarity boosting, field weighting, windowing, and more
  • File-native storage (JSON, human-readable, grep-able)
  • Zero runtime dependencies beyond Python stdlib
  • 185 memories/sec ingest, 65.7ms p50 search

See the full 12-layer engine documentation for details on each retrieval layer.

Troubleshooting

Plugin loads but nothing happens:

  • Check that your openclaw.json uses "parsica-memory" as the plugin key (not "antaris-suite")
  • Make sure pipeline_bridge.py is in the same directory as index.js
  • Check that parsica-memory is installed: pip show parsica-memory

Enrichment not working:

  • Run /enrichment status to check if it's enabled
  • Verify your gateway has a working LLM provider configured
  • Check gateway logs for bridge errors

Slow search on large stores:

  • Stores under 1,000 memories: expect <70ms p50
  • Stores over 10,000 memories: expect 100-500ms depending on query complexity
  • Cold-cache first queries on large stores can take 2-4 seconds

Python version:

  • Parsica Memory requires Python 3.9+

MemoryManager

MemoryManager is the batteries-included wrapper. It gives you everything you need to build a memory-enabled application without writing boilerplate.

What It Does

  • Per-user isolation - each user gets their own memory store in a sub-directory
  • Noise filtering - automatically skips junk (context packets, system messages, very short text)
  • Cross-session and platform semantic recall - facts and decisions cross session/platform boundaries, episodic chatter stays scoped
  • Auto-save - saves to disk after every ingest so nothing is lost on crash
  • LLM enrichment - optional, pluggable, works with any provider
from parsica_memory import MemoryManager

# Basic - no enrichment (BM25-only, still good)
mm = MemoryManager("./store")

# With enrichment - pass any API key
mm = MemoryManager("./store", api_key="sk-ant-...", provider="anthropic")

# Or auto-detect from environment variables
mm = MemoryManager("./store", auto_enrich=True)

# Store a conversation
mm.ingest("user123", "What's the capital of France?", "The capital of France is Paris.")

# Recall relevant memories
results = mm.recall("user123", "France capital")
for r in results:
    print(r)

# Search with full metadata
results = mm.search("user123", "Paris", explain=True)
for r in results:
    print(f"{r['relevance']:.2f} - {r['content'][:80]}")

Option 2: Direct MemorySystem - The Raw Engine (Full Control)

No user isolation built in. You manage scope yourself. But you get access to everything: search params, graph intelligence, sharding, tiering, maintenance, shared pools, etc.

from parsica_memory import MemorySystem

mem = MemorySystem(workspace="./memory", agent_name="my-agent")
mem.load()

mem.ingest("Deployed v2.3.1 to production. All checks green.",
           source="deploy-log", session_id="session-123")

results = mem.search(
                     "production deployment",
                     session_id="session-456",
                     cross_session_recall="semantic",
                     source_channel="discord:general"
                ) 
                
results = mem.search("deployment", explain=True)
for r in results:
    print(r.entry.created)  # "2026-03-17T18:30:45.123456"

mem.save()

When to use which:

MemoryManager MemorySystem
Multi-bot user Yes - built for this You'd have to manage users yourself
Single agent Works but overkill Perfect
Shared memory across runtimes Not designed for this Yes - point two runtimes at same workspace
Full search control Limited (simplified API) Full access to all params
Graph / tiering / maintenance Hidden Direct access

Installation

pip install parsica-memory

Current package line: 2.x (parsica-memory currently ships as 2.7.0) Requirements: Python 3.9+. Zero external dependencies. stdlib only.


Setup Wizard

Running python -m parsica_memory init launches an interactive wizard that guides you through the full setup flow:

# Interactive (prompts you through setup)
python -m parsica_memory init --workspace ./memory --agent-name my-agent

# Headless (skip wizard, use defaults)
python -m parsica_memory init --workspace ./memory --headless

# Automation-safe: non-TTY shells auto-fallback to headless mode
python -m parsica_memory init --workspace ./memory

Wizard Flow

1. Manager vs System - Choose between MemoryManager (batteries-included, per-user isolation) or MemorySystem (full low-level control). The wizard generates starter code tailored to your choice.

2. Import - Optionally import existing memories from another system. Six source options: mem0, Memvid, MEMO, Zep, LangMem, OpenAI (threads export), plus generic json, jsonl, sqlite, markdown, and csv formats.

3. Enrichment setup - If an API key is detected in your environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, PARSICA_LLM_API_KEY), the wizard prompts to enable enrichment. Key prefix detection identifies the provider automatically - no manual provider selection needed.

4. Model selection - Pick from a recommended model list with cost and quality indicators. Example:

Model Provider Quality Cost per 1K
claude-haiku-4-5 Anthropic Good ~$0.05
claude-sonnet-4-6 Anthropic Best ~$0.50
gpt-4o-mini OpenAI Good ~$0.15
gemini-2.0-flash Google Good ~$0.05
llama3.2 (local) Ollama Variable $0.00

5. Retroactive enrichment - If you have existing memories in the workspace (or just imported from another system), the wizard offers to enrich them now. It shows a time estimate based on store size and the selected model's typical throughput. If multiple API keys are available, it offers the multi-key option (see ParallelEnricher).

6. Background processing - Enrichment runs in the background with a live progress display (done/total counter). You can close the terminal; enrichment resumes on next startup if interrupted.

The wizard writes parsica_config.json to the workspace with your chosen settings and outputs a ready-to-run Python snippet.


Constructor

MemoryManager(
    store_path="./store",      # Root directory for all user stores
    search_limit=10,           # Max results per search
    min_relevance=0.3,         # Minimum score to include (0-1)
    api_key=None,              # LLM API key for enrichment (optional)
    provider=None,             # "anthropic", "openai", "gemini", "local", or "auto"
    model=None,                # Model override (uses sensible defaults)
    auto_enrich=False,         # Auto-detect enricher from env vars
    agent_name="parsica",      # Name for memory provenance
)

Methods

# Ingest a conversation turn (filters noise, enriches if configured, auto-saves)
mm.ingest(user_id, user_message, bot_response, session_id="", channel_id="")

# Ingest arbitrary content (documents, notes, facts)
mm.ingest_raw(user_id, content, source="manual", category="note")

# Recall relevant memories as plain strings
results = mm.recall(user_id, query, session_id="", limit=None, confidence_floor=0.0)

# Recall with metadata (provenance, confidence, etc.)
results = mm.recall_structured(user_id, query, session_id="", limit=None, confidence_floor=0.0)

# Search with full metadata (relevance, source, category, matched_terms)
results = mm.search(user_id, query, session_id="", limit=None, explain=False, confidence_floor=0.0)

# Batch-enrich all existing memories for a user
count = mm.enrich_all(user_id, overwrite=False, progress_fn=None)

# Batch-decompose all existing memories into atomic facts
# Note: this uses the underlying MemorySystem; MemoryManager does not expose
# a first-class decompose_all() wrapper.
count = mm._get_store(user_id).re_decompose(overwrite=False, progress_fn=None)

# Export a user's memories to JSON
count = mm.export(user_id, "./export.json")

# Get stats for a user or all users
stats = mm.stats(user_id)

# Delete all memories for a user
count = mm.clear(user_id)

Provider Auto-Detection

If you pass just an API key without specifying a provider, MemoryManager guesses from the key prefix:

Key Prefix Detected Provider Default Model
sk-ant- Anthropic claude-sonnet-4-6
sk- OpenAI gpt-4o-mini
AI Google Gemini gemini-2.0-flash

Or set provider="auto" / auto_enrich=True to check environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY) in order. The provider-agnostic entry point PARSICA_LLM_API_KEY is also supported (legacy ANTARIS_LLM_API_KEY still works).


LLM Enrichment & Fact Decomposition: How It Works

  • Enrichment is the main reason parsica-memory outperforms vector-based memory systems at recall. When you ingest a memory, an optional LLM call generates additional tags, summaries, keywords, and search_queries for each memory.

  • Fact Decomposition - takes existing memories and breaks them into separate atomic fact entries with parent linking, fuzzy dedup, and individual search surface.

The Problems These Solve

Without either, a memory like:

"We decided to use FastAPI for the backend"

...can only be found by searching for words that appear in it: "FastAPI", "backend", "decided". But a user might search for "web framework", "API server", "http library", or "what tech stack did we pick?" - none of which appear in the original text.

How Enrichment and Fact Decomposition Fixes This

At ingest time, an LLM reads the memory and generates multiple new fields as metadata:

Enrichment

Field What It Contains Search Weight
tags 5-10 semantic concept labels (web_framework, architecture_decision, python) Boosted in field matching
summary One-sentence rephrase using DIFFERENT vocabulary than the original 2x weight in search scoring
keywords 8-15 terms a person might type when looking for this memory 2x weight in search scoring
search_queries 3 specific natural language questions ONLY this memory answers 3x weight in search scoring

Fact Decomosition

Field What It Contains Search Weight
Atomic Facts breaks memories down into multiple different facts, tied to keywords and context Boosted in field matching
Parent Linking Links each fact to the parent memory 2x weight in search scoring
Fuzzy Dedup Automatically removed duplicates from the store for efficiency Boosted in field matching
Individual Search Surface Gives more ways to link back to the correct memory based on new facts 2x weight in search scoring

For the FastAPI example, enrichment might produce:

{
  "tags": ["web_framework", "architecture_decision", "python", "backend", "api_server"],
  "summary": "The team selected FastAPI as their Python HTTP framework for the API layer",
  "keywords": ["framework", "http", "api server", "asgi", "tech stack", "web library"],
  "search_queries": [
    "What web framework did we choose for the backend?",
    "Why did we pick FastAPI over Flask or Django?",
    "What is our Python API server technology?"
  ]
}

Now the memory is findable by ANY of those terms, not just the original words.

Enrichment at Ingest Time

When you configure the enricher and Fact Decomposer, every new memory gets enhanced automatically at write time:

from parsica_memory import MemoryManager

# Enrichment happens automatically on every ingest
mm = MemoryManager("./store", api_key="sk-ant-...", provider="anthropic")
mm.ingest("user1", "Let's use PostgreSQL for the database", "Good choice, PostgreSQL it is.")
# ^ This memory is now findable by: "database", "postgres", "sql",
#   "what DB are we using", "data storage decision", etc.

Enriching and Re-Decomposing Existing Memories (Backfill)

Already have memories stored without enrichment? You can batch-process them through MemoryManager.enrich_all() and the underlying MemorySystem.re_decompose() store method:

from parsica_memory import MemoryManager

mm = MemoryManager("./store", api_key="sk-ant-...", provider="anthropic")

# Enrich all un-enriched memories for a user
count = mm.enrich_all("user1")
print(f"Enriched {count} memories")

# Force re-enrich everything (even already-enriched entries)
count = mm.enrich_all("user1", overwrite=True)

# With progress tracking
def progress(done, total):
    print(f"  {done}/{total}...")
count = mm.enrich_all("user1", progress_fn=progress)

# Fact decomposition for each existing memory uses the underlying store
count = mm._get_store("user1").re_decompose()
print(f"Created {count} new fact entries")

Using the Low-Level API for Backfill

If you're using MemorySystem directly instead of MemoryManager:

from parsica_memory import MemorySystem
from parsica_memory.enrichers import anthropic_enricher

# Create enricher
enrich = anthropic_enricher("sk-ant-...")

# Load existing store and attach enricher
mem = MemorySystem(workspace="./store", agent_name="my-agent", enricher=enrich)
mem.load()

# Metadata only (default) - adds tags, summary, keywords, search_queries
count = mem.re_enrich(
    batch_size=50,          # Progress callback fires every N entries
    overwrite=False,        # Skip already-enriched entries
    decompose=False,        # Default - metadata only, no fact decomposition
    progress_fn=lambda done, total: print(f"{done}/{total}")
)
print(f"Enriched {count} memories")

# Combined pass - metadata enrichment + fact decomposition in one LLM call per entry
count = mem.re_enrich(
    batch_size=50,
    overwrite=False,
    decompose=True,         # Enrichment + decomposition in one pass
    progress_fn=lambda done, total: print(f"{done}/{total}")
)
print(f"Enriched and decomposed {count} memories")

mem.save()

re_enrich(decompose=True) runs both enrichment and decomposition in a single LLM call per entry, which is cheaper than running re_enrich() followed by re_decompose() separately. Use it when you want both and haven't enriched the store yet.

Multi-Key Parallel Enrichment

For large stores, ParallelEnricher distributes work across multiple API keys in round-robin sequence. Each key gets an equal share of the entries and runs independently. Results merge back to the workspace automatically.

from parsica_memory.parallel_enricher import ParallelEnricher

enricher = ParallelEnricher(
    workspace="./memory",
    keys=[
        {"key": "sk-ant-...", "model": "claude-haiku-4-5-20251001"},
        {"key": "sk-proj-...", "model": "gpt-4o-mini"},
    ],
    decompose=True,
)
result = enricher.run(progress_fn=lambda done, total: print(f"{done}/{total}"))
print(f"Enriched {result['enriched']} entries using {result['workers']} workers")

ParallelEnricher is also accessible through the setup wizard when multiple API keys are detected in the environment.


Fact Decomposition

Enrichment adds metadata to memories. Decomposition goes further: it breaks memories into separate atomic fact entries, each independently searchable.

Why Decomposition Matters

A single conversation turn often contains multiple distinct facts buried in one blob:

"We decided to use FastAPI for the backend, PostgreSQL for the database, and deploy on Railway"

With enrichment only, this is stored as one entry. Decomposition pulls each fact out:

  • "The team chose FastAPI for the backend"
  • "PostgreSQL was selected as the database"
  • "Deployment target is Railway"

Each fact becomes its own memory with its own search surface. The difference in recall for specific-fact queries is significant.

Decomposed facts also get:

  • memory_type="fact" - crosses session boundaries automatically, high recall priority
  • provenance="decomposed" - tracks that it was extracted, not created directly
  • parent_hash - links back to the original memory
  • confidence=0.8 - starts high
  • Fuzzy dedup - won't create duplicate facts if the same info already exists (Jaccard similarity >= 0.85 threshold)

How re_decompose() Works Internally

  1. Takes all existing memories (skips entries that are already memory_type="fact")
  2. Runs the LLM enricher on each one to get a facts field (list of atomic fact strings)
  3. For each fact string - creates a new MemoryEntry with memory_type="fact", provenance="decomposed", parent_hash=source.hash, confidence=0.8
  4. Dedup checks: exact hash match, normalized content match, then fuzzy similarity against all existing facts
  5. If a near-duplicate exists, bumps the existing entry's sighting_count and confidence instead of creating a duplicate
  6. Saves to disk after all new facts are created

The overwrite=False default (recommended) skips any source entry whose hash already appears as a parent_hash somewhere in the store - meaning it won't re-decompose something it already processed.

Using re_decompose() with MemoryManager

from parsica_memory import MemoryManager

mm = MemoryManager("./store", api_key="sk-ant-...", provider="anthropic")

# Decompose all existing memories for a user
count = mm._get_store("user1").re_decompose()
print(f"Created {count} new fact entries")

# Force re-decompose (even entries that were already processed)
count = mm._get_store("user1").re_decompose(overwrite=True)

# With progress tracking
def progress(done, total, new_facts):
    print(f"  Processed {done}/{total}, created {new_facts} facts so far...")
count = mm._get_store("user1").re_decompose(progress_fn=progress)

Using re_decompose() with MemorySystem

from parsica_memory import MemorySystem
from parsica_memory.enrichers import anthropic_enricher

enrich = anthropic_enricher("sk-ant-...")
mem = MemorySystem(workspace="./store", agent_name="my-agent", enricher=enrich)
mem.load()

count = mem.re_decompose(
    batch_size=50,           # Progress callback fires every N entries
    overwrite=False,         # Skip entries already decomposed
    progress_fn=lambda done, total, new_facts: print(f"{done}/{total} (+{new_facts} facts)")
)
print(f"Created {count} new fact entries")
mem.save()

Cost Estimate

Decomposition uses one LLM call per source memory, with a 600-character input cap and 256-token output cap - same as standard enrichment. But decomposition processes every non-fact memory in the store, so the total cost scales with store size. If you run re_enrich() and re_decompose() on the same store, you're paying for two passes.

Any provider's API key works. Approximate cost per 1,000 memories:

Provider Model ~Cost per 1,000 memories
Anthropic claude-sonnet-4-6 ~$0.50
OpenAI gpt-4o-mini ~$0.15
Google gemini-2.0-flash ~$0.05
Local (Ollama) llama3.2 $0.00

Core vs Enrichment

Core memory features work without any API key or external service: persistence, search, recency, sharding, graph features, CLI ops, and shared/team memory are all available locally.

LLM-powered enrichment and fact decomposition are optional add-ons. They improve recall by generating extra metadata (tags, summary, keywords, search_queries, facts) but they are not required to use the package.

Best Practice: Enrich First, Then Decompose

# Step 1: enrich (adds tags, summary, keywords, search_queries, facts field)
count = mem.re_enrich(batch_size=50, overwrite=False)
print(f"Enriched {count} memories")

# Step 2: decompose (uses the facts field from enrichment to create atomic entries)
count = mem.re_decompose(batch_size=50, overwrite=False)
print(f"Created {count} fact entries")

mem.save()

Running re_enrich() first ensures the facts field is populated before decomposition reads it. Running them in reverse order means decomposition is working from whatever partial enrichment already existed.


Pre-Built Enricher Templates

Four provider-ready enricher factories ship with Parsica-Memory. All use raw urllib from the standard library - zero SDK dependencies.

Anthropic (Claude)

from parsica_memory.enrichers import anthropic_enricher

enrich = anthropic_enricher(
    api_key="sk-ant-...",            # Or set ANTHROPIC_API_KEY env var
    model="claude-sonnet-4-6",       # Default
)
mem = MemorySystem("./store", agent_name="my-agent", enricher=enrich)

OpenAI (GPT)

from parsica_memory.enrichers import openai_enricher

enrich = openai_enricher(
    api_key="sk-...",                # Or set OPENAI_API_KEY env var
    model="gpt-4o-mini",            # Default (cheap and fast)
    base_url="https://api.openai.com",  # Change for Azure, etc.
)

Google Gemini

from parsica_memory.enrichers import gemini_enricher

enrich = gemini_enricher(
    api_key="AI...",                 # Or set GOOGLE_API_KEY env var
    model="gemini-2.0-flash",       # Default
)

Local Models (Ollama, LM Studio, vLLM)

from parsica_memory.enrichers import local_enricher

enrich = local_enricher(
    base_url="http://localhost:11434",  # Ollama default
    model="llama3.2",
    timeout=60,                         # Longer timeout for local models
)

No API key needed. Works with anything that speaks the OpenAI Chat Completions format.

Auto-Detect

from parsica_memory.enrichers import auto_enricher

# Checks ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY,
# and PARSICA_LLM_API_KEY (provider-agnostic) in order
enrich = auto_enricher()
if enrich:
    mem = MemorySystem("./store", agent_name="my-agent", enricher=enrich)

Custom Enricher

Write your own - just return a dict:

def my_enricher(text: str) -> dict:
    # Call whatever you want
    return {
        "tags": ["tag1", "tag2"],
        "summary": "One sentence rephrase",
        "keywords": ["term1", "term2", "term3"],
        "search_queries": [
            "Question 1?",
            "Question 2?",
            "Question 3?"
        ],
        "facts": [
            "Atomic fact extracted from the text.",
            "Another atomic fact."
        ]
    }

mem = MemorySystem("./store", agent_name="my-agent", enricher=my_enricher)

All fields are optional. The facts field is used by re_decompose(). Missing fields are skipped if omitted.


Provenance Tracking

Every memory carries provenance metadata - where it came from and how it got there:

Provenance Meaning
self Created directly by this agent (default)
decomposed Extracted as an atomic fact from a conversation
shared Received from another agent via shared memory pool
transferred Migrated from another memory system
workspace Ingested from a workspace file
manual Manually added by a user or admin
# Structured recall includes provenance
results = mm.recall_structured("user1", "password")
for r in results:
    print(f"{r['content']} (from: {r['provenance']}, confidence: {r['confidence']:.2f})")

Fuzzy Dedup and Canonicalization

When the same fact appears in slightly different phrasings, Parsica-Memory detects the overlap and links them instead of storing duplicates.

How it works:

  • At ingest time, new facts are compared against existing facts using Jaccard word similarity
  • If similarity >= 0.85 (configurable), the existing canonical fact is kept and its metadata is updated: confidence bumped by +0.05 (capped at 1.0), sighting_count incremented, last_sighted timestamp updated
  • The duplicate is NOT stored

Facts confirmed multiple times become more confident over time while the store stays clean.

# First mention
mm.ingest("user1", "I prefer dark mode", "Noted!")
# -> Stored as fact: "User prefers dark mode" (confidence: 0.8)

# Second mention (different phrasing)
mm.ingest("user1", "Can you enable dark mode? I always use it", "Done!")
# -> Recognized as same fact, sighting_count bumped, confidence: 0.85
# -> NO duplicate created

Confidence Threshold

MemorySystem.search() and the MemoryManager.recall(), MemoryManager.recall_structured(), and MemoryManager.search() wrappers support a confidence_floor parameter. If nothing scores above the threshold, the system returns an empty list instead of guessing.

# Normal search - returns best matches regardless of confidence
results = mm.recall("user1", "quantum physics experiments")
# -> Might return vaguely related results

# With confidence floor - only returns if confident
results = mm.recall("user1", "quantum physics experiments", confidence_floor=0.5)
# -> Returns [] if nothing truly matches

The floor is calculated as entry.confidence * search_relevance_score. Both the memory's inherent confidence AND its match quality must be high enough.


Timestamps and Recency

Every memory gets a timestamp at ingest time. Timestamps drive three distinct systems:

  1. Decay - older memories have their relevance score gradually reduced based on a configurable half-life (default: 7 days). This keeps signal high as stores grow.
  2. Tiering - hot (0-3 days), warm (3-14 days), cold (14+ days). Tier determines load behavior.
  3. Recency retrieval - recall_recent() returns memories sorted by time regardless of semantic relevance.

recall_recent() - Time-Based Retrieval

# Get all memories from the last 6 hours
recent = mem.recall_recent(hours=6)

# Filter by session
recent = mem.recall_recent(hours=12, session_id="session-abc")

# Filter by channel
recent = mem.recall_recent(hours=6, source_channel="discord:dev-ops")

# Filter by both session and channel
recent = mem.recall_recent(
    hours=12,
    session_id="session-abc",
    source_channel="discord:general"
)

The session_id and source_channel filters were added in v2.3.1.

When to Use Recency vs Semantic Search

Use search() (semantic) when:

  • You want the best answer to a question
  • You don't know when the relevant memory was created
  • You're doing cross-session recall of facts and decisions

Use recall_recent() (temporal) when:

  • You want a chronological view of what happened
  • You're summarizing recent activity for a user
  • You're building a "what did I do in this session" feature
  • You need events from a specific time window regardless of their semantic content
# Best answer to a question
mem.search("what database did we choose?", cross_session_recall="semantic")

# Chronological recap of recent work
mem.recall_recent(hours=24, session_id="my-session")

Cross-Session and Cross-Channel Recall

Every memory tracks where it came from: which session created it and which channel it arrived on. This lets you control how memories flow between contexts.

Cross-Session Recall

results = mem.search(
    "what's the API key format?",
    session_id="session-B",
    cross_session_recall="semantic"  # "all" | "semantic" | "none"
)
  • "all" - search everything regardless of session (default)
  • "semantic" - other sessions' memories only surface if they're facts/decisions, not episodic chatter
  • "none" - strict session isolation

Cross-Channel Recall

Memories record their source_channel at ingest time. You can filter by channel on both search and recency:

# Only search memories from a specific channel
results = mem.search("deployment plan", source_channel="discord:dev-ops")

# Recent memories from a specific channel
recent = mem.recall_recent(hours=6, source_channel="telegram:alerts")

# Recent memories from a specific session AND channel
recent = mem.recall_recent(hours=12, session_id="abc", source_channel="discord:general")

This is how an agent running on multiple platforms (Discord, Telegram, Slack, terminal) keeps channel context clean while still sharing important knowledge across all of them.

How It Works Together

Channel A (Discord #dev)     Channel B (Telegram DM)     Channel C (Slack #ops)
        |                            |                           |
        v                            v                           v
    ingest with                  ingest with                ingest with
    source_channel=              source_channel=            source_channel=
    "discord:dev"                "telegram:dm"              "slack:ops"
        |                            |                           |
        +------------+---------------+---------------------------+
                     |
              Shared Memory Store
                     |
        +------------+---------------+---------------------------+
        |                            |                           |
   search from any channel can find memories from any other channel
   (unless filtered by source_channel or cross_session_recall mode)

The default is open: all memories are searchable from everywhere. Use source_channel and cross_session_recall to narrow scope when you need isolation.


Context Packets

A context packet is a structured bundle of relevant memories, environment details, and task-specific context - designed to be injected into a sub-agent's prompt at spawn time.

The Problem Context Packets Solve

Cold-spawned sub-agents start with zero context. They don't know what decisions have been made, what tools are available, what failed last time, or what the current environment looks like. Without context, they make confident mistakes based on incomplete information.

Context packets solve this by pulling the relevant subset of memory into a structured block that goes directly into the sub-agent's system prompt.

Building a Context Packet

from parsica_memory import MemorySystem

mem = MemorySystem("./workspace")
mem.load()

# Build a packet for a specific task
packet = mem.build_context_packet(
    task="Deploy the auth service to staging",
    tags=["auth", "deployment", "staging"],
    environment={"venv": "venv-prod", "python": "3.11", "host": "staging.example.com"},
    instructions=["Do not modify the database schema", "Use the existing Dockerfile"],
    max_memories=10,
    max_tokens=3000,
    include_mistakes=True,   # Include known pitfalls from mistake memories
)

# Inject into sub-agent prompt
prompt = f"""
{packet.render()}

YOUR TASK: {task_description}
"""

Output Formats

# Markdown (default) - readable, good for most LLM prompts
text = packet.render()
text = packet.render(fmt="markdown")

# XML - structured, easier to parse programmatically
text = packet.render(fmt="xml")

# JSON - serialize/deserialize across process boundaries
text = packet.render(fmt="json")
data = packet.to_dict()
packet2 = ContextPacket.from_dict(data)

Multi-Query Packets

When a task spans multiple topics, use build_multi() to pull context from several angles:

packet = mem.build_context_packet_multi(
    task="Refactor the payment service",
    queries=["payment service architecture", "stripe integration", "database schema"],
    max_memories=15,
    max_tokens=4000,
)

Token Budget and Trimming

Packets automatically trim to fit within max_tokens. Known pitfalls (from mistake memories) are never trimmed - they have highest priority. Regular memories are trimmed from the tail (lowest relevance first).

print(f"Packet: ~{packet.estimated_tokens} tokens, {len(packet)} memories")

# Manually trim if needed
trimmed = packet.trim(max_tokens=2000)

Known Pitfalls

When include_mistakes=True (default), the packet surfaces relevant mistake memories as explicit warnings:

### Known Pitfalls
- Known Pitfall: Last time this was attempted, the migration ran twice due to missing lock.
  Correction: Check for existing lock file before running.

These come from memories ingested via mem.ingest_mistake() and are scored against the task query.

Cross-Process Context Sharing

Context packets serialize cleanly to JSON, making them useful for cross-process handoff:

# Process A: build and serialize
packet = mem.build_context_packet(task="analyze logs")
with open("context.json", "w") as f:
    json.dump(packet.to_dict(), f)

# Process B: load and use
from parsica_memory.context_packet import ContextPacket
with open("context.json") as f:
    packet = ContextPacket.from_dict(json.load(f))
prompt_injection = packet.render()

12-Layer Search Engine

Every query runs through 12 scoring layers:

  1. BM25+ TF-IDF - baseline relevance with delta floor
  2. Exact Phrase Bonus - verbatim matches score higher
  3. Field Boosting - tags, source, category weighted; enriched fields get 2-3x weight
  4. Rarity and Proper Noun Boost - rare terms and names surface; proper nouns 3x, quoted strings 3x, version numbers 2.5x, code identifiers 2.5x
  5. Positional Salience - intro/conclusion bias
  6. Semantic Expansion - PPMI co-occurrence query widening
  7. Intent Reranker - temporal, entity, howto detection
  8. Qualifier and Negation - "failed" != "successful"
  9. Clustering Boost - coherent result groups score higher
  10. Embedding Reranker - optional local embeddings (no API)
  11. Pseudo-Relevance Feedback - top results refine the query
  12. Anti-Adjacency Penalty - penalizes topic-match without answer-match; when the query asks a specific question ("what is", "who", "which"), results are checked for concrete identifiers (proper nouns, quoted strings, version numbers). Results that discuss the topic without providing a specific answer get a 0.6x penalty.

Memory Types

Type Decay Rate Importance Cross-Session Use Case
episodic Normal 1x Same session only General events, chat
semantic Normal 1x Yes - crosses sessions Facts, decisions
fact 10x slower High recall Yes Verified/decomposed knowledge
mistake 10x slower 2x Yes Never forget failures
preference 3x slower 1x Yes User/agent preferences
procedure 3x slower 1x Yes How-to knowledge

Memories are automatically classified at ingest time. No manual tagging needed.


Shared / Team Memory

Multiple agents can share a memory pool with role-based access control:

from parsica_memory import SharedPool, AgentRole, AgentConfig

pool = mem.enable_shared_pool(
    pool_dir="./shared",
    pool_name="project-alpha",
    agent_id="worker-1",
    role=AgentRole.WRITER
)
mem.shared_write("Research complete: competitor uses GraphQL", namespace="research")

Graph Intelligence

Automatic entity extraction and knowledge graph:

path = mem.entity_path("payment-service", "database", max_hops=3)
triples = mem.graph_search(subject="PostgreSQL", relation="used_by")

Tiered Storage

Tier Age Behavior
Hot 0-3 days Always loaded, fastest access
Warm 3-14 days Loaded on-demand
Cold 14+ days Requires include_cold=True

Input Gating

P0-P3 priority classification drops noise before it enters the store:

mem.ingest_with_gating("ok thanks", source="chat")  # dropped (P3 - noise)
mem.ingest_with_gating("Production outage: auth down", source="incident")  # stored (P0 - critical)

MCP Server

parsica-memory-mcp --workspace ./memory --agent-name my-agent

Works with Claude Desktop and any MCP-compatible client.


CLI

Parsica-Memory ships a CLI for workspace operations. It is intentionally narrow: the CLI is for bootstrapping, inspection, rebuilding, serving, and import/export. The day-to-day interface lives in the Python API (MemoryManager / MemorySystem).

Command Reference

python -m parsica_memory init

Interactive setup wizard (or headless with --headless).

python -m parsica_memory init --workspace ./memory --agent-name moro
python -m parsica_memory init --workspace ./memory --force
python -m parsica_memory init --workspace ./memory --headless   # non-interactive

What it does:

  • runs the interactive setup wizard (if not --headless)
  • creates the workspace if missing
  • writes initial parsica_config.json
  • creates .consolidated/ and .shared_pools/
  • smoke-tests MemorySystemV4

python -m parsica_memory status

Load the workspace, print live stats, and run health checks.

python -m parsica_memory status --workspace ./memory

What it shows:

  • total entries, hot/warm/cold tier counts
  • WAL pending count
  • enrichment count and lifetime enrichment cost
  • graph nodes/edges
  • cache size
  • workspace health checks

python -m parsica_memory rebuild-graph

Rebuild graph intelligence from loaded entries. Useful after imports, large migrations, or structural changes.

python -m parsica_memory rebuild-graph --workspace ./memory

python -m parsica_memory serve

Start the MCP server over stdio.

python -m parsica_memory serve --workspace ./memory --agent-name moro

python -m parsica_memory export

Export all memories to a JSON file.

python -m parsica_memory export --workspace ./memory --output ./backup.json

python -m parsica_memory import

Import memories from another system. Supports auto-detection or explicit source format.

# Auto-detect source format
python -m parsica_memory import --workspace ./memory --input ./export.json

# Explicit source (mem0, Memvid, MEMO, Zep, LangMem, OpenAI, or generic formats)
python -m parsica_memory import --workspace ./memory --input ./mem0_export.json --from mem0
python -m parsica_memory import --workspace ./memory --input ./zep_export.json --from zep
python -m parsica_memory import --workspace ./memory --input ./langmem.json --from langmem
python -m parsica_memory import --workspace ./memory --input ./openai_threads.json --from openai
python -m parsica_memory import --workspace ./memory --input ./data.csv --from csv
python -m parsica_memory import --workspace ./memory --input ./notes/ --from markdown

# Dry run - show what would be imported without writing
python -m parsica_memory import --workspace ./memory --input ./export.json --dry-run

# Replace instead of merge
python -m parsica_memory import --workspace ./memory --input ./export.json --replace

Supported --from values: auto, mem0, memvid, memo, zep, langmem, openai, json, jsonl, sqlite, markdown, csv.

CLI Workflows

Fresh workspace

python -m parsica_memory init --workspace ./memory --agent-name my-agent
python -m parsica_memory status --workspace ./memory

After importing or copying a store

python -m parsica_memory status --workspace ./memory
python -m parsica_memory rebuild-graph --workspace ./memory

Migrate from another system

python -m parsica_memory import --workspace ./memory --input ./old_store.json --from mem0 --dry-run
python -m parsica_memory import --workspace ./memory --input ./old_store.json --from mem0
python -m parsica_memory rebuild-graph --workspace ./memory

Serving the store to another runtime

python -m parsica_memory serve --workspace ./memory --agent-name shared-memory

CLI vs Python API

Use the CLI when you want to:

  • initialize a workspace
  • inspect health/stats
  • rebuild graph structures
  • expose the store over MCP
  • import from or export to another system

Use MemoryManager when you want:

  • per-user isolation
  • app-ready memory with minimal code
  • optional auto-enrichment

Use MemorySystem when you want:

  • low-level control over ingest/search/tiering/graph/maintenance
  • explicit session/channel/recency/shared-pool behavior

Full API Reference

MemorySystem

from parsica_memory import MemorySystem

mem = MemorySystem(
    workspace="./memory",          # Required - directory path
    agent_name="my-agent",         # Required - scopes the store
    half_life=7.0,                 # Decay half-life in days
    enricher=None,                 # LLM enrichment callable
    tiered_storage=True,           # Hot/warm/cold tiers
    graph_intelligence=True,       # Entity extraction + graph
    semantic_expansion=True,       # PPMI query expansion
)

# Lifecycle
mem.load()                         # Load from disk
mem.save()                         # Save to disk
mem.flush()                        # WAL to shards
mem.close()                        # Flush + release

# Ingestion
mem.ingest(content, source=..., session_id=..., channel_id=..., source_channel=...)
mem.ingest_fact(content, source=...)
mem.ingest_mistake(what_happened=..., correction=..., root_cause=..., severity=...)
mem.ingest_preference(content, source=...)
mem.ingest_procedure(content, source=...)
mem.ingest_file(path, category=...)
mem.ingest_url(url, depth=2)

# Search
mem.search(query, limit=10, cross_session_recall="semantic", explain=False, confidence_floor=0.0)
mem.recent(limit=20)
mem.recall_recent(hours=24, session_id=None, source_channel=None)

# Enrichment
mem.re_enrich(batch_size=50, overwrite=False, decompose=False, progress_fn=None)  # metadata only
mem.re_enrich(batch_size=50, overwrite=False, decompose=True, progress_fn=None)   # metadata + fact decomposition
mem.re_decompose(batch_size=50, overwrite=False, progress_fn=None)

# Graph
mem.graph_search(subject=..., relation=..., obj=...)
mem.entity_path(source, target, max_hops=3)

# Context packets
mem.build_context_packet(task=..., tags=None, environment=None, instructions=None,
                          max_memories=15, max_tokens=4000, include_mistakes=True)

# Maintenance
mem.compact()
mem.consolidate()
mem.reindex()
mem.forget(topic=..., before_date=...)
mem.stats()

ParallelEnricher

from parsica_memory.parallel_enricher import ParallelEnricher

enricher = ParallelEnricher(
    workspace="./memory",          # Required - workspace directory
    keys=[                         # List of {key, model} dicts
        {"key": "sk-ant-...", "model": "claude-haiku-4-5-20251001"},
        {"key": "sk-proj-...", "model": "gpt-4o-mini"},
    ],
    decompose=True,                # Also decompose into atomic facts (default: False)
    batch_size=50,                 # Entries per progress callback
    overwrite=False,               # Skip already-enriched entries
)

# Run enrichment (blocking, returns summary dict)
result = enricher.run(progress_fn=lambda done, total: print(f"{done}/{total}"))
# result = {"enriched": int, "workers": int, "errors": int, "elapsed_s": float}

Each key processes entries in round-robin sequence. decompose=True runs combined enrichment + decomposition in one LLM call per entry per worker.


Benchmarks

Measured on Mac Mini M4 (10-core, 32GB), Python 3.14, 9,896 live memories:

Metric Result
LOCOMO Accuracy 78.0%
doc2query R@1 88.5%
p50 Search Latency 21ms
Dependencies 0
Requires API Key No (enrichment optional)
Requires Vector DB No

For full benchmark methodology and comparisons, see our benchmarks page.


Architecture

Parsica-Memory is a local-first memory engine. It combines persistence, recall, enrichment, recency, graph intelligence, sharding, and team memory into one package.

High-Level Component Map

parsica-memory/
├── Core Engine
│   ├── MemorySystem / MemoryManager
│   ├── MemoryEntry
│   ├── WAL + versioning + recovery
│   └── maintenance / compaction / consolidation
│
├── Retrieval
│   ├── 12-layer BM25+ search
│   ├── cross-session recall
│   ├── source_channel filtering
│   ├── recency recall
│   └── query expansion / reranking
│
├── Intelligence Layers
│   ├── LLM enrichment
│   ├── fact decomposition
│   ├── entity extraction
│   ├── graph intelligence
│   ├── cooccurrence / clustering
│   └── confidence / calibration
│
├── Storage System
│   ├── shard manager
│   ├── hot / warm / cold tiers
│   ├── audit + provenance
│   └── namespace support
│
└── Multi-Agent / Ops
    ├── SharedPool / team memory
    ├── context packets
    ├── CLI (init, status, export, import, serve)
    ├── MCP server
    └── instrumentation / health

Memory Lifecycle

Input
  |
  v
Gating / classification
  |
  v
MemoryEntry creation
  |- session_id
  |- source_channel
  |- source / category / type
  +- provenance metadata
  |
  v
Optional LLM enrichment
  |- tags
  |- summary
  |- keywords
  |- search_queries
  +- facts
  |
  v
WAL append
  |
  v
Shard persistence
  |
  v
Indexes / graph / derived layers
  |
  v
Available for:
  |- semantic search
  |- recency recall
  |- cross-session recall
  |- context packets
  |- shared pools
  +- export / serving

Session IDs, Channel IDs, and Recency

Parsica-Memory tracks where a memory came from, not just what it says.

  • session_id identifies the conversation/runtime session
  • source_channel identifies the surface/channel (Discord, Telegram, DM, thread, etc.)
  • cross_session_recall controls whether other sessions can surface facts or only local memories
  • recall_recent() provides a recency-first retrieval path independent of semantic scoring

Timestamps are set automatically at ingest time and are used for:

  • decay scoring (half-life applied to relevance scores)
  • tier assignment (hot/warm/cold based on age)
  • recency retrieval (sorted by created_at descending)
  • session reconstruction (ordering events within a session)

Sharding and Tiering

Parsica-Memory shards memory for scale and retrieval efficiency, and classifies shards into temperature tiers:

Tier Age Behavior
Hot newest eagerly loaded, fastest recall
Warm medium age available on demand
Cold oldest optional / lazy / archival-friendly

This gives you faster search over large stores, easier archival/backup, better operational safety, and lower memory overhead for long-lived agents.

Retroactive Enrichment

If you migrated an old store, copied shards, or started without enrichment, you can backfill later:

mm.enrich_all("user123")

# or low-level
mem.re_enrich(batch_size=50, overwrite=False)
mem.re_decompose(batch_size=50, overwrite=False)

Enrichment is best from day one, but not locked to day one.


Changelog

v2.6.0 (2026-03-18)

  • Setup wizard with guided mode: Manager vs System choice, import flow (6 source options), enrichment setup with key detection, model selection with cost/quality tags, retroactive enrichment with time estimates and multi-key option, background processing with progress display
  • re_enrich(decompose=True) - combined enrichment + fact decomposition in one pass (cheaper than two separate calls)
  • Multi-key enrichment via ParallelEnricher - distributes work across API keys/models in round-robin
  • Import CLI with converters for mem0, Memvid, MEMO, Zep, LangMem, OpenAI threads export, plus generic json, jsonl, sqlite, markdown, csv
  • Export CLI command (python -m parsica_memory export)
  • Background enrichment with live progress display
  • Rate limit handling with backoff across all enrichment paths

v3.1.0 (2026-03-26) — Quality Levers + Transparency

  • Scoring transparency — every search result now includes a per-component score breakdown (final_score, phrase_boost, tag_boost, category_boost, enriched_boost, doc2query_boost, structured_boost, anti_adjacency). All values are numeric multipliers, not booleans.
  • parsica explain <query> CLI — run a search and see every scoring decision with provenance (source, timestamp, session, channel).
  • parsica enrich CLI — batch enrichment with --keys key1,key2,key3 for multi-key round-robin across providers. --coverage shows enrichment percentage.
  • /search <query> in OpenClaw — search results with provenance (source, timestamp, channel, session, score breakdown) formatted for Discord/chat.
  • /enrichment on|off|status — toggle enrichment live from chat without editing config. Enrichment is on by default.
  • /decompose on|off|status — toggle fact decomposition live from chat. Decomposition is off by default.
  • LEA (Lexical Entity Amplification) — entity detection with position bug fix (fallback position=-1 prevents false entity boosting).
  • Heuristic gating — P0-P3 input classification filters noise at ingest time (+33 pts R@1 at zero API cost).
  • Toggle wiring/enrichment and /decompose toggles now correctly gate ingest behavior in real-time.
  • re_enrich() ungated — batch re-enrichment always works regardless of live toggle state.
  • PDF page_number fix — page 0 (zero-indexed) no longer silently drops its tag.
  • README corrections — hook name unified to before_agent_start, enrichment language corrected from "parallel" to "round-robin".
  • 1307 tests passing — 17 new v3.1 tests + decomposition test compatibility.

v2.3.1 (2026-03-17)

  • Native channel/session/recency support - search() now supports source_channel filtering; recall_recent() supports both session_id and source_channel filters.
  • Provider-agnostic enrichment - smart_enricher() auto-detects Anthropic, OpenAI, Google, or OpenAI-compatible providers from key prefix. auto_enricher() now supports PARSICA_LLM_API_KEY as a provider-agnostic entry point (legacy ANTARIS_LLM_API_KEY still works).
  • Bootstrap / health checks - startup/bootstrap integrity checks retained and documented more clearly.

v2.1.2 and earlier

See CHANGELOG.md for earlier release notes.


Relationship to the Antaris ecosystem

parsica-memory is the standalone memory package from Antaris Analytics. It also powers broader Antaris agent infrastructure, but it is documented and shipped here as an independent package with its own install surface.


License

Apache 2.0


Links


Built by Antaris Analytics LLC. Zero dependencies. Ships complete.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

parsica_memory-3.3.3.tar.gz (2.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

parsica_memory-3.3.3-py3-none-any.whl (1.8 MB view details)

Uploaded Python 3

File details

Details for the file parsica_memory-3.3.3.tar.gz.

File metadata

  • Download URL: parsica_memory-3.3.3.tar.gz
  • Upload date:
  • Size: 2.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for parsica_memory-3.3.3.tar.gz
Algorithm Hash digest
SHA256 cc9127cb21867f2925f7c22cdefd8d65152a2e6657109bdbe39aa3edcaf5759c
MD5 68450a2f302ae11122cd216a60dac598
BLAKE2b-256 38733905017e5a91668521742122963068c3d1d8f9691d05a16b89e0f0997231

See more details on using hashes here.

File details

Details for the file parsica_memory-3.3.3-py3-none-any.whl.

File metadata

  • Download URL: parsica_memory-3.3.3-py3-none-any.whl
  • Upload date:
  • Size: 1.8 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for parsica_memory-3.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 da048b1f32a1a8ba74663fecd5a21f44d0bf46cf2803683b6f65c564ec3896f1
MD5 7f0d15de25ec6f9918aea25116b577d1
BLAKE2b-256 50411e2553062f4b9ddd2cd2159cfeef285820a12c72291677525b194fd4235f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page