Skip to content

Memory Manager API reference

MemoryManager is the main Python interface to ZettelForge memory. Use it to store notes, retrieve context, inspect relationships, and synthesize answers. This page gives you the public signatures and the behavior observed in ZettelForge 2.8.0.

Prerequisites

Install ZettelForge 2.8.0 on Python 3.10 or later. Review the configuration reference before you enable LLM-backed enrichment or synthesis.

The signatures below come from the official v2.8.0 source tag. The runtime examples were exercised against a fresh, isolated SQLite and LanceDB store.

Get an instance

from zettelforge import MemoryManager

mm = MemoryManager()
MemoryManager(jsonl_path: str | None = None, lance_path: str | None = None)
  • jsonl_path defaults to None. This compatibility path selects the data directory through its parent and is passed to the vector-store adapter.
  • lance_path defaults to None and overrides the LanceDB index path.

Call get_memory_manager() when you want the process-wide singleton instead of an explicit instance. Construction initializes SQLite, LanceDB access, and the background enrichment worker.

Background enrichment dispatch is enabled by default. Set ZETTELFORGE_ENRICHMENT_ENABLED=false before import when you need a deterministic offline write path. Disabling dispatch does not disable regex entity extraction or direct storage.

Write API

remember

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

Creates one memory note. The direct path constructs an embedding, writes the note, updates entity mappings and graph edges, and returns (note, status). status is "created", "updated", "corrected", or "noop".

When background enrichment is enabled, sync=False queues causal extraction and sync=True runs it inline. Set evolve=True to run fact extraction and the ADD / UPDATE / DELETE / NOOP decision path before direct storage.

note, status = mm.remember(
    "APT28 used Mimikatz against the target network.",
    source_type="threat_report",
    source_ref="sample-report",
    domain="cti",
)

The ZettelForge 2.8.0 public signature does not accept per-call TLP arguments.

remember_chunked

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

Splits long content on sentence boundaries and stores each chunk through remember(). Content at or below chunk_size becomes one note. Longer input gets source references such as report#c0, report#c1, and report#c2.

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,
) -> list[tuple[MemoryNote | None, str]]

Uses an LLM to extract scored facts, then compares each retained fact with existing notes and applies an operation. Facts below min_importance are discarded, and max_facts limits extraction. Each result is (note, status); the status is "added", "updated", "corrected", or "noop".

The method rejects explicit prompt-injection patterns in content and context with PromptInjectionError before it calls the model.

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]]

Chunks a report and sends each chunk through remember_with_extraction(). When present, published_date is added to the extraction context as Published: <date>. The source reference for each chunk uses the form <source_url>:chunk:<index>.

LLM prerequisite

remember_with_extraction() and remember_report() require a reachable, configured LLM for fact output. With a deliberately unreachable provider, both returned an empty list. remember(evolve=True) fell through to direct storage and returned "created" when extraction produced no facts.

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,
) -> list[MemoryNote]

Retrieves notes with blended vector, entity, and graph search. The intent classifier selects retrieval weights. When cross-encoder reranking is enabled, ZettelForge reranks only the configured head of the candidate list and keeps the tail in blended order.

caller records telemetry attribution. actor is its compatibility alias; caller wins when both are supplied. Superseded and expired notes are excluded by default.

If governance.limits.recall_timeout_seconds is greater than zero, a timeout logs a warning and returns an empty list. Explicit prompt-injection patterns in query raise PromptInjectionError before retrieval.

The ZettelForge 2.8.0 public signature has no per-call TLP filter or override.

Direct entity lookups

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]

These methods bypass the blended scoring pipeline and read entity mappings directly.

  • recall_cve() normalizes the identifier to uppercase.
  • recall_actor() checks actor, threat_actor, and intrusion_set, then removes duplicate notes.
  • recall_technique() checks the attack_pattern type.
  • recall_tool() normalizes the tool name to lowercase.

The built-in entity index tracks activity, actor, attack_pattern, campaign, cve, domain, email, event, intrusion_set, ipv4, location, md5, organization, person, sha1, sha256, temporal, tool, and url. External loaders can add other type strings because recall_entity() does not validate the type.

Direct lookup resolves only mappings already in the index. Alias enrichment depends on the LLM enrichment path.

get_context

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

Returns formatted memory context for an agent prompt. Explicit prompt-injection patterns in query raise PromptInjectionError.

Statistics and lifecycle

get_stats

get_stats() -> dict

Returns the current counters plus store and entity-index totals. ZettelForge 2.8.0 returns these top-level keys:

  • notes_created, retrievals, entity_index_hits, and consolidations_triggered
  • llm_ner_success, llm_ner_failure, llm_ner_no_new, llm_ner_total_new_entities, and llm_ner_total_duration_ms
  • total_notes and entity_index

These are process counters, not durable historical metrics.

flush

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

Waits for queued and in-flight enrichment jobs. It returns True when all jobs finish. It returns False when a supplied timeout expires first.

evolve_note

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

Runs neighbor evolution for an existing note. With sync=True, it returns the evolution report. With sync=False, it queues the job and returns None. A missing note_id also returns None and logs a warning.

mark_note_superseded

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

Links the old and new notes, removes the old note from entity mappings, and writes a SUPERSEDES temporal edge. It returns False when either identifier does not resolve.

snapshot

snapshot() -> str

Creates a storage snapshot under the configured data directory.

In progress in ZettelForge 2.8.0

The observed SQLite snapshot was created as snapshots/zettelforge_<timestamp>.db, but snapshot() returned a snapshots/notes_<timestamp>.jsonl path that did not exist. Inspect the snapshots directory for the .db file instead of trusting the returned path. The 2.8.0 signature accepts no arguments.

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

Writes a relationship without creating a note. The SQLite edge identity uses the source node, target node, relationship, and note identifier. Repeating the same direct relationship produced one matching edge in the isolated store.

provenance_chain

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

Returns causal steps from the canonical entity. direction="forward" follows outgoing edges; direction="backward" follows incoming edges. Any other value raises ValueError.

Each step has this shape:

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

get_entity_relationships

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

Alias-resolves the entity value and returns its direct graph neighbors. Each observed result contained node, relationship, edge_properties, and note_id.

traverse_graph

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

Traverses outward from the canonical start entity. ZettelForge clamps requests above depth 2 to depth 2 and logs the clamp.

Return annotation mismatch

The source annotation says list[dict], but the observed value was a list of paths. Each path was a list of step dictionaries with from_type, from_value, relationship, to_type, and to_value. Treat the runtime value as list[list[dict]] until the implementation or annotation is fixed.

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,
) -> dict[str, Any]

Retrieves notes and asks the configured LLM for an answer. k limits the retrieved notes. A missing tier_filter becomes ['A', 'B', 'C']. caller records telemetry attribution, and actor is its compatibility alias.

The verified format is "direct_answer". Requests for "synthesized_brief", "timeline_analysis", or "relationship_map" are logged and fall back to "direct_answer". No current ThreatRecall.ai public synthesis surface was verified for the other formats, so they remain in progress rather than documented as available SaaS behavior.

Explicit prompt-injection patterns in query or retrieved context raise PromptInjectionError. The public signature has no per-call TLP filter or override.

The returned dictionary has this shape:

{
    "query": ...,
    "format": "direct_answer",
    "synthesis": {"answer": ..., "confidence": ..., "sources": [...]},
    "metadata": {
        "query_id": ...,
        "model_used": ...,
        "tokens_used": ...,
        "latency_ms": ...,
        "confidence_threshold": ...,
        "sources_count": ...,
        "tier_filter": ...,
    },
    "sources": [
        {
            "note_id": ...,
            "relevance_score": ...,
            "quote": ...,
            "tier": ...,
        }
    ],
}

With a deliberately unreachable provider, the method still returned the full wrapper, a direct_answer fallback with confidence 0.0, and the retrieved source records. It preserved the wrapper shape, but validation returned False because confidence was below the configured threshold.

validate_synthesis

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

Returns (is_valid, errors) after checking a synthesis response.

check_synthesis_quality

check_synthesis_quality(response: dict) -> dict

Returns a score from 0 to 1, a quality label, and component scores. The no-provider fallback scored 0.48 and NEEDS_WORK in the isolated run.

Feature state

This page remains in_progress for three precise reasons:

  1. A real LLM response was not observed, so extracted facts, alias enrichment, causal extraction, and generated synthesis content remain unverified.
  2. snapshot() creates a SQLite file but returns a nonexistent JSONL path.
  3. traverse_graph() returns nested path lists while its public annotation says list[dict].

All public signatures, direct write and recall paths, entity lookup, graph ingest and traversal, prompt-injection rejection, no-provider fallbacks, synthesis validation, statistics, and flush() were verified on 2026-08-16 against the official ZettelForge 2.8.0 source tag.