The first thing my memory system remembered wrong was an ampersand.
A patient registered with the complaint "chest pain & dizziness." When I later pulled up their history, it read chest pain & dizziness. Nothing had crashed. The queue had moved normally, the visit had completed, and the memory request had succeeded. Yet somewhere between the registration form and the doctor's history panel, the patient's own words had changed.
That small bug changed how I thought about the feature. I had spent time deciding where to store patient memory and how to retrieve it. I hadn't paid enough attention to the text entering the system. The quality of recall was already being decided before Hindsight saw a single note.
A queue that needs to remember more than a token number
My project began as a hospital queue system. A patient registers, receives a token, waits for registration, moves to the doctor stage, and eventually reaches completed. Emergency patients are prioritized; other patients are served in arrival order.
The application uses React for the frontend, Express and Mongoose for the API, MongoDB for queue records, and Docker Compose to run the services. MongoDB answers operational questions: which patients are waiting, which stage are they in, and who should be called next? A compound index supports that ordering:
patientSchema.index({ stage: 1, status: 1, isEmergency: -1, createdAt: 1 })
;
But when someone returns for another visit, the doctor needs a different answer: What happened last time, and is any of it relevant now? A queue record contains information that can help, but the doctor shouldn't have to search old notes manually while a patient is waiting.
I added Hindsight, an open-source agent memory system, to handle that second question. After a doctor completes a visit, the API retains a written summary. When the same patient reaches the doctor's screen again, the API recalls their history.
The important separation is this: MongoDB runs the queue; Hindsight supplies remembered context. The memory service doesn't decide who is next, and its availability shouldn't determine whether a patient can finish a visit.
The memory entry is an interface, not just a string
The integration looks simple from the outside: retain after a completed visit, recall when the doctor needs history. Most of the design work is hidden inside the text passed to retain.
Here's the summary built when the doctor stage completes:
const visitDate = new Date().toISOString();
const summary = [
`Visit date: ${visitDate}`,
`Patient: ${patient.name} (age ${patient.age})`,
`Presenting issue: ${patient.issue}`,
patient.notes ? `Doctor notes: ${patient.notes}` : 'Doctor notes: none',
patient.isEmergency ? 'Priority: Emergency' : 'Priority: Standard',
].join('\n');
await retainVisit(String(patient._id), summary);
I could have sent the entire MongoDB document as JSON. Instead, I chose short, labeled lines. They give both the retrieval system and the person reading the result enough context to interpret each field without looking up the original record.
The timestamp belongs inside the memory, not only in MongoDB metadata. If a later visit raises the question "what happened last spring?", the earlier event needs its own date. The patient name and age make a recalled snippet understandable on its own. An explicit Doctor notes: none distinguishes a visit with no recorded notes from an incomplete-looking summary. And Priority: Emergency reads more naturally in a returned note than a bare boolean.
This is also why I didn't write a generic sentence such as "patient visited the doctor." It is technically true but gives the next doctor almost nothing to work with. Retention is not merely saving something; it is deciding what a future reader will need.
The ampersand took a different route than I expected
The bug started in registration validation, before any memory call. My text sanitizer looked like this:
function sanitizeText(value) {
if (typeof value !== 'string') return value;
const trimmed = value.trim();
return validator.escape(trimmed);
}
I used validator.escape because I was thinking about untrusted text reaching a web page. But it encodes characters such as &, <, >, and quotation marks as HTML entities. Because I applied it before storage, the encoded string became the value of patient.issue in MongoDB.
The rest of the pipeline did exactly what I'd asked:
Registration form
↓
Input validation + HTML escaping
↓
MongoDB patient.issue = "chest pain & dizziness"
↓
Visit-summary construction
↓
Hindsight retain
↓
Doctor's recalled history
Nothing in that chain knew the original text had been different. A successful retain operation couldn't repair it. A successful recall operation simply brought the altered wording back.
There was another clue: doctor notes weren't processed by that same sanitizer. They were trimmed and length-limited, so a summary could contain an escaped presenting issue beside a perfectly readable doctor's note. That inconsistency pointed me back to the input path rather than the memory API.
My correction was to preserve the original text in storage and escape it at the output boundary, for the destination that actually needs escaping. React's normal text rendering already escapes interpolated text. Input validation still matters: check types and lengths, and keep the separate protections against MongoDB operator injection. But HTML encoding and database validation solve different problems. Encoding a patient's words for HTML is not a reason to replace the stored words.
One consequence is worth emphasizing: fixing the sanitizer prevents new corrupted entries. It does not automatically rewrite notes already retained. Existing affected records would need to be identified and repaired from a trustworthy original source, if one exists. Otherwise, blindly decoding stored strings could create another data-integrity problem.
Why each patient gets a separate memory bank
The ampersand was a quality problem. Patient separation is a different, more serious concern.
The wrapper derives a bank name from the MongoDB patient ID:
const client = new HindsightClient({
baseUrl: 'http://host.docker.internal:8888'
});
function bankId(patientId) {
return `patient-${patientId}`;
}
async function retainVisit(patientId, text) {
try {
await client.retain(bankId(patientId), text, {
context: 'visit-summary'
});
} catch (err) {
console.warn('[Hindsight] retain failed:', err?.message || err);
}
}
I chose one bank per patient instead of putting everyone's visits into one shared bank and relying only on a patient-ID filter at retrieval time. That makes the intended retrieval scope explicit: a call for one patient's bank is not a general search over every patient's history.
A separate bank is not a complete privacy guarantee. The API still has to authenticate staff, authorize access to the requested patient, protect identifiers, and handle deletion and retention rules appropriately. It also depends on correct identity matching: if a returning patient is accidentally assigned a new ID, their previous memories won't be found under that new bank. If two people are incorrectly assigned the same ID, separation by bank won't fix the mistake.
The design gives me a clearer boundary to build those safeguards around. It also keeps the retain call readable: the patient ID selects the bank, while { context: 'visit-summary' } identifies the type of entry being stored.
Recall has to be useful before anyone types a question
In this workflow, the doctor doesn't open a chat window and compose a perfect search prompt. The history panel needs to load when the patient becomes the one currently being served. That shaped the recall query:
async function recallHistory(
patientId,
query = 'past visits, symptoms, and treatment'
) {
try {
const response = await client.recall(
bankId(patientId),
query,
{ maxTokens: 1024 }
);
return Array.isArray(response?.results)
? response.results
: [];
} catch (err) {
console.warn('[Hindsight] recall failed:', err?.message || err);
return [];
}
}
The default query is intentionally broad. At the moment the patient arrives, I don't know which part of their history will matter. I want a compact overview of prior visits, symptoms, and recorded treatment information, not a narrow answer that might omit useful context.
The maxTokens: 1024 setting limits how much recall returns. That isn't a clinical rule; it's a UI tradeoff. A short panel is easier to scan during a consultation than an unfiltered dump of old records. The original line-by-line retention format helps here too: even a plain
rendering is understandable because the date, issue, notes, and priority each have a label.
The API exposes the read path through GET /patients/:id/history, checking the patient record in MongoDB before requesting that bank's history. The recalled content is supporting context, not a new diagnosis or an independent verification of the notes. A doctor still needs to interpret it alongside the current encounter.A return visit, from beginning to end
Consider an illustrative example, not captured production output.
A patient first registers with "persistent headache, worse in the mornings." The doctor completes the visit with notes saying, "Advised sleep hygiene, review in two weeks." The API marks the doctor stage complete and builds a dated memory entry with that issue and those notes.
A month later, the patient returns reporting "blurred vision." Once the doctor calls them, the frontend requests their history. Hindsight returns relevant material from the same patient's bank, and the doctor sees the earlier headache complaint and follow-up advice beside the current issue.
The system hasn't concluded that the two symptoms are connected. It has done something narrower and more useful for this project: made an existing note visible at the moment it might matter. That's the distinction I want the feature to preserve.What happens when memory isn't available?
A hospital queue can't depend on the memory service being online. In my wrapper, failed retain and recall calls are caught and logged. A failed retain doesn't prevent the queue's completion logic from proceeding; a failed recall returns an empty array rather than throwing an error into the doctor's screen.
There are tradeoffs. The completion route still awaits retention, so a slow memory service can increase response time even when its eventual failure is caught. And a swallowed retain error can mean a missing memory entry with only a warning in the logs. At a larger scale, I'd want a retryable background job or an outbox pattern, along with monitoring for failed writes.
There's also a presentation problem: an empty array could mean "no previous visits" or "the memory service failed." Those aren't the same fact. The wrapper currently gives the frontend the same shape for both, which keeps the screen simple but hides an important distinction. A future version should expose a safe availability indicator without blocking the visit.
The container networking was another practical lesson. Hindsight runs on the host at port 8888, while Express runs inside Docker. Using localhost inside the API container points back to the container, not the host. My setup uses host.docker.internal, with the following Compose mapping for Linux:
extra_hosts: - "host.docker.internal:host-gateway"That configuration issue has nothing to do with the quality of memory, but if the API cannot reach the service, neither retain nor recall can do its job. Operational reliability is part of the feature.
What I'd test differently next time
The ampersand bug wasn't hard to fix after I saw it. It was hard to notice because each component appeared to be working. I'd test the whole journey of the text, not just whether the endpoints return success.
Character preservation: register complaints containing &, apostrophes, angle brackets, and ordinary punctuation; compare the stored text and the recalled entry against the original input.
Missing notes: complete a visit without doctor notes and verify that the retained summary explicitly says Doctor notes: none.
Patient isolation: retain distinct histories for two IDs, then verify each history request uses only its intended bank; separately test authorization at the API boundary.
Returning identity: confirm that repeat visits resolve to the same patient ID before expecting earlier history to appear.
Service failure: stop Hindsight and verify that queue completion still works, that failures are logged, and that the UI does not imply an unavailable history is an empty history.
Recall readability: inspect what a doctor actually sees, rather than assuming a valid API response is a useful summary.
These are tests I would prioritize, not a claim that all of them are already automated in this project.The lesson that stayed with me
I started this feature thinking the main question was whether an agent memory system could bring an old visit back at the right time. It can only do that well if the earlier visit was represented faithfully in the first place.
The surprising part was where the failure lived. It wasn't in a retrieval algorithm or a complicated prompt. It was in an ordinary input sanitizer that had made sense when I was thinking only about HTML. Adding a second consumer of that data exposed the assumption.
For this hospital queue, the architecture remains straightforward: MongoDB manages the live workflow, Hindsight retains and recalls visit context, and the doctor's screen presents that context without treating it as a diagnosis. But the detail I'll carry into the next version is simpler: before asking whether your system remembers, check whether it stored the right words.
Top comments (0)