I Used Hindsight Tags to Draw a Customer Boundary
The first version of customer memory had a deceptively simple question: how do I tell Hindsight which customer a support issue belongs to? It is tempting to put the customer's name in the recall query and trust semantic search to do the rest. That is useful for relevance, but it is a poor tenant boundary.
I wanted two different properties from memory retrieval. The current issue should influence which facts rank highly, and the customer identity should determine which facts are even eligible. Hindsight tags let me keep those concerns separate: issue text guides retrieval within a customer partition, while a strict tag filter defines the partition itself.
Relevance is not isolation
The application uses one configured Hindsight bank. Every support request contains a customer name and an issue. The route can search for “Ravi's Wi-Fi issue,” but a natural-language instruction such as “only return Ravi's memories” is still just part of a query. It is not a database constraint. A similar name, a strongly related issue, or a model-generated memory that mentions more than one person can make query wording an unreliable security boundary.
This distinction is easy to miss because the same search query often appears to work in a two-person test. If I test Ravi with a router issue and Mina with a billing issue, the semantic differences make results look isolated. Then I try two customers with the same problem and learn that similarity and ownership are independent dimensions.
I made ownership explicit in a Hindsight tag. The tag is computed from the normalized customer string using SHA-256:
function getCustomerTag(customer: string) {
const normalizedCustomer = customer.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
const digest = createHash("sha256").update(normalizedCustomer).digest("hex");
return `support-customer-${digest}`;
}
Normalization prevents harmless presentation differences from creating separate memory partitions. Unicode compatibility normalization handles equivalent forms, trimming removes leading and trailing spaces, whitespace runs collapse, and lowercasing makes capitalization consistent. Hashing gives the tag a stable, fixed-format value without exposing the display name directly in the tag string.
None of those operations turns a name into an identity system. “Ravi” and “ravi” become one key by design. More importantly, two real people both named Ravi also become one key. A hash is a representation of the input; it cannot add information the input did not contain. If the application is connected to a customer database, the partition key should be an immutable customer ID from that system, not a name typed into a form.
There is a second boundary worth keeping straight: a tag is a retrieval filter, not an authorization policy. It prevents this recall call from selecting records with another tag, but it does not establish who is allowed to call the route or choose a customer. In a deployed support tool, the authenticated operator's permissions should determine which customer IDs they may access, and the server should resolve those IDs rather than trust arbitrary browser input. Hindsight can scope the memory query; the application still owns the access decision around that query.
Tags decide who can be recalled
With a customer tag available, Hindsight recall can still use the current issue for relevance. The request uses both:
const taggedMemory = await hindsight.recall(
bankId,
`Recall previous support issues, resolutions, and preferences related to: ${issue}`,
{ tags: [customerTag], tagsMatch: "any_strict" }
);
The query asks what is useful. The tag says whose memory is in scope. The strict matching mode matters because the SDK's default tag matching can include untagged memories. If customer separation is a requirement, I do not want old global records silently admitted by a permissive default.
The same customer tag scopes the read and accompanies the write.
The write path applies the same tag. Hindsight retention stores the customer marker, the issue, and the generated response together:
await hindsight.retain(
bankId,
`Customer ${customer} contacted support.
Current issue:
${issue}
Support response:
${response}`,
{ tags: [getCustomerTag(customer)] }
);
That symmetry is more valuable than a second search instruction. A memory written for Ravi carries Ravi's tag, and a later recall for Mina asks Hindsight to filter on Mina's tag. I can then ask Groq to reason about the selected history without depending on the model to determine whether a fact belongs to the current person.
I think of this as two filters in sequence. The tag is the hard boundary. The issue query is the ranking signal inside that boundary. Combining them makes the retrieval behavior easier to reason about: first exclude other tenants, then search for what is relevant to this request.
The tag also gives me a stable place to evolve the retrieval strategy. I can ask for more than one relevant result, add a score threshold, or adjust the query wording without changing the customer partition. Conversely, I can move from a name-derived key to a CRM key without changing the support prompt itself. That separation reduces the number of unrelated decisions that have to move together when the system changes.
The tag also gives me a stable place to evolve the retrieval strategy. I can ask for more than one relevant result, add a score threshold, or adjust the query wording without changing the customer partition. Conversely, I can move from a name-derived key to a CRM key without changing the support prompt itself. That separation reduces the number of unrelated decisions that have to move together when the system changes.
What the operator sees
The interface follows the same model. It asks for a customer name and a current issue, then presents “Memory Found” separately from “AI Support Response.” This is not just a visual nicety. If an answer refers to an earlier router replacement, the operator can inspect whether that history was actually retrieved for this customer.
Consider two people who both report intermittent Wi-Fi failures. For Ravi, Hindsight might return that replacing a power adapter resolved a previous outage. For Mina, the same semantic query should not make Ravi's replacement history eligible. Mina can receive a response based on her own recalled history, or a response that simply addresses the current issue if her partition is empty.
The point is not that tags make the response correct. They make one class of error less likely: using a memory that the request should never have seen. Relevance still needs inspection, and the answer still needs to follow the support prompt. I keep those as separate responsibilities instead of treating “memory found” as a correctness certificate.
Hindsight gives this boundary a first-class place in the memory API. The Hindsight open-source repository and Hindsight API documentation describe the memory and tag-filtering capabilities. The agent memory overview also explains why persistent memory is more than putting a longer transcript into each model request.
The identity decision I would keep
There are four practical lessons I took from this design:
- Do not use semantic wording as authorization. “Only return records for this person” is not a substitute for a filter enforced by the memory query.
- Separate ownership from relevance. A stable tag defines scope; the issue text helps retrieve the useful facts inside that scope.
- Use an actual system identifier when one exists. Normalized names are convenient for a prototype, but a production customer boundary needs a key that distinguishes people with the same name.
- Apply the same key when writing and reading. A partition that is only used at recall time is incomplete if new records are still retained without it.
My opinion is that customer-specific memory starts with identity, not prompt engineering. Hindsight makes it possible to attach that identity to retained facts and enforce it at recall. The part I still have to own is choosing an identity that really represents one customer. Once that key is trustworthy, retrieval can focus on the support problem instead of being asked to guess who the memory belongs to.
Top comments (0)