Memory consolidation

Available on NAMS: No. The procedure on this page needs the Bolt backend. With the hosted NAMS backend its operations are unavailable: depending on the call, the client raises NotSupportedError, AttributeError or TypeError, or ignores a Bolt-only setting or argument (NAMS manages embedding and extraction server-side). See the backend capabilities reference for what NAMS provides instead.

How to keep your memory graph clean over time. The library ships four dry-runnable consolidation primitives — entity dedupe, long-trace flagging, preference supersede detection, and conversation TTL archival.

All primitives default to dry_run=True: they identify candidates without mutating the graph. Pass dry_run=False to apply changes, which writes a :ConsolidationRun audit node when candidates are applied so you can track when each job last ran.

Prerequisites

  • A connected Bolt client and configured MemorySettings; run the snippets inside an async function.

  • A representative test graph with embeddings and the relevant vector indexes.

  • A scheduler owned by your application; the library does not start periodic jobs.

In 0.7.0, do not apply detect_superseded_preferences to a database that holds more than one user’s preferences. Even with user_identifier, its candidate query scopes the old preference but does not enforce the same owner on the proposed replacement. Review candidates and use explicit, owner-checked superseding in Store and revise a user preference instead.

Goal

Preview each hygiene job before scheduling mutations:

from neo4j_agent_memory import MemoryClient

async with MemoryClient(settings) as client:
    # 1. Find near-duplicate entities above 0.95 similarity.
    report = await client.consolidation.dedupe_entities(
        similarity_threshold=0.95, dry_run=True
    )
    print("Candidate pairs:", report.candidate_count)

    # 2. Flag long traces for out-of-band summarization.
    await client.consolidation.summarize_long_traces(
        min_steps=20, dry_run=True
    )

    # 3. Inspect preference candidates on a single-user test graph only.
    await client.consolidation.detect_superseded_preferences(
        dry_run=True
    )

    # 4. Archive conversations older than 90 days.
    await client.consolidation.archive_expired_conversations(
        ttl_days=90, dry_run=True
    )

Steps

1. Always dry-run first

Every primitive returns a ConsolidationReport with a candidates list. Inspect it before applying.

report = await client.consolidation.dedupe_entities(dry_run=True)
for c in report.candidates[:10]:
    print(c.description)
print(f"... {report.candidate_count} total candidates.")

2. Apply with dry_run=False

report = await client.consolidation.dedupe_entities(
    similarity_threshold=0.95, dry_run=False
)
print(f"Run id: {report.run_id}")
print(f"Applied: {report.actions_taken}")

When mutations occur, the library writes a (:ConsolidationRun {id, kind, started_at, completed_at, stats_json}) node. Inspect the most recent run for a given kind. This is an audit record, not a transactional checkpoint or an automatic scheduler:

MATCH (cr:ConsolidationRun {kind: 'dedupe_entities'})
RETURN cr.id, cr.completed_at, cr.stats_json
ORDER BY cr.completed_at DESC LIMIT 1

3. Idempotent re-runs

Each primitive skips work it has already done — for example, summarize_long_traces only flags traces where summarization_pending is not already true. Re-running on a clean graph is a no-op.

4. Verify the applied state

Compare report.actions_taken with the candidates you reviewed. When changes were applied, query report.run_id in ConsolidationRun and inspect the resulting relationship or marker. A run with no candidates has no run ID. Repeat the dry run to confirm those candidates no longer appear. Each operation can make multiple writes; a mid-run failure is not an atomic rollback.

Primitives reference

Primitive What it does

dedupe_entities

Find entity pairs with embedding similarity above the threshold (defaulting to 0.95) that aren’t already linked via :SAME_AS. Mutation writes [:SAME_AS {status: 'auto_consolidated'}]; it does not merge node properties or delete duplicate nodes.

summarize_long_traces

Find :ReasoningTrace nodes with >= min_steps steps that haven’t been summarized. Mutation sets summarization_pending = true so an out-of-band summarizer can pick them up; the library does not summarize traces itself (that’s prompt + LLM choice).

detect_superseded_preferences

Find pairs of preferences in the same category with high embedding similarity but different text — likely supersedes. Mutation writes [:SUPERSEDED_BY] and sets valid_until. Apply only on a verified single-user graph because replacement ownership is not enforced by this query.

archive_expired_conversations

Mark :Conversation nodes whose creation time is older than ttl_days as archived (falling back to update time when creation time is missing). Sets archived = true and archived_at; does not delete data. Pass ttl_days explicitly, for example from MemorySettings.memory.conversation_ttl_days; no scheduler or automatic retrieval filter is installed.