An agent that remembers who it is talking to, and who it works for.
We built a knowledge agent with three separate memory levels (user, team, and organisation) so answers stay personal without going private, and stay canonical without going stale. It runs on OpenSearch and a graph store, and people reach it by tagging @sync in Slack or Teams. Discovery in week 1. Production in week 8.
Their first agent had perfect recall and no memory.
The client had already shipped a retrieval assistant over 1.2M documents. It could quote the credit policy verbatim. It could not remember that the analyst asking had requested the EMEA variant four times that month, that her team had agreed in April to stop citing the deprecated 2023 memo, or that Legal had since replaced the policy outright.
Every session started from zero. So people re-explained their context, re-corrected the same mistakes, and eventually stopped asking. Usage fell 40% in the second quarter after launch.
The instinct was to give the agent one big memory store. We argued against it. A single store forces one answer to two incompatible questions: what is true for you, and what is true for the company. Those need different write rules, different lifetimes, and different blast radius.
One agent. Three memories. Different rules for each.
Scope determines who can write, how long a record lives, and how loudly it speaks at retrieval time.
User memory
Working context for one person: their region, their reporting line, the format they want, the corrections they made last week. Written automatically, visible only to them, never cited to anyone else.
Team memory
The decisions and vocabulary a group operates on: which vendor was chosen, what "tier 2 exposure" means here, the runbook for month-end close. Promoted from user memory when two or more members confirm it.
Organisational memory
What the company will stand behind: policy, approved answers, product truth, named owners. Every record is versioned, has an accountable human, and supersedes lower levels on conflict.
The rule the whole system rests on: memory moves up, never sideways. Nothing enters team memory without confirmation, and nothing enters organisational memory without a name attached to it.
Capture is cheap. Promotion is expensive on purpose.
The agent proposes memory constantly and commits it rarely. Five stages sit between a sentence in a conversation and a record the company vouches for.
After each turn, a cheap model pulls durable statements out of the transcript and drops anything time-bound or task-local.
If organisational memory already says it, the candidate is dropped. If it contradicts, it becomes a conflict ticket rather than a record.
Personal preference, team convention, or company fact. Ambiguous cases default down to the narrowest scope. The cost of a wrong personal memory is one annoyed user, not 4,200.
A record seen by 2+ people in a team surfaces in that team's weekly digest as a one-click promote or reject. 71% of promotions happened from the digest, not the console.
Only a named owner in the knowledge council can write level 3, and every write produces a new version with a diff. No silent edits to company truth.
Every answer says which memory it came from.
Retrieval fans out across all three levels in parallel, filters by the caller's ACLs before ranking, then reranks with a scope weight: canonical outranks team, team outranks personal, and a personal preference can change the shape of an answer but never its facts.
The agent then shows its sources by level. Users trusted the system roughly a week after we shipped that label. Being able to see why it knew something mattered more than the answer quality gains.
Underneath, this is a RAG system with two stores rather than one. OpenSearch holds the enterprise corpus and every memory record, with BM25 and vector search in a single hybrid query. A graph database holds the relationships OpenSearch cannot express: which team owns which policy, which record supersedes which, who confirmed what, and how a person sits in the org. Retrieval walks the graph to decide what is in scope, then asks OpenSearch for the text.
1.2M documents plus 4,100 memory records. Hybrid BM25 + kNN, scope and ACL as filter clauses so nothing unauthorised reaches the reranker.
People, teams, systems, policies, and memory records as nodes. Edges carry OWNS, SUPERSEDES, CONFIRMED_BY, MEMBER_OF.
Resolves the caller, fans out across the three memory levels, reranks with scope weighting, and writes the answer trace. One service, four engineers own it.
Nobody opens a knowledge tool. They tag the agent in Slack.
The console exists for governance. The actual surface is Slack: mention @sync in any channel or DM and the agent answers in thread, resolving your identity from your Slack user, your team memory from the channel, and canonical memory from the org.
The same agent runs in Microsoft Teams. One bot service, two adapters: Slack Events API and the Bot Framework. Identity resolves from Slack user or Entra ID to the same graph node, and a Teams channel binds to a team memory scope exactly as a Slack channel does. Their compliance group required Teams for the regulated business units, so both run side by side against one memory layer.
Channel context does real work. The same question asked in #emea-risk and #us-risk returns different team conventions over the same canonical policy. Answers in a channel are also the cheapest promotion signal we have. A thumbs-up from a second team member is one of the two confirmations a team memory needs.
Two corrections, both in week 4.
We let the agent promote to team memory on a single confirmation. Within days, one manager's private assumption about approval thresholds was being quoted to her whole department as team convention. Two confirmations, plus a visible provenance line, fixed it.
Our first TTLs deleted stale memory silently. People noticed the agent getting dumber and couldn't say why. Expiry now archives rather than deletes, and the weekly digest lists what lapsed.
One schema across all three levels. Scope, owner, and provenance are mandatory fields, and that is what makes governance possible without a second system.
{
"id": "mem_9f4c21",
"scope": "team", // user | team | org
"scope_id": "team_emea_risk",
"claim": "Do not cite the 2023 exposure memo.",
"provenance": ["conv_8812", "conv_9043"],
"confirmed": ["u_214", "u_377"],
"owner": "u_214",
"supersedes": "mem_71ab08",
"version": 2,
"expires": "2026-10-11", // null when scope = org
"acl": ["grp_emea_risk"]
}