Pre-submission checklist | 提交前检查
Bug Description | 问题描述
When MOS_ENABLE_REORGANIZE=true, the GraphStructureReorganizer periodic structure optimization silently works on only one graph tenant — the one configured on the shared default cube (memosdefault) — while data written through the REST API is stored under each request's user_name. As a result, no PARENT / INFERS / FOLLOWS edges are ever created for real tenant data, and the feature appears to "write nodes only, never build edges".
There are also two latent bugs in the same code path:
- The 100-second periodic schedule never runs.
GraphStructureReorganizer._periodic_optimize_structure registers schedule.every(100).seconds.do(self.optimize_structure, ...) but the loop never calls schedule.run_pending(), so these scheduled jobs are dead code. Only the _reorganize_needed new-node trigger actually executes (again, single-tenant).
_summarize_cluster crashes on non-JSON LLM output. If the LLM returns something that is not a JSON object, _parse_json_result yields a non-dict value and the subsequent response_json.get(...) raises, killing the whole optimization pass (including any already-computed sub-clusters).
Environment | 环境信息
- Version: main @ a7367d0 (2.0.33)
- Deployment: docker compose (
docker/docker-compose.yml), MOS_ENABLE_REORGANIZE=true, NEO4J_BACKEND=neo4j-community
Root Cause | 原因分析
src/memos/memories/textual/tree_text_memory/organize/reorganizer.py: _periodic_optimize_structure calls self.optimize_structure(scope=...) without a user_name, so it defaults to self.graph_store.config.user_name — the default cube's tenant (memosdefault), not the tenants that actually received writes via /product/add.
- The same method registers
schedule.every(100).seconds.do(...) jobs but its polling loop lacks schedule.run_pending().
_summarize_cluster assumes the LLM always returns a parseable JSON object and passes the result straight into _create_parent_node, so a single malformed response aborts the entire pass.
Proposed Fix | 建议修复
I have a working patch (validated on a live deployment) that:
- Adds
Neo4jGraphDB.get_all_user_names() (src/memos/graph_dbs/neo4j.py) — MATCH (n:Memory) RETURN DISTINCT n.user_name, with graceful fallback to the configured user_name.
- Adds
GraphStructureReorganizer._optimize_all_users(scope) — enumerates all tenants in the graph and runs optimize_structure(scope=..., user_name=...) for each, tolerating per-tenant failures.
- Routes both the scheduled jobs and the
_reorganize_needed trigger through _optimize_all_users, and adds the missing schedule.run_pending() call.
- Makes
_summarize_cluster return None when the LLM response is not a JSON object, and guards _create_parent_node / add_edge calls against None.
Note: since BaseGraphDB defines the interface, other graph backends (neo4j-community / polardb / postgres) would need an equivalent get_all_user_names() implementation — or the base class could provide a default fallback to config.user_name.
I am not submitting a PR yet per my own workflow, but I can if the approach is endorsed.
Willingness to Implement | 实现意愿
Pre-submission checklist | 提交前检查
Bug Description | 问题描述
When
MOS_ENABLE_REORGANIZE=true, theGraphStructureReorganizerperiodic structure optimization silently works on only one graph tenant — the one configured on the shared default cube (memosdefault) — while data written through the REST API is stored under each request'suser_name. As a result, noPARENT/INFERS/FOLLOWSedges are ever created for real tenant data, and the feature appears to "write nodes only, never build edges".There are also two latent bugs in the same code path:
GraphStructureReorganizer._periodic_optimize_structureregistersschedule.every(100).seconds.do(self.optimize_structure, ...)but the loop never callsschedule.run_pending(), so these scheduled jobs are dead code. Only the_reorganize_needednew-node trigger actually executes (again, single-tenant)._summarize_clustercrashes on non-JSON LLM output. If the LLM returns something that is not a JSON object,_parse_json_resultyields a non-dict value and the subsequentresponse_json.get(...)raises, killing the whole optimization pass (including any already-computed sub-clusters).Environment | 环境信息
docker/docker-compose.yml),MOS_ENABLE_REORGANIZE=true,NEO4J_BACKEND=neo4j-communityRoot Cause | 原因分析
src/memos/memories/textual/tree_text_memory/organize/reorganizer.py:_periodic_optimize_structurecallsself.optimize_structure(scope=...)without auser_name, so it defaults toself.graph_store.config.user_name— the default cube's tenant (memosdefault), not the tenants that actually received writes via/product/add.schedule.every(100).seconds.do(...)jobs but its polling loop lacksschedule.run_pending()._summarize_clusterassumes the LLM always returns a parseable JSON object and passes the result straight into_create_parent_node, so a single malformed response aborts the entire pass.Proposed Fix | 建议修复
I have a working patch (validated on a live deployment) that:
Neo4jGraphDB.get_all_user_names()(src/memos/graph_dbs/neo4j.py) —MATCH (n:Memory) RETURN DISTINCT n.user_name, with graceful fallback to the configureduser_name.GraphStructureReorganizer._optimize_all_users(scope)— enumerates all tenants in the graph and runsoptimize_structure(scope=..., user_name=...)for each, tolerating per-tenant failures._reorganize_neededtrigger through_optimize_all_users, and adds the missingschedule.run_pending()call._summarize_clusterreturnNonewhen the LLM response is not a JSON object, and guards_create_parent_node/add_edgecalls againstNone.Note: since
BaseGraphDBdefines the interface, other graph backends (neo4j-community / polardb / postgres) would need an equivalentget_all_user_names()implementation — or the base class could provide a default fallback toconfig.user_name.I am not submitting a PR yet per my own workflow, but I can if the approach is endorsed.
Willingness to Implement | 实现意愿