Unbounded sourceObservationIds inflate a 4062-node graph to 192MB; every traversal drops the worker and takes /search down with it

Author: aberenyiCreated Sep 16, 2026Updated Sep 16, 2026

Corroborates #1124 / #1168 / #1360 / #1238 / #825 from a corpus that isolates one variable the earlier reports could not: our graph is logically tiny and still fatal.

Previous reports hit this at 9,474 nodes (#1124) and ~75K nodes (#825), so "large graph" was a plausible confound. Ours is 4,062 nodes / 7,357 edges — an order of magnitude smaller than #825 — and the worker still dies on every traversal. The failure tracks bytes per record, not graph size.

Environment

@agentmemory/agentmemory 0.9.29 (global npm, and current latest)
iii engine 0.11.2 (pinned)
Node v24.9.0
OS Darwin 25.6.0, arm64 (macOS, Apple Silicon)
Store file-based, ~/.agentmemory/data, 362 MB
Corpus 166 sessions, ~17.9K observations, 8,468 semantic, 1,130 insights, 205 lessons, 176 memories
Graph 4,062 nodes / 7,357 edges
LLM / embeddings OpenRouter deepseek/deepseek-v4-pro

The bloat is per-record, and it is sourceObservationIds

mem%3Agraph%3Aedges.bin      114M      # 7,357 edges  -> ~15 KB per edge
mem%3Agraph%3Anodes.bin       62M      # 4,062 nodes  -> ~15 KB per node
mem%3Agraph%3Asnapshot.bin    16M
                             ----
                             192M      # 55% of the entire store

sourceObservationIds occurs 10,453 times in nodes.bin (2.6 arrays per node). The first record in the file is representative — a single CLAUDE.md node whose properties are ~200 bytes, followed by an unbounded id array:

json
{"gn_mtwyuhjg_7457e0ed06ea":{"id":"gn_...","name":"CLAUDE.md",
 "properties":{"purpose":"AI coding agent guidance","lines":"268", ...},
 "sourceObservationIds":["obs_mtwn5si8_9b5aba785ac3","obs_mtws75sp_e3579dbb294e",
  "obs_mtwn626h_9de3743437cc","obs_mtwn62bg_0ad8795459bf", ... ]}}

Same shape as the sourceSessionIds / sourceMemoryIds duplication in #1360 and #1168, but on the graph collections.

Traversal kills the worker; summary reads do not

The split is clean and reproducible. /graph/stats takes the #816 snapshot-only hot path and survives; anything that traverses does not:

bash
curl -s http://127.0.0.1:3111/agentmemory/health | jq '.health.workers[0].function_count'
# 282

curl -s http://127.0.0.1:3111/agentmemory/graph/stats -o /dev/null -w '%{http_code}\n'
# 200   -- returns {"fromSnapshot":true,...}; function_count still 282

curl -s -X POST http://127.0.0.1:3111/agentmemory/graph/query \
  -H 'content-type: application/json' -d '{"query":"balena","limit":3}'
# 500 {"error":"Invocation stopped"}

curl -s http://127.0.0.1:3111/agentmemory/health
# 500 {"error":"Function middleware::api-auth not found"}     <- all 282 functions gone, ~5s

Exactly the #1124 signature, but reached through graph/query rather than smart-search.

/search is collateral damage, and AGENTMEMORY_GRAPH_WEIGHT=0 does not avoid it

This is the part worth highlighting, because it makes graph bloat present as "search is broken" rather than "the graph is broken", which is a much harder thing to diagnose.

HybridSearch.search() calls graph expansion unconditionally — it is not gated on graphWeight:

javascript
const topVectorObs = vectorResults.slice(0, 5).map(r => r.obsId);
if (topVectorObs.length > 0) try {
    const expansionResults = await this.graphRetrieval.expandFromChunks(topVectorObs, 1, 5);
    graphResults = [...graphResults, ...expansionResults];
} catch {}

graphWeight is only consulted later, when combining ranks. So setting AGENTMEMORY_GRAPH_WEIGHT=0 still pays the full load cost. The try/catch also cannot help here, since the worker dies rather than throwing.

The visible result is that memory_recall, memory_smart_search and POST /agentmemory/search all return nothing or 500 while the underlying data is completely intact — GET /observations?sessionId=<id> returns healthy LLM-compressed observations throughout. We spent a long time chasing the search index before finding the graph, because mem::search rebuilds the index when idx.size === 0 and neither its success log (Search index rebuilt) nor its own .catch log (Index rebuild failed) ever appears — the worker dies first, so the index path looks guilty.

Confirmed fix: removing the graph files restores search entirely

Both documented remedies are unavailable: /graph/reset and /graph/snapshot-rebuild crash the same way they are meant to repair (#825), and GRAPH_EXTRACTION_ENABLED=false does not stop growth (#1238). So we moved the files aside with the daemon fully down:

bash
launchctl bootout gui/$UID/com.agentmemory.server   # KeepAlive would restart it mid-move
agentmemory stop --force                            # stops the iii engine too
mv ~/.agentmemory/data/state_store.db/mem%3Agraph%3A*.bin  <elsewhere>
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.agentmemory.server.plist

Immediately afterwards, with no other change:

before after
POST /search Invocation stopped, worker dropped 3 results in 0.6 s, function_count stays 282
memory_recall (MCP) {"results":[]} 5 scored hits (0.99–0.59), 399 tokens
/graph/query drops the worker 0 results, degrades cleanly
data/ 362 MB 183 MB

token_budget combined with format:"compact" also started working again — we had recorded that as a separate quirk for months, and it was only ever a symptom of this.

Suggestions

  1. Bound sourceObservationIds on graph nodes and edgesslice(0, N), or drop it in favour of a reverse index. As with sourceMemoryIds in #1360, it appears to be write-only. This alone would have kept our graph around 5 MB.
  2. Gate expandFromChunks on graphWeight > 0. Cheap, and it gives operators a real escape hatch — right now the documented knob silently does nothing for cost.
  3. Let the graph degrade like /graph/stats already does. The #816 snapshot-only hot path is the right pattern; traversal should fall back to it rather than taking the worker down.
  4. A size warning in /diagnostics would have saved us a day. Every check passed while a 192 MB graph made the product's main feature unusable.

Happy to provide the store sample, the full nodes.bin histogram, or to test a patch.