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
When /v1/ingest receives content with first-person language ("I'm working on X", "my favorite Y is Z"), the extractor produces triples whose subjects are bare strings like "User", "I", or "User's private library" — never the userId that was passed in the request.
userId is correctly honored as a storage-side filter (search and recall scope correctly), but it never reaches packages/memory/src/providers/extract.ts or the extraction prompt. The extractor has no knowledge of who "I" refers to.
Runs against a live selfhost (http://localhost:4010), repeats 5x, ingests one turn under userId="andrew" containing both first-person facts ("my favorite color is teal", "I'm working on matthammer") and a third-party named entity reference (research about "Shashank, founder of Contexto").
0/N runs ever produce a subject containing andrew or any binding to the userId despite it being passed in the request.
Knock-on effect (the bigger issue)
When the extraction context contains a more "concrete" named entity, the extractor can latch onto that entity as the substitute for first-person references — producing triples like (Shashank, prefers, teal) when the user actually said "my favorite color is teal".
Real-world manifestation we hit while wiring Contexto into hermes-agent: a research subagent's findings about a real person mixed with primary-agent self-reflection, and the extractor started attributing the user's own projects to the researched person across hundreds of triples (e.g. matthammer → is a project of → Sha). Hard to repro deterministically in isolation, but the unanchored-subject bug is the root enabler.
Proposed fix (small, drop-in)
Thread userId into the extraction prompt in packages/memory/src/providers/extract.ts. Roughly:
asyncfunctioncallExtract(cfg,model,apiKey,text,userId?: string){constuserHint=userId
? `\n\nNote: First-person language ("I", "my", "me") in the conversation below refers to the user with id "${userId}". Use that as the subject when extracting first-person facts.\n\n`
: "";constpromptText=`${EXTRACT_PROMPT}${userHint}${text}`;// ... rest unchanged: pass promptText into the LLM call}
And plumb userId through:
extract() in extract.ts
Memory.ingest() in memory.ts:103
router.post('/v1/ingest') in router.ts:152 (already has userId in scope from the request body)
Environment
Selfhost stack built from Dockerfile.selfhost (with a couple of patches — pnpm@9 pin and node:22-slim runtime — happy to send those as a separate PR if useful)
MEMORY_EXTRACT_PROVIDER=gemini, MEMORY_EMBED_PROVIDER=gemini, gemini-2.5-flash for extraction, gemini-embedding-001 for embeddings
Happy to send the fix as a follow-up PR if the team's open to the approach. The fix would also break ties for the knock-on attribution drift, since the model would have an explicit anchor for first-person rather than reaching for the most-salient nearby named entity.
Summary
When
/v1/ingestreceives content with first-person language ("I'm working on X", "my favorite Y is Z"), the extractor produces triples whose subjects are bare strings like"User","I", or"User's private library"— never theuserIdthat was passed in the request.userIdis correctly honored as a storage-side filter (search and recall scope correctly), but it never reachespackages/memory/src/providers/extract.tsor the extraction prompt. The extractor has no knowledge of who "I" refers to.Reproduction
Standalone Python script:
clients/python/examples/repro_extractor_no_user_identity.py(no dep on the in-flight #143 client — uses rawhttpx).Runs against a live selfhost (
http://localhost:4010), repeats 5x, ingests one turn underuserId="andrew"containing both first-person facts ("my favorite color is teal", "I'm working on matthammer") and a third-party named entity reference (research about "Shashank, founder of Contexto").Observed (Gemini Flash,
MEMORY_EXTRACT_PROVIDER=gemini)Across multiple 5-run batches:
"User's private library""User"(User → is working on → matthammer)"User""User"0/N runs ever produce a subject containing
andrewor any binding to theuserIddespite it being passed in the request.Knock-on effect (the bigger issue)
When the extraction context contains a more "concrete" named entity, the extractor can latch onto that entity as the substitute for first-person references — producing triples like
(Shashank, prefers, teal)when the user actually said "my favorite color is teal".Real-world manifestation we hit while wiring Contexto into hermes-agent: a research subagent's findings about a real person mixed with primary-agent self-reflection, and the extractor started attributing the user's own projects to the researched person across hundreds of triples (e.g.
matthammer → is a project of → Sha). Hard to repro deterministically in isolation, but the unanchored-subject bug is the root enabler.Proposed fix (small, drop-in)
Thread
userIdinto the extraction prompt inpackages/memory/src/providers/extract.ts. Roughly:And plumb
userIdthrough:extract()inextract.tsMemory.ingest()inmemory.ts:103router.post('/v1/ingest')inrouter.ts:152(already hasuserIdin scope from the request body)Environment
Dockerfile.selfhost(with a couple of patches — pnpm@9 pin and node:22-slim runtime — happy to send those as a separate PR if useful)MEMORY_EXTRACT_PROVIDER=gemini,MEMORY_EMBED_PROVIDER=gemini,gemini-2.5-flashfor extraction,gemini-embedding-001for embeddingsHappy to send the fix as a follow-up PR if the team's open to the approach. The fix would also break ties for the knock-on attribution drift, since the model would have an explicit anchor for first-person rather than reaching for the most-salient nearby named entity.