Skip to content

Memory Manager API reference

MemoryManager is the main programmatic interface to ZettelForge memory. It owns note creation, recall, the knowledge graph, and synthesis. This page documents the public methods, their verified signatures, and return shapes.

Signatures on this page are taken from source (src/zettelforge/memory_manager.py, ZettelForge 2.7.0). Return-value examples that depend on a live LLM are marked; see feature state.

Getting an instance

from zettelforge.memory_manager import MemoryManager, get_memory_manager

# Explicit instance (tests, custom data paths)
mm = MemoryManager()

# Process-wide singleton (the usual path)
mm = get_memory_manager()

MemoryManager() initializes the SQLite storage backend, the LanceDB vector index, and a background enrichment worker thread. The worker drains on process exit, so short-lived scripts call flush() before they return when they need LLM enrichment to finish.

MemoryManager(jsonl_path: str | None = None, lance_path: str | None = None)
Argument Default Purpose
jsonl_path None Override the JSONL data path. The data directory is derived from its parent.
lance_path None Override the LanceDB index path.

Write API

remember

remember(
    content: str,
    source_type: str = "conversation",
    source_ref: str = "",
    domain: str = "general",
    evolve: bool = False,
    sync: bool = False,
    tlp: str | None = None,
    actor: str | None = None,
) -> tuple[MemoryNote, str]

Create one memory note from content. Uses a dual-stream write path: the fast path (embedding, storage, vector index, entity index, heuristic graph edges) returns quickly, and LLM causal-triple extraction is deferred to the background worker.

Set evolve=True to run the two-phase fact pipeline (extract facts, then decide ADD / UPDATE / DELETE / NOOP against existing notes). Set sync=True to run causal extraction inline instead of in the background.

Returns (note, status) where status is one of "created", "updated", "corrected", or "noop".

note, status = mm.remember(
    "APT28 used X-Agent against the target network.",
    source_type="threat_report",
    domain="cti",
    tlp="amber",
)

remember_chunked

remember_chunked(
    content: str,
    source_type: str = "conversation",
    source_ref: str = "",
    domain: str = "general",
    chunk_size: int = 800,
    sync: bool = False,
    tlp: str | None = None,
) -> list[MemoryNote]

Split long content on sentence boundaries and store each chunk as its own note. Content at or under chunk_size is stored as a single note. Chunked notes carry an ordinal "{source_ref}#c{i}" suffix so provenance survives the split.

remember_with_extraction

remember_with_extraction(
    content: str,
    source_type: str = "conversation",
    source_ref: str = "",
    domain: str = "general",
    context: str = "",
    min_importance: int = 3,
    max_facts: int = 5,
    tlp: str | None = None,
) -> list[tuple[MemoryNote | None, str]]

Two-phase pipeline: an LLM distills content into scored candidate facts, then each fact is compared to existing notes and an operation is chosen. Facts below min_importance are skipped; at most max_facts are extracted. Returns a list of (MemoryNote | None, status) tuples, where status is one of "added", "updated", "corrected", "noop".

remember_report

remember_report(
    content: str,
    source_url: str = "",
    published_date: str = "",
    domain: str = "cti",
    min_importance: int = 3,
    max_facts: int = 10,
    chunk_size: int = 3000,
) -> list[tuple[MemoryNote | None, str]]

Ingest a news or threat report. Chunks long content, runs two-phase extraction on each chunk, and stores published_date (ISO 8601) as temporal metadata. Returns the combined list of (MemoryNote | None, status) tuples across all chunks.

LLM prerequisite

remember_report and remember_with_extraction depend on a configured LLM provider for their extraction phase. With no LLM available they return zero results. Configure llm in config.yaml (see the configuration reference) before using report ingestion.

Recall API

recall

recall(
    query: str,
    domain: str | None = None,
    k: int = 10,
    include_links: bool = True,
    exclude_superseded: bool = True,
    include_expired: bool = False,
    caller: str | None = None,
    actor: str | None = None,
    max_tlp: str | None = None,
    tlp_override_reason: str | None = None,
) -> list[MemoryNote]

Retrieve memories relevant to query using blended vector and graph retrieval with cross-encoder reranking. An intent classifier sets the strategy weights per query.

If governance.limits.recall_timeout_seconds is greater than zero, the retrieval pipeline is bounded by that wall-clock timeout; exceeding it logs a warning and returns an empty list. This is a defense-in-depth control against deep graph traversal denial of service.

Entity lookups

These bypass the scoring pipeline for a direct index lookup.

recall_entity(entity_type: str, entity_value: str, k: int = 5) -> list[MemoryNote]
recall_cve(cve_id: str, k: int = 5) -> list[MemoryNote]
recall_actor(actor_name: str, k: int = 5) -> list[MemoryNote]
recall_technique(technique_id: str, k: int = 25) -> list[MemoryNote]
recall_tool(tool_name: str, k: int = 5) -> list[MemoryNote]

recall_entity accepts these entity_type values: cve, actor, threat_actor, intrusion_set, tool, campaign, person, location, organization, event, activity, temporal.

  • recall_cve normalizes the id to upper case.
  • recall_actor searches actor, threat_actor, and intrusion_set and deduplicates. APT/UNC/FIN-style designations are usually stored as intrusion_set; older stores may still use actor.
  • recall_technique looks up MITRE ATT&CK technique ids (for example T1059) under the attack_pattern entity type.

Alias resolution needs the LLM enrichment path

Direct entity lookups match canonical values already in the index. Resolving an alias to its canonical actor (for example "Fancy Bear" to APT28) depends on the LLM enrichment path; without it, only exact stored values return notes.

get_context

get_context(query: str, domain: str | None = None, k: int = 10, token_budget: int = 4000) -> str

Return a formatted memory context string suitable for injecting into an agent prompt. token_budget caps the size of the returned context.

Statistics and lifecycle

get_stats

get_stats() -> dict

Return memory-system statistics. The dict includes the running counters notes_created, retrievals, entity_index_hits, consolidations_triggered, the llm_ner_* observability counters, enrichment_failures, and evolution_failures, plus total_notes, entity_index (the entity-index sub-stats), and enrichment_degraded.

enrichment_degraded flips to True after the first background enrichment failure. Watch it: an unset LLM API key makes every background enrichment call fail silently, which leaves causal extraction, NER, and evolution inactive while remember() still reports success.

flush

flush(timeout: float | None = None) -> bool

Block until queued and in-flight enrichment jobs complete. Returns True when all jobs are done, or False if timeout is supplied and expires first. Call this in short-lived scripts that need background enrichment to finish before exit.

evolve_note

evolve_note(note_id: str, sync: bool = False) -> dict | None

Trigger neighbor evolution around an existing note (manual or MCP invocation). Returns an evolution report dict when sync=True; returns None when the job is queued to the background worker or when note_id is not found.

mark_note_superseded

mark_note_superseded(note_id: str, superseded_by_id: str) -> bool

Mark one note as superseded by another and link them. Returns False if either id does not resolve to a stored note.

snapshot

snapshot(actor: str | None = None, tlp_override_reason: str | None = None) -> str

Export a JSONL snapshot of memory and return its path. If the snapshot contains AMBER or RED notes, the export raises TlpSharingError unless you supply an audited tlp_override_reason, which is recorded in the audit log.

Knowledge graph API

ingest_relationship

ingest_relationship(
    from_type: str,
    from_value: str,
    to_type: str,
    to_value: str,
    relationship: str,
    properties: dict | None = None,
) -> None

Write a STIX relationship edge directly into the knowledge graph without creating a memory note. Intended for sync clients. The edge is deduplicated by (from_type, from_value, to_type, to_value, relationship); re-ingesting the same triple is idempotent.

provenance_chain

provenance_chain(
    entity_type: str,
    entity_value: str,
    max_depth: int = 3,
    direction: str = "forward",
) -> list[dict]

Trace a causal provenance chain from an entity. direction="forward" follows outgoing causal edges (what does X cause or enable?); direction="backward" follows incoming causal edges (why did X happen?). Any other value raises ValueError.

Returns a list of steps, each shaped:

{"from_entity": ..., "relationship": ..., "to_entity": ..., "edge_type": ..., "note_id": ...}

get_entity_relationships

get_entity_relationships(entity_type: str, entity_value: str) -> list[dict]

Return the direct knowledge-graph neighbors of an entity. The entity value is alias-resolved to its canonical form before lookup.

traverse_graph

traverse_graph(start_type: str, start_value: str, max_depth: int = 2) -> list[dict]

Traverse relationships outward from a starting entity. In the open-source distribution, max_depth is capped at 2; requesting a larger depth is clamped to 2 and logged. Deeper multi-hop graph traversal is available in ThreatRecall.ai, the hosted SaaS.

Synthesis API

synthesize

synthesize(
    query: str,
    format: str = "direct_answer",
    k: int = 10,
    tier_filter: list[str] | None = None,
    caller: str | None = None,
    actor: str | None = None,
    max_tlp: str | None = None,
    tlp_override_reason: str | None = None,
) -> dict[str, Any]

Synthesize an answer from retrieved memories (retrieval-augmented answer). k sets how many notes are pulled for context; tier_filter restricts by epistemic tier (for example ["A", "B"]).

format="direct_answer" is the open-source format. The extended formats "synthesized_brief", "timeline_analysis", and "relationship_map" are ThreatRecall.ai SaaS capabilities; when requested on the open-source distribution they fall back to "direct_answer" and the fallback is logged.

The return dict carries the synthesis result, its metadata, and the source notes:

{
  "synthesis": {"answer": ..., "confidence": ..., "sources": [...]},
  "metadata": {
    "query_id": ..., "model_used": ..., "tokens_used": ..., "latency_ms": ...,
    "confidence_threshold": ..., "sources_count": ..., "tier_filter": ...,
    "max_tlp": ..., "tlp_filter": ..., "tlp_override": ...,
  },
}

LLM prerequisite

synthesize needs a configured LLM provider. With none available it returns a well-formed dict whose answer is "No specific answer found" and whose confidence is 0.0.

validate_synthesis

validate_synthesis(response: dict) -> tuple[bool, list[str]]

Validate a synthesis response for quality. Returns (is_valid, errors).

check_synthesis_quality

check_synthesis_quality(response: dict) -> dict

Compute a quality score for a synthesis response. Returns quality metrics including a score from 0 to 1 and a letter grade.

Feature state

This page is marked in_progress. Every method signature and return shape here is verified against ZettelForge 2.7.0 source. The runnable examples that depend on a live LLM (report ingestion, alias resolution, synthesize) are documented from source behavior and their offline fallbacks; exact representative output captured from a running instance with an LLM provider configured is pending a follow-up run.