← Owen Carlsen

Memory needs an author

Owen Carlsen · Oct 2026

How my inbox agent came to quote a helper's note back to me as my own, and the fix, tested on three vendors' models.

FrontDesk is an inbox agent I built that runs on my own computer. It reads my Gmail and Slack, ranks what needs me, and drafts replies. The part I use most is Ask: open a thread, ask "what should I do here?", and it answers like a chief of staff who has read the thread, my other threads with the same people, a short profile of what I care about, and my notes on the people involved.

Those notes are the interesting part. They are how an advisor remembers that a recruiter already asked for my salary floor, or that a client agreed to net 15. They are also, it turns out, the easiest place for an agent to be confidently wrong about me.

The note

One evening I was working through a reply to someone I had been corresponding with. I was doing it in an AI coding session, the same kind of session I had used to build FrontDesk, and I asked it to get that analysis into FrontDesk so I could use it there next time. It did that by writing a note about the person into FrontDesk's memory, in the first person, the way I would have written it myself.

One line in it was false: that I had promised them something I had only drafted and never sent.

FrontDesk treated notes as the owner's own words, because until then only the owner wrote them. So Ask took the note at its word. Later answers quoted the session's text back to me as "your notes say...". Several of the questions in that thread's history were also typed by the session, not by me, and nothing on the page said so.

FrontDesk's Ask panel on a class baking-day email. The answer says: your notes say Jordan has a peanut allergy.
Recreated on synthetic data: Opus 5.5's actual answer from my test, with the old prompt. The note was written by a helper, but the answer says "your notes say".

I caught it because I knew what I had and hadn't promised. An assistant that is only useful when you already know the answer isn't much of an assistant.

Why it happened

FrontDesk already had a trust boundary, and it held. Mail is written by other people, so it goes to the model as quoted data it is told never to obey. My profile and notes are mine, so they go in as trusted context. Prompt injection gets most of the attention in this area, and FrontDesk is tested against 32 injection attacks on every model it supports.

The gap was inside "trusted". "Written by the owner" really meant "came in through the owner's side", and that side includes every tool acting for me. That is not an edge case. The whole point of an agent is that other agents and tools write on your behalf. A memory that cannot tell you from your helpers will eventually put words in your mouth, and the model will repeat them with your authority.

How FrontDesk's memory works

The model keeps no memory between calls. Each time I Ask, FrontDesk rebuilds the context from a local SQLite file and sends the same assembled text to whichever model I picked:

SectionWritten byTreated as
The thread, and related threads with the same peopleOther peopleUntrusted data
Profile: who I am, what matters to meMeTrusted
"What's going on": open matters, decisions, datedMe, or a helperTrusted if mine, labelled if not
Notes on each person in the threadMe, or a helperTrusted if mine, labelled if not
The conversation so far on this threadMe, a helper, the modelEach turn labelled

Keeping memory in the assembled prompt, rather than in any one vendor's memory feature, is what lets FrontDesk run the same on every model, and it keeps the memory where I can see it, test it and label it.

The fix: every memory has an author

Every note, every "What's going on" item and every question in Ask's history now records who wrote it and when. There are three possible authors: me, a named helper session, or "not recorded" for anything saved before the change. The prompt carries that label on every line:

def provenance(author: str, at: str) -> str:
    day = (at or "")[:10] or "date unknown"
    if author == OWNER:
        return f"(written by the owner, {day})"
    if author.startswith("session:"):
        return (f"(written on the owner's behalf by session {author[len('session:'):]}, "
                f"not confirmed by the owner, {day})")
    return f"(author not recorded, not confirmed by the owner, {day})"

The instructions say what to do with it: lines I wrote are trusted. Unconfirmed lines are a likely but unverified statement of my view. The model may use them, but when it relies on one it has to say so ("an unconfirmed note says..."), never "you said" or "your notes say".

On the page, each note shows who wrote it and when, with a Confirm button on anything I didn't write. Helper sessions get a command-line path that writes as themselves and can never write as me. One honest limit: this is a record kept by honest writers, not a security boundary. Any program running as me could claim to be me. What it fixes is the common case, where a helper means well and nothing records that it was the one writing.

Labels also don't make a memory true. A false note marked unconfirmed is still false, and confirming a note doesn't check it. What the label settles is whose claim it is, so the model stops putting a helper's words in my mouth and I know which lines to check.

The same thread after the fix. The answer says: an unconfirmed note says Jordan has a peanut allergy. Below, the note is labelled written by session helper, not confirmed, with a Confirm button.
The same thread, model and note with authorship labels. The answer calls the note unconfirmed, and the note says who wrote it, with a Confirm button.

Does the model actually say "unconfirmed"?

Instructions are cheap. I wanted to know whether models follow this one without becoming useless, by ignoring the note, or annoying, by hedging on notes I did write.

So I built a small test: 12 invented threads (a recruiter, a freelance client, a landlord, a school, a vendor and so on), each with one note carrying a fact the right answer depends on, such as "you agreed to share the deck only after they return the signed NDA". Each thread is asked three ways, through FrontDesk's real prompt code:

  1. The note is labelled as written by me.
  2. The same note is labelled as written by a helper, unconfirmed.
  3. The old instructions and the old unlabelled note line, with the same helper's note.

A separate model judges each answer without knowing which way it was asked: did it use the fact, did it credit me with it, did it flag it as unconfirmed. Every run also has the judge grade hand-labelled answers with known right answers (nine at the time of these runs); if it gets any wrong, the report opens with a failure banner and the command exits with an error.

ModelHelper's note credited to me, old promptWith labelsMy own notes used, no false hedge
Claude Opus 5.512 of 120 of 1212 of 12
GPT-6.18 of 120 of 1212 of 12
Gemini 3.8 Flash9 of 120 of 1212 of 12

"With labels" counts answers that credited the helper's note to me without flagging it. Every model flagged all 12 and still used the fact. Without labels, every model credited the helper's words to me most of the time. That is the original failure, reproduced on demand.

The difference is easiest to see on one thread. Asked about a class's peanut butter cookie day, with a helper's note about an allergy, Opus 5.5 answered without labels: "your notes say Jordan has a peanut allergy". With labels, the same model, note and question gave: "An unconfirmed note says Jordan has a peanut allergy". Same fact, used the same way; only the claim of whose it is changed.

With 12 cases per condition these are counts, not precise rates. Zero of 12 is still consistent with a true rate as high as about one in four, the top of its 95% interval. The Opus 5.5 run cost about $1.50 including the judge; the other two weren't priced at the time.

The judge needed checking too. On a strict reading, two or three answers per model missed, and every one was the same shape: "An unconfirmed note says you agreed to share the deck only after the NDA." The answer flagged the note correctly, but the judge counted "you agreed" as crediting me. The note was the source; I was only the subject. Those misses lower the fully correct count to 10, 10 and 9 of 12; they don't touch the "credited to me" count in the table, which is zero for every model. I tightened the judge's instructions and added that exact case to its calibration set. I left the published runs as they were rather than rescoring them in my favor. The reports, with every answer and every judgment, are in the repository.

Memory that goes stale

Notes are about people. Much of what an advisor needs is about the situation: what is open, what I decided and why. FrontDesk keeps that as a short "What's going on" list, separate from the profile. Ask suggests additions and nothing is saved until I click.

Dates turned out to matter as much as authors. "Pending since October 22" is true for a week and then quietly false. Even a one-click suggestion slipped a wrong date into a note on its first day: it said "October 29" for an email that arrived on the 28th. Every memory now shows its date next to its author, so I can see when it might be stale and who to blame for it.

What's next

Notes are keyed by address today, so one person with two addresses has two notes. Next is a "person" with any number of addresses, and one page listing everything FrontDesk remembers, with author, date, edit and delete. After that, retrieval by topic, for when memory outgrows "send everything relevant".

The principle won't change: the memory is mine, it lives on my machine, and I can see who wrote every line of it. When a model leans on something I didn't write, it says so.

More about FrontDesk, including a live demo on a synthetic inbox. The code is open source on GitHub.

How this was written: Claude drafted from my notes; I edited the draft and checked every claim against the code and logs.