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_pathdefaults toNone. This compatibility path selects the data directory through its parent and is passed to the vector-store adapter.lance_pathdefaults toNoneand 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()checksactor,threat_actor, andintrusion_set, then removes duplicate notes.recall_technique()checks theattack_patterntype.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, andconsolidations_triggeredllm_ner_success,llm_ner_failure,llm_ner_no_new,llm_ner_total_new_entities, andllm_ner_total_duration_mstotal_notesandentity_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:
- A real LLM response was not observed, so extracted facts, alias enrichment, causal extraction, and generated synthesis content remain unverified.
snapshot()creates a SQLite file but returns a nonexistent JSONL path.traverse_graph()returns nested path lists while its public annotation sayslist[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.