Olivier Couthaud

V

An assistant on my own email that decides what is worth interrupting me for, and stays quiet the rest of the time. It is now being rebuilt as a company of AI agents.

Why I built it

I had already built a few systems where a team of AI agents works on its own, each with its own job. V was the natural upgrade of those, pointed at one concrete job: my email.

What it is

V reads my email and works out which messages are worth bothering me about. The aim is that most days the honest answer is very little, and that is the point. Anything can ping you for every email. The hard part is looking at a few hundred messages and spotting the one that has a consequence, like a reply I promised and never sent, while leaving the rest alone.

Think of a good assistant sitting next to you in the morning. They do not read you your inbox. They say "this one, today, and here is why", and then nothing for the rest of the day. That is what I was trying to build. I ran it on my own Gmail, so when it got something wrong, it was wrong about my real mail.

Your inboxhundreds of emails V narrows itcheap rules first Claude judgesmatters? and why Day Ahead pageplus a phone ping I tap approveor nothing sends
Diagram. The morning run in five boxes. The technical version has the full picture.

What it does

Every morning V went through my recent mail and narrowed it to the threads that might matter. Then it asked a language model, Claude, to judge each one: does this have a consequence, and why. Whatever earned a place went on a page I call the Day Ahead, and could be pushed to my phone. I could answer back on that page.

If an email needed a reply, V could write a draft. Nothing is ever sent until I tap approve. The talking part, the voice you can speak to, is a separate project of mine called VibeOS. V is only the thinking behind it.

What happened

For a long time I could not tell whether it worked. On 12 July the whole thing went through three separate reviews, and all three came back with the same verdict: it talked like an assistant but behaved like a notification app. Every morning it forgot yesterday's conclusions and worked everything out again from scratch. So I rebuilt the middle. Each judgment is now saved, and if the email has not changed, V reuses what it decided before.

Then I built a way to check it, a small separate tool called proof. The idea is simple. Keep track of the things V did not flag too, then see whether the flagged ones really went wrong more often. On one test, "has this sender gone quiet?", they did.

How often the sender really stayed quiet Senders V flagged 68% Senders V let through 36%
Chart. 100 judgments about 51 senders, on my own mail, as a share of each group.

That is a real gap, but I want to be straight about what it shows. Most of those senders are companies, so it is closer to "will this service email again" than to anything about people. On the question I actually care about, whether I will keep a promise I made, it found nothing, and when I dug into why, the thing I was measuring with turned out to be partly blind. That is the kind of result I would rather know about than not.

What's next

I no longer run V every day. I am working on an upgrade instead, and it is a bigger idea than a better inbox filter. It has newer models, a separate model whose only job is to make decisions quickly and cheaply, and decisions for every agent that rest on unbiased statistics instead of on what a model happens to feel that morning.

The larger plan is to turn it into a small company of AI agents. Each agent has a role, like a CEO, a CTO, a CFO and an HR agent. The CFO handles the budget, meaning how much the agents are spending. HR can create new agents, let some go and keep track of how each one is performing. I am the board, and the CEO reports to me. I am designing it carefully before building it, and the first pieces of code exist.

V itself was left with two open problems. It needed far more feedback from me: in July I counted somewhere between 15 and 25 times I had told it whether it got something right, which is not a lot to learn from. And it treated everyone at a company as one sender, so it could not tell when a particular person had gone quiet.

What I wanted

Notifying is easy. Deciding that one email out of a few hundred has a consequence, and that the rest can wait, is the hard part. I wanted an assistant that keeps a model of my situation, says nothing most of the time, and speaks up when something I would otherwise miss is about to cost me.

V came out of the autonomous agent-team systems I had already built. It was the natural upgrade of those, narrowed to one job. It is only the brain. The voice it talks through is a separate project of mine, VibeOS, which connects to V over a websocket. I ran V on my own Gmail and calendar, so its mistakes landed on a real inbox. There is one user, and that turned out to be the actual difficulty: building it was the easy half, and knowing whether any of it works was the hard one. I built it between late June and late July 2026, and I no longer run it daily.

How it works

As I left it, V is a set of scripts on one Windows machine, not a service. A scheduled job ran the morning pass at 7am, there are some manual commands, and a small localhost server serves the page. (That job died without logging a single line on the runs from 4 to 10 July. Failures now land in a log.) A daemon that polls mail exists, but it is not the path I used. Between runs, V senses nothing.

The morning pass fetches between 150 and 2,300 recent messages. Each detector narrows candidates with a regex or a count, then makes one stateless Claude call per candidate at temperature 0. The call returns a verdict, a 0 to 1 consequence score and a sentence of reasoning.

What does temperature 0 mean?

Temperature controls how much randomness a model adds when it picks its words. At 0 it takes the most likely option every time, so the same input should give the same answer. It is not a guarantee, and V's early verdicts still flipped between runs, which is a big part of why I changed the middle.

Gmail (IMAP) Regex prefilterper detector Evidence hash seen?thread + last message no Claude judgeone call, temp 0 yes: reuse writes row Verdict rowssaved judgments Outcome labels zero taps Day Ahead page+ phone push Claude drafts You tap approve SMTP send Voice path (VibeOS)result type has no send recipient bound to the verified sender; no auto-send
Diagram. The morning run as the code stands after the July rebuild. Verdicts are stored as rows and reused while the evidence is unchanged. Sending needs a tap, and the voice path cannot send at all. The eight steps are explained below; with scripts on, you can click a box, or use the buttons under it, to jump to its step.

Step through the morning run

  1. Mail comes in. Gmail over IMAP. A morning run pulls somewhere between 150 and 2,300 recent messages, depending on the day. V reads the full message, not just the subject.
  2. Cheap filter first. Each detector (a thread I left hanging, a sender who has gone quiet, and so on) narrows the pile with a regex or a count before any model sees it. That keeps the model from reading everything.
  3. Seen this before? V takes a fingerprint of the thread and its latest message. If it already has a verdict on exactly that evidence, it reuses it. If a reply has been added, the fingerprint changes and V judges again.
  4. One Claude call. One stateless call per candidate, temperature 0. It returns a verdict, a 0 to 1 consequence score and a sentence of reasoning.
  5. The verdict is saved. The verdict goes into the store as a row keyed by that fingerprint. This is the middle I rebuilt in July. It used to be computed, shown on the page and thrown away.
  6. The Day Ahead page. What earns a place lands on the Day Ahead page and can be pushed to my phone. The page is two-way: I can talk back to it.
  7. Draft, approve, send. V can draft a reply. Nothing goes out until I tap approve, and the recipient is locked to the sender the mail really came from. The voice side cannot send at all, because the result type it returns has no send operation.
  8. Did it matter? Afterwards V labels what actually happened to the things it raised, with no taps from me, and writes that back onto the rows. That is where feedback comes from without me rating anything.

The part I changed most is the store in the middle. Originally every verdict was computed, rendered and thrown away, and I patched the run-to-run flapping by asking three times and taking the majority. Now a verdict is a durable row keyed by a hash of the evidence it was judged on, and unchanged evidence reuses the old conclusion. A steady-state run reuses nearly all of them.

What is an evidence hash?

A hash turns a thread and its latest message into a short fingerprint. The same input gives the same fingerprint, and a new reply gives a different one. The date is left out on purpose, so a thread does not look new just because another day has passed. V compares fingerprints to decide whether anything it would be judging has changed since last time, which is cheaper and steadier than asking the model again.

Before, until mid-July Mail Prefilter Claude verdict Page Verdict discarded flaps between runs: ask three times, take the majority After Mail Prefilter Same evidence? Verdict row Page yes: reuse the row, no call no: one Claude call, saved as a new row outcome labels write back here
Diagram. The change in the middle. Before, every judgment was weather. After, every judgment is a row that tomorrow's run reads first.

The output is a Day Ahead page I can answer back to. V can draft an email reply, but nothing sends without my explicit approval. The recipient is bound to the verified sender of the thread, automated addresses are blocked, anything about money or law is forced to ask first, and sends are idempotent. The voice path has no send operation, and that is not a check I could get wrong: the type its calls return simply does not contain one. In July the send path had been used about once.

Measuring it

I keep a denominator. V writes a row for every candidate it considered, flagged or not, and the unflagged rows are my control group. The measure is whether flagged items go wrong more often than unflagged ones. That harness is now its own package, proof, which V uses. It refuses to print a percentage below an evidence bar, and it counts distinct subjects, not rows.

Subject, asOf dateevidence cut at asOf Judgeany function Claim rowflagged or not, kept Oracle at checkDateconfirmed / refuted Report or unknown: not scored or: refuse Leak guard: judge gets onlyevidenceAt(subject, asOf). Unflagged rows are the control group.
Diagram. The claim ledger in proof. The judge slot takes any function, and V plugs a Claude judge into it. The report gives lift and separation. "Unknown" is a proper answer and is left out of the rates rather than counted as a failure.
What is an oracle, here?

Just whatever tells me what actually happened by the check date. For "did this sender write again" it is a lookup in my mail, so it cannot guess. For "did I deliver what I promised" it has to read conversations, and it cannot see anything that happened on a channel V cannot read. That difference ends up mattering a lot below.

What are lift and percentage points?

Lift is how much more often the flagged items went wrong than the unflagged ones, subtracted as plain percentages. Going from 36% to 68% is a gap of about 33 points (the figures are rounded, so a point can go missing). Separation is the threshold-free version: it looks at the judge's raw scores instead of the yes/no flag, so it does not depend on where I put the line.

The same test, cut six ways

+33pp
All 100 judgments +33pp

"Has this sender gone quiet?" over 100 judgments about 51 senders. Flagged senders stayed quiet 68% of the time, unflagged ones 36%.

What the measurements said. All on my own data, one user. The sender test is 100 judgments about 51 subjects, with oracle coverage 100 of 100.
TestResult
Gone quiet?Flagged 68% vs 36% unflagged: +33pp, separation +0.10
Gone quiet, one row per subject+19pp (first draw), +31pp (last draw)
Gone quiet, vs the old formula33pp vs 26pp on identical rows: inside the noise at this size
Gone quiet, by subject type48 domains: +27pp over 95 judgments. 3 people: +50pp over 5 judgments
Deliver a promise, run 3Flagged 64% vs unflagged 75%, separation -0.03: no signal
Same, after the oracle fixCoverage 11 of 11, but only 4 gradeable promises: not enough evidence
Promise extractionFrom sent email and chat: 226 messages, 20 promises, 17 verified kept or moot, 3 open
Day Ahead from stored state38% of attention items in one real run in late July, against a 50% target
Bad outcome happened, % of resolved solid = flagged, outline = unflagged Has this sender gone quiet? 100 judgments, 51 subjects 68% 36% Will I deliver this promise? run 3, before the oracle fix 64% 75% The first is a gap in the right direction. The second is flat or backwards.
Chart. The two tests side by side, to the same scale. Same harness, very different answers. The second one stopped being trustworthy once I found the instrument was blind, which is the next section.

The flag line for the +33pp test was inherited from the promise work, not fitted on it. The slider above shows how much the number moves depending on how I cut it, so treat +33 as a rough size.

I also ran proof on public GitHub issues (vercel/next.js), a domain it was not built for. A hand-written scoring rule, not a model, gets a +9pp lift over 206 issues, and a one-line age heuristic gets the same 9pp, so the harness says the richer rule is not earning its cost. The proof project has that run.

What didn't work

On 12 July I wrote down, as neutrally as I could, what the code actually does, and it then went through three separate reviews. The three converged: V had an assistant's vocabulary and safety instincts wrapped around a notifier. Every judgment was a stateless call, thrown away after rendering. One review counted eight episodes in the log, about three of them real feedback, and a self-graph whose 15 rows all came from the seed function. The rebuild described above is my answer to them.

A metric I built to say how much of a page was caused by a change in the model was wrong. A truncation bug made two threads collide on one verdict row, which manufactured "flips" that never happened, so on 16 July I deleted the metric instead of patching it.

My oracle could not see anything that happened on a channel V cannot read, so on 11 of 13 failure labels "I never delivered" looked the same as "I delivered by hand or in person". That means the "no signal" result on promises was a statement about my instrument, not about the judge. Run 2 had a separate bug: promises tied to a date, like a call or a move-out, were closed when the date passed and scored as delivered.

The promise judge, run by run

19 of 21
Items it flagged as at risk 19 of 21

Run 1. I asked yes or no: is this promise at risk? About something already open, it nearly always says yes. That is a mood, not a prediction.

Three attempts at learned memory (JEPA-style latent models among them) lost to simple methods at n=1. Silencing mail on sender relationship would have skipped the model call on only 4 of 32 emails, and rested on verdicts that flipped between runs, so I kept escalating instead. And a daily cycle once wrote ten predictions, deleted them and reported success, because three open stores each rewrote the whole file. Stores now merge on write.

What is unfinished

I no longer run V daily. I am working on an upgrade, and it is a different shape from an inbox assistant. It brings in newer models, plus a separate decision model whose job is speed and efficiency: making each call quickly and cheaply. The agents' decisions also move onto unbiased statistics rather than a single model's opinion.

The bigger idea is to run the whole thing as a company of AI agents. Each agent has a role (CEO, CTO, CFO, HR and so on). The CFO handles the budget, meaning how much the agents are spending. HR can create new agents, lay some off and track how each one performs. I am the board and the CEO reports to me. I am designing it carefully before building it, and the first pieces of code exist.

As for V in its original form, most of what surfaces is still triggered by an email scoring high. The 38% above is one run. Feedback is the binding constraint: in July I counted somewhere between 15 and 25 explicit feedback events in total.

The cadence layer is dominated by automated senders and keyed by organisation, so person-level joins mostly cannot work yet. Some of what V knows about me is typed into the source instead of learned. A full morning run cost about $2 when I measured it on 12 July, before verdicts were reused. The tests cover the pure functions, send safety and the stores, not what the model decides. Multi-user isolation exists but has never been tried with a second person.

Stack
TypeScript, Claude, IMAP and iCal, JSONL stores
Status
In progress
Measurement
proof, a separate package, extracted on 28 July
Code
Not public (V, proof, VibeOS and the upgrade)

← All work