Summary
When calling the openmemory_query MCP tool with type: "factual" (or "unified") without supplying a user_id, queries return zero temporal facts even though the facts were stored successfully and are present in the SQLite DB. The root cause is an asymmetric user_id contract between the store path and the query path.
Reproducer
But inspecting the DB directly shows the row exists:
sqlite> SELECT id, user_id, project_id, subject, predicate, object FROM temporal_facts;
80c8…|<NULL>|system_global|kts2|has_ip|10.0.0.146
Root Cause
The store path writes user_id as SQL NULL when none is supplied — packages/openmemory-js/src/temporal_graph/store.ts:114:
INSERT INTO temporal_facts (id, user_id, ...) VALUES (?, ?, ...)
[id, user_id || null, ...]
But the MCP query handler coerces missing user_ids to the literal string "anonymous" — packages/openmemory-js/src/ai/mcp.ts:220-221:
const facts = await query_facts_at_time({
user_id: u ?? "anonymous",
...
});
And query_facts_at_time unconditionally adds user_id = ? to the WHERE clause — packages/openmemory-js/src/temporal_graph/query.ts:31-32:
conditions.push("user_id = ?");
params.push(user_id);
Final SQL: WHERE … AND user_id = 'anonymous'. SQL NULL = 'anonymous' evaluates to NULL (not true), so the row is never returned. Every fact stored without a user_id is unreachable via MCP factual queries until it accumulates a user_id.
Notably, find_active_fact() in store.ts:74-84 already handles this correctly — it only adds the user_id = ? filter when user_id is truthy. query_facts_at_time just didn't get the memo.
Suggested Fix (two lines)
packages/openmemory-js/src/temporal_graph/query.ts:
export const query_facts_at_time = async (opts: {
- user_id: string;
+ user_id?: string;
project_id?: string;
...
}): Promise<TemporalFact[]> => {
...
- conditions.push("user_id = ?");
- params.push(user_id);
+ if (user_id) {
+ conditions.push("user_id = ?");
+ params.push(user_id);
+ }
packages/openmemory-js/src/ai/mcp.ts:
const facts = await query_facts_at_time({
- user_id: u ?? "anonymous",
+ user_id: u,
project_id: proj,
The other caller (server/routes/temporal.ts:184) always passes tenant from auth middleware, so making user_id optional in the function signature is backward-compatible.
Verification
Applied locally on main, rebuilt the container, re-ran the exact same MCP factual query that previously returned empty — now returns the stored fact correctly. A separate previously-orphaned fact (stored days earlier without a user_id) also became reachable, so the fix recovers historical data, not just fixes future writes.
Environment
Happy to open a PR if useful.
Summary
When calling the
openmemory_queryMCP tool withtype: "factual"(or"unified") without supplying auser_id, queries return zero temporal facts even though the facts were stored successfully and are present in the SQLite DB. The root cause is an asymmetricuser_idcontract between the store path and the query path.Reproducer
But inspecting the DB directly shows the row exists:
Root Cause
The store path writes
user_idas SQLNULLwhen none is supplied —packages/openmemory-js/src/temporal_graph/store.ts:114:But the MCP query handler coerces missing user_ids to the literal string
"anonymous"—packages/openmemory-js/src/ai/mcp.ts:220-221:And
query_facts_at_timeunconditionally addsuser_id = ?to the WHERE clause —packages/openmemory-js/src/temporal_graph/query.ts:31-32:Final SQL:
WHERE … AND user_id = 'anonymous'. SQLNULL = 'anonymous'evaluates to NULL (not true), so the row is never returned. Every fact stored without a user_id is unreachable via MCP factual queries until it accumulates a user_id.Notably,
find_active_fact()instore.ts:74-84already handles this correctly — it only adds theuser_id = ?filter whenuser_idis truthy.query_facts_at_timejust didn't get the memo.Suggested Fix (two lines)
packages/openmemory-js/src/temporal_graph/query.ts:export const query_facts_at_time = async (opts: { - user_id: string; + user_id?: string; project_id?: string; ... }): Promise<TemporalFact[]> => { ... - conditions.push("user_id = ?"); - params.push(user_id); + if (user_id) { + conditions.push("user_id = ?"); + params.push(user_id); + }packages/openmemory-js/src/ai/mcp.ts:const facts = await query_facts_at_time({ - user_id: u ?? "anonymous", + user_id: u, project_id: proj,The other caller (
server/routes/temporal.ts:184) always passestenantfrom auth middleware, so makinguser_idoptional in the function signature is backward-compatible.Verification
Applied locally on
main, rebuilt the container, re-ran the exact same MCP factual query that previously returned empty — now returns the stored fact correctly. A separate previously-orphaned fact (stored days earlier without a user_id) also became reachable, so the fix recovers historical data, not just fixes future writes.Environment
main@de39bcd(merge of PR chore(openmemory-js): security & test hardening (P1+P2 close-out) #172)hybrid(synthetic embeddings, dim=256)/data/openmemory.sqlite)Happy to open a PR if useful.