Unbounded sourceObservationIds inflate a 4062-node graph to 192MB; every traversal drops the worker and takes /search down with it
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 storesourceObservationIds 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:
{"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:
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, ~5sExactly 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:
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:
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.plistImmediately 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
- Bound
sourceObservationIdson graph nodes and edges —slice(0, N), or drop it in favour of a reverse index. As withsourceMemoryIdsin #1360, it appears to be write-only. This alone would have kept our graph around 5 MB. - Gate
expandFromChunksongraphWeight > 0. Cheap, and it gives operators a real escape hatch — right now the documented knob silently does nothing for cost. - Let the graph degrade like
/graph/statsalready does. The #816 snapshot-only hot path is the right pattern; traversal should fall back to it rather than taking the worker down. - A size warning in
/diagnosticswould 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.
Source: rohitg00/agentmemory