Source: https://oliviercouthaud.com/v · Status: In progress · 2026 · Tags: TypeScript, Claude, IMAP # 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. ## 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](https://oliviercouthaud.com/vos), which connects to V over a websocket. I ran V on my own Gmail and calendar, so its judgments were about a real inbox. There is one user, and that shaped the work: building it was one half, and measuring whether it works was the other, harder half. I built it between late June and late July 2026, and I do not run it daily now. ## 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. (The job now logs failures. On the runs from 4 to 10 July it had logged nothing.) 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. Figure: 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 - **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. - **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. - **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. - **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. - **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. - **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. - **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. - **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. 1 / 8 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. Figure: 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](https://oliviercouthaud.com/proof), which V uses. It refuses to print a percentage below an evidence bar, and it counts distinct subjects, not rows. Figure: 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 miss. **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 "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.** | Test | Result | |---|---| | 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 formula | 33pp vs 26pp on identical rows: inside the noise at this size | | Gone quiet, by subject type | 48 domains: +27pp over 95 judgments. 3 people: +50pp over 5 judgments | | Deliver a promise, run 3 | Flagged 64% vs unflagged 75%, separation -0.03: no signal | | Same, after the oracle fix | Coverage 11 of 11, but only 4 gradeable promises: not enough evidence | | Promise extraction | From sent email and chat: 226 messages, 20 promises, 17 verified kept or moot, 3 open | | Day Ahead from stored state | 38% of attention items in one real run in late July, against a 50% target | Figure: Chart. The two tests side by side, to the same scale. Same harness, very different answers. The second one comes with a caveat: the instrument could not see everything, which is covered in 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](https://oliviercouthaud.com/proof) project has that run. ## What I learned On 12 July I wrote down, as neutrally as I could, what the code actually does, and it then went through three separate reviews. They converged on one point: V used an assistant's vocabulary and safety instincts around what was, underneath, 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 came out of those reviews. In everyday use on my own inbox it surfaced an important email I had missed, which was useful. It also raised quite a few false positives. The main cause is that V lacks the context of my sent mail when it judges a candidate, and there were some other complications on top of that. Adding that sent-mail context is the next thing to do. A metric I built to say how much of a page was caused by a change in the model turned out to rest on a truncation bug. The bug made two threads collide on one verdict row, which produced "flips" that never happened, so on 16 July I removed the metric instead of patching it. My oracle cannot see anything that happened on a channel V cannot read, so on 11 of 13 "did not deliver" labels, "not delivered" looked the same as "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 also exposed a scoring bug, since fixed: 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 **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) did not beat 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. ## Where it stands 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 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 joining at the level of a person is the next step. 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 and has not been tried with a second person yet. - **Stack**: TypeScript, Claude, IMAP and iCal, JSONL stores - **Status**: In progress - **Measurement**: [proof](https://oliviercouthaud.com/proof), a separate package, extracted on 28 July - **Code**: Not public (V, proof, VibeOS and the upgrade)