You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Running the OpenClaw local memory plugin (@memtensor/memos-local-plugin v2.0.8), the gateway node process pinned one CPU core at 100% for 40+ minutes on startup, holding 4.2 GB RSS. Investigation traced this to two distinct bugs in the plugin:
Vector retrieval does a synchronous O(n) full-table scan on the Node main thread, decoding every vector BLOB and computing cosine in-process — this starves the event loop (100% CPU, GC/IO threads idle).
A pagination cap in the capture-side dedup read causes the same trace to be re-inserted indefinitely, bloating the traces table (in our case 518,375 rows of which 437,792 — 84% — were exact duplicates), which is what made bug feat: add internet retrieval and CoT functionality to MemOS #1 catastrophic.
Together: bug #2 grows the table without bound → bug #1 loads the whole thing synchronously on every dreaming/retrieval cycle → CPU melts.
Environment
@memtensor/memos-local-pluginv2.0.8 (MemOS Local Memory (V7))
Node 26, better-sqlite3
dreaming.enabled: true, embedding.enabled: false
memos.db had grown to 6.8 GB (traces table alone = 5.96 GB, ~4.2 GB of that in vec_summary/vec_action BLOBs)
Evidence — native stack of the 100% CPU main thread (gdb)
strace -c over 3s on the main thread: 56,637 pread64 calls, 99.97% of time — i.e. the thread is doing nothing but synchronously reading BLOB overflow pages off a multi-GB table.
exportfunctionscanAndTopK(db,table,selectExtra,query,k,opts){const{ vecColumn, norm2Column, where, params, hardCap }=opts;constcols=["id",vecColumn, ...(norm2Column ? [norm2Column] : []), ...selectExtra];constsql=[`SELECT ${cols.join(", ")} FROM ${table}`,where ? `WHERE ${where}` : "",`LIMIT ${hardCap??100000}`,// default cap = 100,000 rows].filter(Boolean).join(" ");constrows=db.prepare(sql).all(params);// <-- loads up to 100k rows w/ vector BLOBs synchronouslyconstdecoded=[];for(constrofrows){constvec=decodeVector(r[vecColumn]);// <-- decode every BLOB on main thread
...
}returntopKCosine(query,decoded,k);// <-- cosine over all of them on main thread}
Called from dist/core/storage/repos/traces.js:255 (searchByVector).
Problems:
No ANN/vector index — retrieval is a brute-force O(n) scan over the entire table.
.all() materializes up to hardCap ?? 100000 rows, each carrying multi-KB vector BLOBs, into JS memory in one synchronous call (hence 4.2 GB RSS and the JS_all stack above).
Decode + cosine also run synchronously on the main thread.
With a table in the hundreds-of-thousands of rows, every dreaming/retrieval cycle blocks the event loop long enough to peg a core and make the gateway unresponsive.
Suggested fixes: add a real vector index (e.g. sqlite-vec), or at minimum (a) stream/iterate instead of .all(), (b) do decode+cosine off the main thread (worker), (c) pre-filter candidates by cheap SQL predicates before loading BLOBs, (d) make the default hardCap much smaller and explicit.
Bug #2 — dedup pagination cap → unbounded re-insertion
Documented in a code comment at dist/core/storage/repos/traces.js (listAllForEpisode):
/** * Full episode-scoped trace fetch with NO pagination cap. The paginated * `list({ episodeId })` path silently truncates to 500 rows (default 50), * which breaks capture-side dedup when an episode has more than the cap * worth of steps — the next runLite/runReflect re-inserts everything past * the cap as "novel". Use this for any dedup / reconciliation read. */
If any code path still feeds dedup from the paginated list({ episodeId }) (cap 500) instead of listAllForEpisode, then for any episode with >500 steps the dedup check cannot see the older traces, so runLite/runReflect treats them as novel and re-inserts them every cycle.
Observed in our DB — the damage is exactly this shape:
traces: 518,375 rows, but only 80,583 distinct by (episode_id, turn_id, user_text, agent_text, tool_calls_json) → 437,792 exact duplicates (84%).
Duplicates concentrated in a few large episodes, e.g. one episode had 49,402 rows but only 153 distinct turn_ids / 588 distinct contents — i.e. the same turns re-inserted hundreds of times.
Duplicates span the full lifetime of the DB (2026-05-19 → 2026-07-08), consistent with a per-cycle re-insertion loop rather than a one-off.
Suggested fix: ensure all capture-side dedup/reconciliation reads use the uncapped listAllForEpisode (or a keyset-paginated full read), and/or add a UNIQUE constraint / INSERT … ON CONFLICT DO NOTHING on the natural trace identity so re-insertion is idempotent.
Impact
Gateway CPU pinned at 100%, unresponsive, 4.2 GB RSS.
DB bloated 8× (≈850 MB → 6.8 GB) purely from duplicate rows + their vectors.
Workaround we applied (data-side only — does not fix the plugin bugs)
Stopped the gateway, backed up, de-duplicated traces by full content identity (518,375 → 80,583 rows), rebuilt the traces_fts FTS5 index, VACUUMed (6.8 GB → 1.3 GB), restarted. CPU settled to ~11%. But both root causes are in plugin code, so this will recur as data grows again.
One operational note that bit us during cleanup: traces has trigram FTS5 triggers (traces_fts_ai/ad/au), so a row-by-row DELETE of 437k rows is pathologically slow (didn't finish in 50 min). Dropping the triggers, doing the bulk delete, then rebuilding the FTS content and recreating the triggers was orders of magnitude faster. Not a bug per se, but worth being aware of for any maintenance tooling.
Summary
Running the OpenClaw local memory plugin (
@memtensor/memos-local-pluginv2.0.8), the gatewaynodeprocess pinned one CPU core at 100% for 40+ minutes on startup, holding 4.2 GB RSS. Investigation traced this to two distinct bugs in the plugin:tracestable (in our case 518,375 rows of which 437,792 — 84% — were exact duplicates), which is what made bug feat: add internet retrieval and CoT functionality to MemOS #1 catastrophic.Together: bug #2 grows the table without bound → bug #1 loads the whole thing synchronously on every dreaming/retrieval cycle → CPU melts.
Environment
@memtensor/memos-local-pluginv2.0.8 (MemOS Local Memory (V7))dreaming.enabled: true,embedding.enabled: falsememos.dbhad grown to 6.8 GB (tracestable alone = 5.96 GB, ~4.2 GB of that invec_summary/vec_actionBLOBs)Evidence — native stack of the 100% CPU main thread (gdb)
strace -cover 3s on the main thread: 56,637pread64calls, 99.97% of time — i.e. the thread is doing nothing but synchronously reading BLOB overflow pages off a multi-GB table.Bug #1 —
scanAndTopKblocks the event loopdist/core/storage/vector.js:149Called from
dist/core/storage/repos/traces.js:255(searchByVector).Problems:
.all()materializes up tohardCap ?? 100000rows, each carrying multi-KB vector BLOBs, into JS memory in one synchronous call (hence 4.2 GB RSS and theJS_allstack above).With a table in the hundreds-of-thousands of rows, every dreaming/retrieval cycle blocks the event loop long enough to peg a core and make the gateway unresponsive.
Suggested fixes: add a real vector index (e.g.
sqlite-vec), or at minimum (a) stream/iterate instead of.all(), (b) do decode+cosine off the main thread (worker), (c) pre-filter candidates by cheap SQL predicates before loading BLOBs, (d) make the defaulthardCapmuch smaller and explicit.Bug #2 — dedup pagination cap → unbounded re-insertion
Documented in a code comment at
dist/core/storage/repos/traces.js(listAllForEpisode):If any code path still feeds dedup from the paginated
list({ episodeId })(cap 500) instead oflistAllForEpisode, then for any episode with >500 steps the dedup check cannot see the older traces, sorunLite/runReflecttreats them as novel and re-inserts them every cycle.Observed in our DB — the damage is exactly this shape:
traces: 518,375 rows, but only 80,583 distinct by(episode_id, turn_id, user_text, agent_text, tool_calls_json)→ 437,792 exact duplicates (84%).turn_ids / 588 distinct contents — i.e. the same turns re-inserted hundreds of times.Suggested fix: ensure all capture-side dedup/reconciliation reads use the uncapped
listAllForEpisode(or a keyset-paginated full read), and/or add aUNIQUEconstraint /INSERT … ON CONFLICT DO NOTHINGon the natural trace identity so re-insertion is idempotent.Impact
Workaround we applied (data-side only — does not fix the plugin bugs)
Stopped the gateway, backed up, de-duplicated
tracesby full content identity (518,375 → 80,583 rows), rebuilt thetraces_ftsFTS5 index,VACUUMed (6.8 GB → 1.3 GB), restarted. CPU settled to ~11%. But both root causes are in plugin code, so this will recur as data grows again.One operational note that bit us during cleanup:
traceshas trigram FTS5 triggers (traces_fts_ai/ad/au), so a row-by-rowDELETEof 437k rows is pathologically slow (didn't finish in 50 min). Dropping the triggers, doing the bulk delete, then rebuilding the FTS content and recreating the triggers was orders of magnitude faster. Not a bug per se, but worth being aware of for any maintenance tooling.Happy to provide more detail or test a fix.