The assistant I lost when my internship ended

I spent my first two weeks at Lindy on the email and meetings team. When work started on Lindy Teammate, Lindy's assistant for Slack, my work shifted to integrations, guardrails, MCP support, migration infrastructure, and the new dashboard.

I used Lindy's executive assistant enough that opening Gmail started to feel unnecessary. Its daily brief merged my calendar and inbox into a short list of what needed attention. A few weeks in, I wanted the brief to ask what I planned to do that day and add my answer to a Notion task list I share with a friend. I wanted one loop: get the brief, answer one question, and update the shared list without leaving Messages.

When the internship ended, my access ended too. Keeping the phone assistant cost $50 a month. I liked the product, but I did not need a platform built for many customers. I needed Google and Notion.

The code was new, but the product decisions were not. Lindy had taught me to treat integrations, permissions, migrations, and traces as product work, not plumbing. Annie came together quickly because I was shrinking a system I already understood to one user, one phone, and the tools I needed.

I did not need thirty integrations

At Lindy, I learned that breadth has a cost. While debugging bug reports, I often found that the model was not the part that failed. It had been given several versioned actions with nearly identical names. A correct interpretation could still end with the wrong tool.

Put those actions beside more than thirty integrations and the model has to choose among tools that look interchangeable. More context did not make it smarter. It gave the model more ways to be wrong.

Pi's approach to coding helped me make the opposite choice. I removed anything I did not use. Annie has Google and Notion because those are the two integrations I need. The provider registry contains eight tools, and only two can write.

In daily use, a cold request returns in under 20 seconds, a warm request returns in under 10, and the first week cost about $0.20.

The smaller catalog gives DeepSeek fewer ambiguous choices and less context to read.

Lindy's hard problem is serving many users with different accounts, tools, and expectations. Annie has one user and one phone. I can make choices that would never work for a product at that scale.

The Notion guard guarded too much

Notion took the most work to get right. My first guard stored one connection-wide snapshot of every Notion tool Annie allowed. If any one of those schemas changed, Annie would stop every valid Notion request and tell me to reconnect my account.

The old guard's drift fixture shows the failure. Adding an optional filter to notion-search changed its SHA-256 fingerprint. A request for notion-update-page would still stop, although that tool and the credentials were unchanged.

request
notion-update-page
changed
notion-search
before
a2c799262a3ce3c19ef5cdd983bf3d12b43ab3c426227091b909dcb7054738c0
after
1334b022cc5ca9485754eb529c50f557b45052e9c51176eec71068d5adbbc942
result
reconnect_required / schema_drift

I had written a safety check broad enough to turn a compatible change in one tool into an authentication problem for all four.

I narrowed the check instead of removing it. Annie now reads the live catalog, keeps the four Notion tools it allows, and validates only the selected tool against its current input schema. A changed parameter, name, or type blocks that call. A change to some other tool does not break the request or mark the connection unhealthy.

A guard should fail only the operation whose assumptions changed.

The cheapest model was enough

I tried Gemini first because the free tier was convenient. The rate limits interrupted normal use often enough that I moved on. I did not run a formal model comparison. I picked the next inexpensive option, DeepSeek V4 Flash, and it handled the work.

I bought $2 of credit. After the first week, Annie had used about $0.20. At that pace, $2 lasts roughly two months, or about $12 a year. The Lindy phone experience would have cost $50 each month.

DeepSeek usage dashboard showing $0.17 in cost, 245 API requests, and 1,121,674 tokens for deepseek-v4-flash
DeepSeek usage after about four days: $0.17 across 245 API requests and 1,121,674 tokens. Much of that activity came from automated benchmarks for latency, prompt and response quality, and failures I wanted to catch before deployment.

I do not need the most expensive model available to triage an inbox and choose among eight tools. Current models infer ordinary intent with much less instruction than they needed a year ago.

I still do not ask the model to define its own permissions. Gmail is read-only because Annie has no email-write tool. The code limits Notion writes. Removing a capability is cheaper and safer than spending prompt tokens asking a model not to use it.

I wanted her to be a little annoyed

I chose the name Annie because I think it is beautiful. The acronym came later: Assistant for Notion, Notifications, iMessage, and Email.

I did not want the polished customer-support voice that most assistants default to.

I wanted Annie to sound like someone who wanted to help, but was honestly a little annoyed that you asked.

Her prompt asks for casual, concise lowercase prose. The morning brief separates what needs attention from what is worth a peek and what is safe to ignore. She answers the question, offers the next useful action, and does not pretend every email matters.

A failure should leave a trace

An agent can fail in too many places for its own explanation to be trustworthy. The model can choose the wrong tool, a provider can reject the call, or a worker can stop halfway through. Agents also invent plausible explanations for failures they did not understand.

I designed Annie around a durable trace instead. Every inbound message gets a trace ID. Events from the queue, model, tool calls, providers, and outbound delivery are written to SQLite in order with sensitive values redacted. If a run cannot finish, the fallback comes from code, not the model:

I couldn't complete that request. Trace: tr_3d5e93de1fd84326b04fe3b932d6b65b

I can give that ID to a coding agent and reconstruct the run with pnpm trace -- <trace-id>. I learned this trace-first pattern while working at Lindy. A failed request becomes a reproducible debugging case instead of a conversation about what the model thinks happened.

Reliability comes before another integration

I tend to pad products with capabilities. I added a nutrition tab to Logit because I wanted it to exist, even though I barely use it. Annie has made me resist that habit.

I think Annie is finished for now. I do not have a list of integrations waiting to be added. Staying small is part of the point.

The code still does unglamorous work. The Sendblue receiver rereads a 60-second overlap so a restart does not miss a message. SQLite records each accepted message and queue job together. Annie never retries a provider write when the provider may already have accepted it.

I want to stop thinking about development and use the thing. Agents feel magical while they work. One failed ordinary request breaks that feeling fast, so reliability is the priority now. Daily use will show me the few gaps worth fixing. Until then, Google, Notion, and eight tools are enough.