Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions