Repository navigation
A fact one Bot remembered is reported as remembered to a second Bot, which then never sees it #714
Description
Activity
kvnloo commented
on Oct 3, 2026 ContributorMore actionsI traced this against current
main, and Chebaleomkar's diagnosis matches the store/schema contract exactly.The surrounding code makes the intended ownership pretty explicit:
personalMemories.formedByAgentIdexists specifically for Bot ownership.- The schema comment says a Bot-formed memory is delivered only to the Bot that formed it.
retrieve(..., formed)enforces that withformedByAgentId === formed.botId.- Source-app revocation is also checked per asking Bot.
Given that contract, I think option 1 is the internally consistent fix: scope Bot-formed deduplication to the forming Bot, rather than widening retrieval across Bots.
Concretely, the partial unique key and both duplicate lookups would become effectively:
(ownerUserId, formedByAgentId, importDigest) WHERE formedBy = 'bot'That also keeps the current provenance/security property: Bot B does not inherit a fact Bot A observed through an app B may not hold.
The regression matrix I'd want before changing the migration:
- same Bot + same content twice → one row / duplicate
- two Bots + same content → two rows
- Bot A retrieves only A's row; Bot B only B's row
- deleting A's row prevents A from silently reforming it, but does not tombstone B's independent memory
- concurrent duplicate formation by the same Bot still collapses to one row
Happy to keep this as architecture guidance rather than competing with the implementation you offered.
Thanks @Chebaleomkar for writing this up so clearly, and @kvnloo for tracing it against the schema. With both directions laid out side by side, this one was easy to decide.
Let's go with option 1, where each Bot keeps its own copy. Everything else about a Bot-formed memory already works per Bot. It is only delivered to the Bot that formed it, and only while that Bot still holds the app it came from. Resetting a Bot clears its own rows and nobody else's. Sharing across Bots would let Bot B read something Bot A learned from an app B was never given.
One small detail for whoever picks it up. The dedup lives in
formMemory, notcreate(server/src/memory/store.ts:77-96). Soremember_personal_factisn't affected. The paths that hit this aresave_observed_memoryand proactive research.There are three things that option 1 changes, and they're worth handling in the same PR.
The first is forgetting. Today, when someone deletes a Bot-formed fact, it stays forgotten for every Bot. That's because the deleted row keeps its digest, and the lookup finds it no matter which Bot asks. Once the check is per Bot, Bot B could form the same fact again and put it back in front of the person for review. I'd keep forgetting person-wide. Before inserting, check whether this person deleted the same content from any Bot, and return
forgotten: trueif they did. Happy to hear it if anyone thinks forgetting should be per Bot instead.The second is the Memory page. It lists every row for the person and labels each one "Formed by your Bot". With one row per Bot, the same fact from two Bots would show up as two identical cards. Showing which Bot formed each one would clear that up, and
formedByAgentIdis already on the row.The third is that the index and the conflict target have to change together. The new migration drops
personal_memories_formed_digest_idxand recreates it on(owner_user_id, formed_by_agent_id, import_digest) WHERE formed_by = 'bot'. TheonConflictDoNothingtarget informMemoryneeds exactly the same columns, or Postgres refuses the insert. Existing rows can't collide under the wider key, so no backfill is needed.No test today covers two Bots forming the same fact. It would be good to cover:
- two Bots, same fact: two rows, each recalled only by its own Bot;
- the same Bot is still deduplicated, including two runs at once;
- forgetting across Bots;
- resetting Bot A leaves Bot B's row alone.
@Chebaleomkar, you offered to send the PR, so it's yours if you'd like it.
- added a commit that references this issue
on Oct 10, 2026
rememberfrom a Bot is deduplicated per person, but read back per Bot.server/src/memory/store.ts(thecreatepath forformedBy: "bot") looks for an existing row by(ownerUserId, formedBy = 'bot', importDigest). The unique index has the same shape. It does not includeformedByAgentId.retrievereturns Bot-formed rows only whereformedByAgentIdis the asking Bot.So if Bot A remembers "I prefer metric units" for a person, and Bot B later forms the same sentence, B gets
{ duplicate: true }with A's id. B never records it, and B's retrieval never returns it. B is told the fact is remembered but never sees it.There are two possible directions:
formedByAgentId, so each Bot keeps its own copy.Which one is intended? I'm happy to send a PR for either.