Skip to main content
Jonathan Andrei
Back to all posts
Aug. 202613 min read

Unsay: An AI Medication-Safety Agent that Goes Back and Un-Says What It Told You. When the FDA Escalates a Recall, It Corrects Every Named Patient It Already Reassured, in Seconds.

Agent memory has a failure mode retrieval quality cannot fix: stale context. Similarity to a stored memory does not prove the memory is still true. For most agents that is embarrassing. In a pharmacy it is a Class I recall. Unsay's fix is a bitemporal schema on CockroachDB (`valid_from`/`valid_to` for the world, `asserted_at`/`retracted_at` for this system's belief), and a join no vector store can express: every answer still standing that leaned on a claim version we no longer believe. The replay still works past the AS OF SYSTEM TIME horizon, and every correction is exactly-once across a region failure.

CockroachDBHackathonCockroachDBBitemporalVector SearchMCPAWS LambdaAmazon BedrockopenFDAAgent MemoryPython
I created this post and the Unsay project for the Build the Future of AI Agent Memory with CockroachDB hackathon. #CockroachDBHackathon
Two claims, both demonstrated: other agent memories can tell you what they knew, Unsay goes back and fixes what it said. And its replay still works in six months, when MVCC time-travel expired after twenty-five hours. In the hosted demo, twelve patients dispensed one real recalled lot (amlodipine/benazepril GB01616, NDMA above the acceptable intake limit); the sweep examines all twelve, reverses nine, and leaves three alone because they had already been told to stop. Nobody receives a message whose answer did not actually change.
Unsay hosted demo: four numbered steps on a warm off-white page. 1) A patient asks 'My amlodipine and benazepril is from lot GB01616. Is it safe to keep taking?' 2) The world changes: FDA escalates lot GB01616 to Class I, one transaction that retracts the old claim and asserts the new. 3) Retroactive repair: 'Every answer still standing that leaned on a claim version we no longer believe.' 4) Corrections, by name. Header chips: aws-us-east-1 serving, survive zone, 554 claims, 12 standing.
The whole app in one page. Real openFDA data, 554 live claims, hosted on a single AWS Lambda Function URL in front of a CockroachDB Cloud cluster. First request after a quiet spell pays a cold start of a few seconds; nothing after that.

Claim 1: repair, not just recall

When the FDA publishes a recall, some number of answers already given are now wrong. They were correct when given. Nothing about them changed; the world moved underneath them. Finding them is one SQL query. Read it as English: every answer still standing that leaned on a version of a claim we no longer believe. A vector database cannot express this query. Not slowly, not approximately, at all. It has no notion that a memory has versions, and no record of which version a given answer consumed. The most it can return is today's nearest neighbours, which tells you nothing about what you said in March. Reconstructing history is an insight layer; Unsay closes the loop by re-deciding each affected answer against current memory, and where the verdict moves, drafting a correction and queueing a notification for a named person.

'The world changes' step: after publishing the Class I recall, the version chip shows 'v1 → v2 · prior version retracted' and the Class I recall reason text: 'amlodipine-besylate-and-benazepril-hydrochloride lot GB01616 recalled. Reason: NDMA above the acceptable intake limit. Stop use and return to pharmacy.'
One transaction: the old claim is retracted and a new one is asserted beside it under the same key. Nothing already read is mutated. Every answer that consumed v1 is unchanged in the database until the sweep re-decides it.
Corrections list after the sweep: named patients (Tomas Alvarez, Samuel Adeyemi, Hiro Tanaka, Ruth Bernstein, Wei Chen, Colm Doherty, and more) each with a CAUTION → STOP verdict change, the corrected message text, and a once-only dedupe key that turns a replayed sweep into a no-op.
Nine reversed, three left alone. Each correction is written in the same transaction as the status change, the outbox entry, and the audit line, so a sweep killed halfway and restarted cannot double-send. Class I recalls arrive on Tuesdays and Fridays; this loop is what makes it possible to answer them by Friday afternoon.

Claim 2: the replay does not expire

The obvious way to build 'what did the agent know when it decided' on CockroachDB is to store the read timestamp and replay it with `AS OF SYSTEM TIME`. It is elegant, needs no extra tables, and is correct right up until the garbage collector moves past the timestamp you saved. CockroachDB's own docs say so: `gc.ttlseconds` 'is not meant to be a solution for long-term retention of history; for that you should handle versioning in the schema design at the application layer.' The default window is 4 hours. 25 hours is the largest value Cockroach Labs regularly tests. Unsay's durable mechanism is the bitemporal schema those docs prescribe, and `AS OF SYSTEM TIME` remains as a fast path inside the window. `scripts/expiry.py` asks one question both ways at three different ages: inside the window, the two agree exactly (a control that matters, because it proves the bitemporal model is reconstructing real history rather than inventing a convenient one); past the horizon, one route is gone and the other still answers.

Terminal output of scripts/expiry.py comparing bitemporal reconstruction to AS OF SYSTEM TIME at three ages. Inside the GC window: both agree, v[2] = v[2], agree=True. Past the horizon: route A (bitemporal) ANSWERED with the correct historical claim; route B (AS OF SYSTEM TIME) FAILED with 'batch timestamp must be after replica GC threshold'.
The control matters as much as the failure. Inside the window the two routes agree exactly, so the bitemporal model is reconstructing real history rather than inventing a convenient one.

The properties the design carries

Provenance is written atomically with the answer. The decision row and its read set commit together or not at all. There is no code path that stores an answer without recording what produced it, because an answer whose provenance was lost can never be repaired, and a memory system that drops provenance under load effectively has none. Corrections are exactly-once across a region failure: the correction, the status change, the outbox entry and the audit line are one transaction. The outbox dedupe_key is a deterministic hash of (decision, new verdict, triggering fact version), so a sweep killed halfway and restarted recomputes the identical key and the unique constraint turns the replay into a no-op. 'Zero duplicate patient notifications' is a property of the schema, not a hope about how the process exits. Residency is enforced by storage, not by code: `patient` is REGIONAL BY ROW, so an EU patient's memory lives in eu-west-1 because CockroachDB puts it there. The local cluster runs 9 nodes across 3 simulated AWS regions under SURVIVE REGION FAILURE, which places 5 replicas so no region holds a majority.

Signed audit export excerpt showing the chain of decision, decision_read, fact versions, correction and outbox rows for one patient, cryptographically sealed and downloadable as a JSON snapshot.
The audit export is the receipt the app hands a pharmacist. Same query the sweep uses, sealed with a signature, so a regulator sees exactly what the system said, what it read, and what it corrected.
Related project

Unsay: An AI Medication-Safety Agent that Goes Back and Un-Says What It Told You. When the FDA Escalates a Recall, It Corrects Every Named Patient It Already Reassured, in Seconds.

View the project
Assay: A Skincare Tracker with an Error Bar. Cropping the Same Photo Moves Texture by 5.81 Points, So a Verdict Only Fires When the Change Beats That Floor on Your Face.
Previous post

Assay: A Skincare Tracker with an Error Bar. Cropping the Same Photo Moves Texture by 5.81 Points, So a Verdict Only Fires When the Change Beats That Floor on Your Face.

The measurement exists: YouCam's Skin Analysis API scores sixteen skin outputs from a photograph, and it is a genuinely good instrument (byte-identical input gives byte-identical output). The problem is that a score reported without its error is a number you cannot make a decision with. On my own face with one variable changed at a time, brightness ±8% moves blemishes by 4.85 points and cropping the same photograph differently moves texture by 5.81. A realistic four-week treatment effect is about five. Assay measures its own error first, then calls a change real only when it beats that floor.

Aug. 202611 min read
Bloom: Nobody Cancels, They Just Stop Coming. Bloom Reads Each Client's Own Visit Rhythm, Flags the Ones Drifting from It, and Writes a Personal Note to Each. Rules Decide Who, Gemini Decides What to Say.
Next post

Bloom: Nobody Cancels, They Just Stop Coming. Bloom Reads Each Client's Own Visit Rhythm, Flags the Ones Drifting from It, and Writes a Personal Note to Each. Rules Decide Who, Gemini Decides What to Say.

A typical salon loses about 40% of its clients every year, a first-timer who does not rebook within 30 days has about a one-in-five chance of ever returning, and a loyal regular is worth several hundred dollars a year. The signal is invisible because it is an absence, spread across hundreds of people who each have their own rhythm. Bloom's architecture is the idea: rules decide WHO (deterministic risk engine on each client's own median visit gap), Gemini decides WHAT TO SAY (a short note in the owner's voice, referencing that person's real history). The demo of the whole thesis is two clients: Aisha and Jane are both 44 days since their last visit; Aisha comes every 8 weeks, Jane comes every 4. Any tool that flags 'no visit in 60 days' is wrong about one of them.

Jul.-Aug. 202612 min read