Docs

Memory Curation Cycle

Sulcus runs an autonomous background curator that continuously cleans, consolidates, and optimizes your agent's memory. It reclassifies stale nodes, merges near-duplicates via LLM, summarizes verbose entries, re-vectorizes content, marks stale confidence, and syncs everything to the knowledge graph. Zero human maintenance required. The curator never deletes — it archives.

Sulcus Server v2.9 · Curator Reference · 2026

Overview

Autonomous memory hygiene that runs in the background

Agent memory accumulates fast. Without maintenance, you get duplicates, stale entries, unclassified nodes, and bloated summaries that waste tokens and degrade recall quality. Most memory systems leave this mess for humans to clean up.

Sulcus solves this with the Curator — a background process that runs a multi-step curation cycle over every active namespace. It uses LLM-powered consolidation (via GPT-5.4-nano) to intelligently merge near-duplicates and condense verbose entries, while generating training signals that feed back into the SIU pipeline for continuous model improvement.

Deploy Sulcus. Your memory gets cleaner every 30 minutes. No cron jobs, no manual intervention, no data loss.

30 min

Default cycle interval

0.92

Cosine similarity merge threshold

0 deletes

Archive-only policy

GPT-5.4-nano

LLM consolidation engine

When It Runs

Two triggers: timed interval and namespace idle detection

The curator spawns as a background Tokio task when the server starts. After a 15-second stagger delay (to avoid thundering herd with other startup tasks), it enters an interval loop.

Timed Interval

Every CURATOR_INTERVAL_SECS seconds (default: 1800 = 30 minutes), the curator runs a full pass over all tenants and their active namespaces.

Idle Detection

Namespaces with no interactions for 10 minutes (IDLE_THRESHOLD_SECS = 600) are prioritized for curation. The curator cleans up while your agents are quiet.

// From crates/sulcus-server/src/curator.rs
pub fn spawn(pool: PgPool) {
    let interval_secs = std::env::var("CURATOR_INTERVAL_SECS")
        .ok()
        .and_then(|s| s.parse::<u64>().ok())
        .unwrap_or(1_800); // 30 minutes default

    tokio::spawn(async move {
        // Stagger startup: 15s delay avoids thundering herd
        tokio::time::sleep(Duration::from_secs(15)).await;
        let mut interval = tokio::time::interval(
            Duration::from_secs(interval_secs)
        );
        interval.tick().await; // skip immediate tick

        loop {
            interval.tick().await;
            run_curation_cycle(&pool, extraction_config).await;
        }
    });
}
rust

The 7 Steps

Each curation cycle runs these steps per tenant/namespace

The curator iterates over every active tenant, then every namespace within that tenant. For each namespace, it executes seven steps in order. Each step is independent and fault-tolerant — if one fails, the rest still run.

async fn curate_namespace(pool, tenant_id, namespace, config) {
    // Step 1: Re-classify stale unrecalled nodes
    stats.reclassified += step_reclassify(pool, tenant_id, namespace);

    // Step 2: Consolidate near-duplicates (archive, never delete)
    stats.consolidated += step_consolidate_duplicates(
        pool, tenant_id, namespace, config
    );

    // Step 3: Summarize verbose nodes
    stats.summarized += step_summarize_verbose(
        pool, tenant_id, namespace, config
    );

    // Step 4: Re-vectorize nodes with missing embeddings
    stats.revectorized += step_revectorize(pool, tenant_id, namespace);

    // Step 5: Mark stale confidence (not recalled in 30+ days)
    stats.stale_marked += step_mark_stale_confidence(
        pool, tenant_id, namespace
    );

    // Step 6: Sync modified nodes to AGE graph
    step_sync_age_graph(pool, tenant_id, namespace);

    // Step 7: Log curation activity
    log_curation_activity(pool, tenant_id, namespace, &stats);
}
rust
1
Re-classifyFlag stale unrecalled nodes for SIU reclassification
reclassify_pending
2
ConsolidateMerge near-duplicates via LLM (cosine > 0.92)
archive
3
SummarizeCondense verbose entries (>500 chars, low recall)
summarize
4
Re-vectorizeGenerate embeddings for nodes missing vectors
backfill
5
Stale ConfidenceMark nodes not recalled in 30+ days as stale
stale
6
AGE Graph SyncSync all modified nodes to Apache AGE knowledge graph
sync
7
Activity LogRecord curation stats for observability
log

Step 1: Re-classify Stale Nodes

Ensure old unrecalled memories get properly typed

Nodes with recall_count = 0 that are lagging more than 100 epochsbehind the namespace's current interaction epoch get flagged for SIU reclassification.

These are memories that were stored but never recalled — they may have been classified hastily during a high-activity period or stored before the SIU models were fully trained. The curator generates a reclassify_pending signal in the training_signals table, which feeds back into offline model retraining.

Selection Criteria

recall_count = 0Never been recalled by any query
epoch_lag > 100Far behind the namespace epoch
archived_at IS NULLOnly active (non-archived) nodes

Processes up to 50 nodes per namespace per cycle to avoid overloading the training signal table.

Step 2: Consolidate Near-Duplicates

LLM-powered merge with archive-only policy

The curator finds pairs of nodes within the same namespace and memory type whose vector embeddings have cosine similarity greater than 0.92. These are near-duplicates — different phrasings of the same knowledge.

When a pair is found, the curator keeps the higher-utility nodeand archives the other. The archived node's content is merged into the kept node via LLM-powered consolidation:

// LLM consolidation via GPT-5.4-nano
let prompt = "Given two overlapping memories, produce a
single condensed summary that:
- Preserves ALL unique facts, dates, names, technical details
- Removes duplicate information
- Strips [merged: ...] artifacts from prior consolidations
- Keeps the result concise but complete
- Maximum 2000 characters";

// Uses Azure Foundry Responses API with JSON Schema
let response = client
    .post(&config.endpoint)
    .header("api-key", &config.api_key)
    .json(&CuratorApiRequest {
        model: config.model,           // GPT-5.4-nano
        text: { format: json_schema }, // structured output
    })
    .send().await;
rust

The LLM produces a single condensed summary that preserves all unique facts from both memories while removing duplicates. If the LLM is unavailable (extraction not configured), the curator falls back to SQL-append behavior: pointer_summary || '\n[merged: ...]'.

After LLM consolidation, the kept node's embedding is nullified (set to NULL) so it gets re-vectorized in Step 4 with the new consolidated content.

With LLM (GPT-5.4-nano)

  • &check; Clean, deduplicated summary
  • &check; Strips [merged: ...] nesting artifacts
  • &check; Preserves all unique facts
  • &check; Max 2000 characters
  • &check; Embedding nullified → re-vectorized

Fallback (No LLM)

  • • SQL append: [merged: content]
  • • Can accumulate nesting over cycles
  • • No deduplication of overlapping facts
  • • Embedding preserved (no re-vectorization)

Processes up to 20 duplicate pairs per namespace per cycle. Archived nodes sync to the AGE graph immediately.

Step 3: Summarize Verbose Nodes

Condense long entries to save tokens without losing meaning

Nodes with pointer_summary longer than 500 characters are candidates for summarization if they meet either condition:

  • low recall recall_count < 3 — rarely accessed, safe to condense
  • merged artifacts Contains [merged: ...] nesting from prior non-LLM consolidations

With LLM enabled, GPT-5.4-nano condenses the node to a concise version (max 500 characters) preserving all key facts. Without LLM, the curator truncates to 200 characters at a word boundary with an ellipsis.

Processes up to 30 verbose nodes per namespace per cycle.

Step 4: Re-vectorize

Generate fresh embeddings for nodes missing vectors

Nodes that had their embeddings nullified during consolidation or summarization — or nodes that were stored before the embedding model was configured — need fresh vector embeddings. The curator identifies these nodes and flags them for the backfill task.

Actual embedding generation happens via the server's backfill pipeline on the next restart or batch cycle. The curator's role here is observability: logging how many nodes are waiting for vectors so operators can track embedding pipeline health.

Why nullify embeddings?

When a node's content changes (via LLM consolidation or summarization), the old embedding no longer represents the new text. Setting it to NULL ensures the node gets a fresh, accurate vector on the next backfill pass. This prevents stale embeddings from degrading semantic search quality.

Step 5: Mark Stale Confidence

Nodes not recalled in 30+ days get flagged

If a memory hasn't been recalled in 30 days, its confidence field is set to stale. This signals to downstream consumers that the information may be outdated.

Nodes with confidence verified (explicitly confirmed by an agent or user) or already staleare excluded. Stale nodes are still recalled normally — consumers can filter or deprioritize them based on the confidence field.

Confidence Levels

verifiedExplicitly confirmed — immune to staleness
staleNot recalled in 30+ days — may be outdated
defaultNormal confidence — eligible for staleness marking

Step 6: AGE Graph Sync

Keep the knowledge graph in sync with curation changes

Every node modified in the current curation window (last 35 minutes) gets synced to the Apache AGE knowledge graph. This includes nodes that were consolidated, summarized, or archived.

Archived nodes are properly marked in the graph via archive_memory_vertex, ensuring the knowledge graph always reflects the current state of memory. Active nodes get their vertices updated with fresh heat values, pin status, and summary text.

Active Nodes

Updated via ensure_memory_vertex— syncs heat, pin status, summary, and namespace to the graph vertex.

Archived Nodes

Marked via archive_memory_vertex— the graph vertex is flagged as archived, not deleted. Relationships preserved.

Syncs up to 100 modified nodes per namespace per cycle. The 35-minute window slightly overlaps the 30-minute cycle to avoid missing nodes modified near cycle boundaries.

Step 7: Activity Logging

Full observability into every curation cycle

After each namespace pass, the curator logs a structured activity record with counts for every step. This feeds the dashboard and enables operators to monitor curation health across all tenants.

-- Query curation activity for a tenant
SELECT
  metadata->>'namespace'    AS namespace,
  metadata->>'reclassified' AS reclassified,
  metadata->>'consolidated' AS consolidated,
  metadata->>'summarized'   AS summarized,
  metadata->>'revectorized' AS revectorized,
  metadata->>'stale_marked' AS stale_marked,
  created_at
FROM activity_log
WHERE tenant_id = 'your-tenant-id'
  AND action = 'curation_cycle'
ORDER BY created_at DESC
LIMIT 10;
sql
MetricDescription
reclassifiedNodes flagged for SIU reclassification
consolidatedDuplicate pairs merged (weaker node archived)
summarizedVerbose nodes condensed via LLM or truncation
revectorizedNodes identified as needing fresh embeddings
stale_markedNodes marked with stale confidence

Never Deletes Policy

Archive-only — your data is always recoverable

The curator never deletes a memory node. When a near-duplicate is found, the weaker node is archived by setting its archived_at timestamp. The original content, metadata, and relationships are fully preserved.

This is a core design principle with direct enterprise implications:

Audit Trail

Every memory that ever existed is recoverable. Compliance teams can trace the full history of any knowledge node.

Zero Data Loss

Consolidation merges content into the surviving node before archiving. Information is preserved, never destroyed.

Reversible

Archived nodes can be restored by clearing archived_at. The curator's work is always undoable.

Training Signal Generation

How the curation cycle feeds back into offline model training

The curation cycle doesn't just clean memory — it generates training signals that improve the SIU models over time. This creates a virtuous feedback loop: curation identifies problems, signals record them, and retraining makes the models better at preventing those problems in the first place.

When the curator flags a stale unrecalled node for reclassification (Step 1), it inserts a reclassify_pending signal into the training_signals table with source = 'curator'. These signals accumulate alongside agent-generated signals (from store, delete, pin, boost) and feed into the same retraining pipeline.

-- Reclassify signals generated by the curator
INSERT INTO training_signals
  (memory_id, tenant_id, signal_type,
   corrected_type, content_snapshot, source)
VALUES ($1, $2, 'reclassify_pending', $3, $4, 'curator')
ON CONFLICT DO NOTHING;

-- These signals feed back into SIU model retraining:
-- SIVU learns better base_utility scoring
-- SICU learns better memory type classification
sql

The Feedback Loop

1

Curator detects problems

Stale nodes, duplicates, verbose entries, missing vectors

2

Signals record corrections

reclassify_pending signals capture what went wrong and why

3

Retraining improves models

SIVU learns better utility scoring, SICU learns better type classification

4

Fewer problems next cycle

Better models mean fewer misclassifications, fewer duplicates, less noise

See the Training Signals documentation for the full retraining pipeline, signal table schema, and SDK integration.

Configuration

Environment variables that control curation behavior

The curator is configured entirely through environment variables. No code changes required — adjust the interval, enable LLM consolidation, or point to a different model endpoint.

# Curation interval (default: 1800s = 30 min)
CURATOR_INTERVAL_SECS=1800

# Enable LLM-powered consolidation and summarization
SULCUS_EXTRACTION_ENABLED=true

# Azure Foundry endpoint for GPT-5.4-nano
SULCUS_EXTRACTION_ENDPOINT=https://your-foundry.openai.azure.com/...
SULCUS_EXTRACTION_API_KEY=your-api-key
SULCUS_EXTRACTION_MODEL=gpt-5.4-nano
bash
VariableDefaultDescription
CURATOR_INTERVAL_SECS180030-minute default cycle interval
SULCUS_EXTRACTION_ENABLEDfalseEnable LLM-powered consolidation and summarization
SULCUS_EXTRACTION_ENDPOINTAzure Foundry Responses API endpoint
SULCUS_EXTRACTION_API_KEYAPI key for the extraction endpoint
SULCUS_EXTRACTION_MODELModel name (e.g., gpt-5.4-nano)

With LLM Enabled

  • &check; Intelligent deduplication via GPT-5.4-nano
  • &check; Clean summaries that strip merge artifacts
  • &check; Embeddings re-generated for consolidated content
  • &check; Higher-quality memory over time

Without LLM

  • • SQL-append consolidation (functional but verbose)
  • • Truncation-based summarization
  • • Zero external API calls — fully offline
  • • Still handles all other curation steps normally

Internal Constants

Hardcoded thresholds in the curator engine

ConstantValuePurpose
DEFAULT_INTERVAL_SECS1,80030-minute cycle interval
IDLE_THRESHOLD_SECS60010-minute idle detection window
RECLASSIFY_EPOCH_LAG100Epochs behind before reclassify flag
DUPLICATE_SIMILARITY_THRESHOLD0.92Cosine similarity merge threshold
VERBOSE_SUMMARY_CHARS500Character threshold for summarization
VERBOSE_RECALL_MAX3Max recall count for summarization eligibility

Memory that maintains itself.

The curation cycle runs automatically on every Sulcus server deployment. Enable LLM-powered consolidation with one environment variable and your agent's memory quality improves continuously — zero human intervention required.