Back to Projects

My Health Chat Asked Me to Paste My Own Data

Alex Wilson••6 min read•Builder Journal
Open card-catalog drawer with one card raised from the back beside a glucose meter

I was away from my desk on a Monday, phone in hand, asking my health app about a glucose test series I ran on September 9. A series of readings over one day, deliberately spaced, which was the whole point of the exercise.

The chat answered: "Your Sep 9 readings are in your records, but the list I can see individually only goes back to Sep 10." Then it asked me to copy and paste them in.

My own data. Sitting in a table the app itself wrote. And the app wanted me to retype it.

That was the fifth time some version of that sentence had come back to me. Different metric, different window, same sentence.

How the chat sees anything

Some background. The health block is a private page on one of my sites: glucose, blood pressure, weight, labs, meds, and whatever my Garmin syncs. There is a chat panel where I ask questions about my own numbers in plain language. Behind it, a worker on my Mac Mini builds a context block from the database, hands it to a model with the question, and posts the answer back.

The important word there is builds. A function called buildHealthContext decides what goes into that block before the question exists. Latest values, 7 and 30 day averages, recent readings, lab history, facts I have saved. It is a summary of roughly four years of records, compressed to fit under a character cap.

The model has no tools. It can only see what the summary chose to include.

Five fixes, one window at a time

Looking back at the history, every incident was a different hole in that summary:

  1. Some Garmin metrics were missing from a label map, so seven weeks of synced data never appeared at all.
  2. Garmin only showed latest values and averages. No history.
  3. The trackers section was assembled after labs and fell off the end when the character cap truncated the tail.
  4. Labs had a read-path bug. The full dated history was stored; the context showed less of it.
  5. Glucose showed the last 14 readings. My September 9 series aged out of that window in 19 days.

Each one got a fix, and each fix was reasonable. Add the missing keys. Add history. Reorder the sections. Fix the query. Widen the window.

The hotfix for number five was the widest yet. Glucose now lists every individual reading from the last 90 days, plus every day from the full history that has more than one reading on it (those are the experiment days, the ones I come back to). Weight and blood pressure got 90 days of individual entries. The context also states plainly what is listed individually, so the model stops claiming it only has summaries.

That shipped first, and it helps. But I had been here four times before, and I could see the sixth question coming. The goal I actually have is deep analysis across everything: "look at all my fasting readings since 2018 by month." No pre-baked summary serves that, and every window I widen pushes against the same 40k character cap.

A summary is a guess about which questions will get asked. Mine kept guessing wrong because the questions are mine, and I am not predictable.

Let it ask

So the fix is retrieval. The model gets to request the slice of data a question needs, instead of hoping the summary already has it.

I built it as two lanes.

Lane A is for ordinary chat. My chat replies already support a family of protocol lines the worker parses off the end of a response (one for saving a memory, one for kicking off research, one for suggested follow-ups). I added one more:

FETCH: [{"q":"glucose_readings","from":"2026-09-01","to":"2026-09-10"}]

When a reply comes back as only that line, the worker runs the queries, appends the rows as an automated message in the same turn, and calls the model again. I never see the round trip. I just get an answer with the actual numbers in it, a few seconds slower. It allows two fetch rounds per turn, and any stray FETCH: line is stripped before anything is saved, both in the worker and again in the HSW read path.

The part I cared most about is what the model is allowed to say inside that line. It composes parameters, never SQL. There are seven named queries: glucose readings, blood pressure, weight, labs, med logs, Garmin metrics, tracker entries. Each one is a fixed parameterized statement. The member ID comes from the job row, not from anything the model writes, so no phrasing of a request can change whose data gets read. A bogus query name gets dropped and the model answers from context. Every result is capped at 200 rows, and a capped result says so out loud: "truncated at 200 rows, narrow the date range for the rest." A bounded answer that looks complete is how this whole mess started, so the bound has to be visible.

I did consider giving the model real tools through MCP. It would work. But that chat endpoint is shared with DraftDesk, my writing app, and opening a tool surface there means per-request gating and query execution happening behind model-controlled tool calls. The protocol line keeps the endpoint tool-free, keeps every query in typed worker code, and leaves an audit trail on the job row. Boring, which is what I want near medical data.

Lane B is for the big questions. Two fetch rounds will not sweep four years. For that, the chat's existing research trigger now also covers deep analysis of my own records. It dispatches an agent on the Mini that has a small health-query CLI, built on the same seven queries, and it can run as many of them as it needs.

The test

Same day, I asked for a deep analysis of fasting glucose across my full record.

The chat answered what it could from context and said an agent was on it. About seven minutes later the agent had worked through all 2,001 fasting readings from 2018 to now and posted a month-by-month report back into the thread. It also picked up two rules I had added the day before: I only test on weekdays, so weekend gaps are not missed readings, and a post-meal number should never be compared against a fasting baseline.

Nobody asked me to paste anything.

What I am keeping

The summary stays. Most questions are about this week, and the context answers those with zero fetches. It is the orientation layer now, not the whole truth.

The lesson I keep relearning in different clothes is that a fix which widens one window is a patch, and five patches for the same sentence is a design problem announcing itself. I could have kept going: 120 days, then 180, then a bigger cap. Instead the thing that knows the question got a narrow, typed, honest way to ask for the data.

Share: