Skip to content

userId is not threaded into the extraction prompt — first-person facts get unanchored #144

Description

@amiller

Summary

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.

Reproduction

Standalone Python script: clients/python/examples/repro_extractor_no_user_identity.py (no dep on the in-flight #143 client — uses raw httpx).

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").

Observed (Gemini Flash, MEMORY_EXTRACT_PROVIDER=gemini)

Across multiple 5-run batches:

matthammer subject in extracted triple Anchored to userId="andrew"?
"User's private library"
"User" (User → is working on → matthammer)
"User"
"User"

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:

async function callExtract(cfg, model, apiKey, text, userId?: string) {
  const userHint = 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`
    : "";
  const promptText = `${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
  • Found while integrating Contexto into hermes-agent (companion plugin branch). Python client for selfhost in flight at feat: add Python client at clients/python/ #143.

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.

Metadata

Metadata

Assignees

No one assigned

    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