Skip to content

Factual MCP queries return zero results for facts stored without user_id (NULL ≠ 'anonymous') #180

@actorman46-blip

Description

@actorman46-blip

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

// 1. Store a fact via MCP openmemory_store with no user_id:
{
  "content": "kts2 has IP 10.0.0.146",
  "type": "both",
  "facts": [
    {"subject": "kts2", "predicate": "has_ip", "object": "10.0.0.146", "confidence": 1}
  ]
}

// Server log:
// [TEMPORAL] Inserted fact: kts2 has_ip 10.0.0.146 (...confidence=1, project=system_global)

// 2. Query that fact:
{
  "query": "kts2 ip",
  "type": "factual",
  "fact_pattern": {"subject": "kts2", "predicate": "has_ip"}
}

// Response:
// "No temporal facts matched the query."

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions