Skip to content

Status shows Memories/Observations as 0. Graph is always empty #666

@LeonemFu

Description

@LeonemFu

1. Problem Overview

While agentmemory v0.9.21 is running normally, the following two anomalies are observed:

Symptom Description
Status shows Memories/Observations as 0 Running agentmemory status in the terminal shows Memories and Observations both as 0, yet the Viewer page (http://localhost:3113) and direct API calls return data correctly.
Graph is always empty The Graph tab in the Viewer is empty; agentmemory status shows 0 nodes, 0 edges; calling /agentmemory/graph/stats returns all zeros.

2. Environment

  • Version: 0.9.21
  • OS: macOS
  • Node: v24.13.1 (via nvm)
  • Data Dir: ./data/state_store.db (file-based SQLite via iii-engine)
  • Actual Data Scale: 26 sessions, 121 observations, 4 memories

3. Root Cause Analysis

3.1 Status Shows 0 — /agentmemory/export API Times Out

The implementation of agentmemory status in dist/cli.mjs is:

const [healthRes, sessionsRes, graphRes, memoriesRes, flagsRes] = await Promise.all([
  apiFetch(base, "health"),
  apiFetch(base, "sessions"),
  apiFetch(base, "graph/stats"),
  apiFetch(base, "export"),   // ← the culprit
  apiFetch(base, "config/flags")
]);
const obsCount = memoriesRes?.observations?.length || 0;
const memCount = memoriesRes?.memories?.length || 0;

Root cause: status relies on /agentmemory/export to obtain memory and observation counts, but this API consistently times out (>5s no response) with the current dataset, causing memoriesRes to be undefined and both counts to fall back to 0.

Verification:

# Times out
$ curl -s http://localhost:3111/agentmemory/export
# (5s+ no response)

# Yet memories/sessions APIs work fine
$ curl -s http://localhost:3111/agentmemory/memories
{"memories": [...]}  // returns 4 items

$ curl -s http://localhost:3111/agentmemory/sessions
{"sessions": [...]}  // returns 26 items

Additional finding: state_store.db contained 2 corrupted session records without an id field. Removing them did not resolve the export timeout, indicating a deeper issue (likely a performance bottleneck in iii-engine's file-based KV adapter when handling large numbers of concurrent kv.list() calls).

3.2 Graph is Empty — session.stopped Event is Never Published

Automatic extraction is broken:

The graph extraction registration in dist/index.mjs is:

sdk.registerFunction("event::session::stopped", async (data) => {
  // ... triggers mem::summarize, mem::slot-reflect ...
  if (isGraphExtractionEnabled()) {
    const compressed = (await kv.list(KV.observations(data.sessionId)))
      .filter((o) => o.title);
    if (compressed.length > 0)
      sdk.triggerVoid("mem::graph-extract", { observations: compressed });
  }
  return summary;
});

This event subscribes to the agentmemory.session.stopped topic. However, after searching the entire codebase, no code publishes the agentmemory.session.stopped event:

  • The session-end hook only calls /agentmemory/session/end
  • api::session::end only updates endedAt / status, publishing no events
  • No other hooks / MCP tools publish this event either

Result: All sessions have endedAt but no stoppedAt, so graph extraction is never triggered automatically.

3.3 Viewer "Build Graph" Button Calls a Non-existent API

The Viewer page (dist/viewer/index.html) calls the following when the graph is empty:

var buildResult = await apiPost('graph/build', {});

However, /agentmemory/graph/build has no corresponding endpoint in v0.9.21 (returns HTTP 404).

4. Local Workaround

The following are local workarounds. The cli.mjs modification will be overwritten after upgrading the npm package.

4.1 Fix agentmemory status Display

Modified file: node_modules/@agentmemory/agentmemory/dist/cli.mjs (or the equivalent path in your global installation)

Changes:

Around line ~2208, change:

apiFetch(base, "export"),

To:

apiFetch(base, "memories"),

Around line ~2221, change:

const obsCount = memoriesRes?.observations?.length || 0;

To:

const obsCount = Array.isArray(sessionsRes?.sessions)
  ? sessionsRes.sessions.reduce((sum, s) => sum + (s.observationCount || 0), 0)
  : 0;

Effect: status no longer depends on the timing-out export API; it reads directly from the memories and sessions endpoints, returning correct counts.

4.2 Manually Build Graph — build-graph.mjs

Since automatic extraction is broken, this script reads the underlying observation data files directly and calls /agentmemory/graph/extract to manually extract the graph.

Attention: This script has only been tested locally and may not be universally applicable.

Full source:

#!/usr/bin/env node
/**
 * Manually build the agentmemory knowledge graph from existing observations.
 * Works around the missing session.stopped event in v0.9.21.
 */

import { readFileSync, readdirSync } from "fs";
import { join } from "path";

const DATA_DIR = "./data/state_store.db";
const BASE = process.env.AGENTMEMORY_URL || "http://localhost:3111";
const SECRET = process.env.AGENTMEMORY_SECRET || "";
const BATCH_SIZE = 15; // observations per batch
const CONCURRENCY = 2; // parallel batches

function headers() {
  const h = { "Content-Type": "application/json" };
  if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
  return h;
}

async function apiPost(path, body) {
  const res = await fetch(`${BASE}/agentmemory/${path}`, {
    method: "POST",
    headers: headers(),
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`POST ${path} failed: ${res.status} ${text}`);
  }
  return res.json();
}

function parseFirstJson(raw) {
  let depth = 0;
  let inString = false;
  let escape = false;
  let start = null;
  for (let i = 0; i < raw.length; i++) {
    const b = raw[i];
    if (escape) {
      escape = false;
      continue;
    }
    if (b === 0x5c) {
      escape = true;
      continue;
    }
    if (b === 0x22 && start !== null) {
      inString = !inString;
    } else if (!inString) {
      if (b === 0x7b) {
        if (depth === 0) start = i;
        depth++;
      } else if (b === 0x7d) {
        depth--;
        if (depth === 0 && start !== null) {
          try {
            const str = raw.slice(start, i + 1).toString("utf-8");
            return JSON.parse(str);
          } catch {
            // continue searching
          }
          start = null;
        }
      }
    }
  }
  return null;
}

function readObservations() {
  const files = readdirSync(DATA_DIR).filter(
    (f) => f.startsWith("mem%3Aobs%3A") && f.endsWith(".bin")
  );
  const observations = [];
  for (const file of files) {
    try {
      const raw = readFileSync(join(DATA_DIR, file));
      const obj = parseFirstJson(raw);
      if (obj && typeof obj === "object") {
        for (const obs of Object.values(obj)) {
          if (obs && typeof obs === "object" && obs.title) {
            observations.push(obs);
          }
        }
      }
    } catch (e) {
      console.warn(`Skip ${file}: ${e.message}`);
    }
  }
  return observations;
}

async function processBatch(batch, batchNum) {
  console.log(`[Batch ${batchNum}] Sending ${batch.length} observations...`);
  try {
    const result = await apiPost("graph/extract", { observations: batch });
    if (result?.success) {
      const nodes = result.nodesAdded || 0;
      const edges = result.edgesAdded || 0;
      console.log(`[Batch ${batchNum}] -> ${nodes} nodes, ${edges} edges`);
      return { nodes, edges };
    } else {
      console.error(
        `[Batch ${batchNum}] -> failed: ${result?.error || "unknown"}`
      );
      return { nodes: 0, edges: 0 };
    }
  } catch (e) {
    console.error(`[Batch ${batchNum}] -> error: ${e.message}`);
    return { nodes: 0, edges: 0 };
  }
}

async function runInParallel(batches, concurrency) {
  const results = [];
  for (let i = 0; i < batches.length; i += concurrency) {
    const chunk = batches.slice(i, i + concurrency);
    const chunkResults = await Promise.all(
      chunk.map((batch, idx) => processBatch(batch, i + idx + 1))
    );
    results.push(...chunkResults);
  }
  return results;
}

async function main() {
  const observations = readObservations();
  console.log(`Found ${observations.length} observations with titles`);

  if (observations.length === 0) {
    console.log("No observations to extract graph from.");
    return;
  }

  const batches = [];
  for (let i = 0; i < observations.length; i += BATCH_SIZE) {
    batches.push(observations.slice(i, i + BATCH_SIZE));
  }
  console.log(
    `Split into ${batches.length} batches (size ${BATCH_SIZE}, concurrency ${CONCURRENCY})`
  );

  const startTime = Date.now();
  const results = await runInParallel(batches, CONCURRENCY);
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);

  const totalNodes = results.reduce((s, r) => s + r.nodes, 0);
  const totalEdges = results.reduce((s, r) => s + r.edges, 0);

  console.log(
    `\nDone in ${elapsed}s! Total extracted: ${totalNodes} nodes, ${totalEdges} edges`
  );

  try {
    const stats = await (
      await fetch(`${BASE}/agentmemory/graph/stats`, { headers: headers() })
    ).json();
    console.log("Current graph stats:", JSON.stringify(stats, null, 2));
  } catch (e) {
    console.warn("Could not fetch graph stats:", e.message);
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Usage:

cd /path/to/your/agentmemory-project
node build-graph.mjs

My local results (174 observations, batch=15, concurrency=2):

Found 174 observations with titles
Split into 12 batches (size 15, concurrency 2)
...
Done in 321.0s! Total extracted: 177 nodes, 104 edges
Current graph stats:
  totalNodes: 253
  totalEdges: 289

4.3 Clean Up Corrupted Data

Directly parsed data/state_store.db/mem%3Asessions.bin and removed entries without an id field, eliminating 2 corrupted records.

5. Suggested Fixes

  1. Fix /agentmemory/export API timeout

    • Investigate the performance bottleneck in iii-engine's file-based KV adapter under large-scale concurrent kv.list() calls; or add pagination / streaming to export.
  2. Fix session.stopped event publishing

    • Explicitly publish the agentmemory.session.stopped topic inside api::session::end or the session-end hook; or have iii-engine auto-publish lifecycle events on kv.update state changes.
  3. Implement /agentmemory/graph/build API

    • This endpoint is called by the Viewer but not implemented on the server side. A batch graph-build endpoint that re-processes existing observations would close the gap.

Reported on: 2026-05-26

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions