Skip to content
All work

Multi-agent · Local-first

Personal AI operating system

My own assistant: a Git-versioned Markdown vault as long-term memory, and a multi-agent runtime that asks before doing anything it cannot undo.

Repo
cortana + cortana-app
Context
Personal project
Role
Sole engineer, architect and only user
Status
In daily use since 2026

The problem

I wanted an assistant for my memory, knowledge, finances and calendar without renting my long-term memory from a vendor. Closed products keep it in a format you cannot read, diff or take with you.

Stack

  • Python
  • FastAPI
  • Next.js
  • Postgres
  • Git as persistence
  • Anthropic API
  • Telegram
  • Docker

What it does

  • Memory is plain text

    A Markdown vault in Git is the source of truth: readable, diffable, portable

  • Approval gate

    Nothing irreversible runs without me approving it first

  • Under $20/month

    A hard spend ceiling every stack decision answers to

  • Restore-tested backups

    Recovery rehearsed and verified, plus a guard against destructive operations

Try it yourself

Interactive
Cortana, the human-in-the-loop gatecortana-app · demo buildSimulation · synthetic data

Say something to it

Where the line is drawn

Reversibility, not importance. Reads pass straight through. Anything that cannot be undone easily, such as a financial record, a sent message or a cancelled event, stops here and waits for me, whatever it is worth.

Pick one of the three on the left. Two of them want to write to the vault, so they will stop and show you the exact change before anything happens. The third only reads, so it does not.

The real assistant runs over Telegram against a Git-versioned Markdown vault, with Postgres for transactional state. The notes, balances and commit hashes below are invented, and nothing is written anywhere.

Context

Cortana is mine. Not a work project, not a product. My own assistant for memory, knowledge, personal finances and calendar, which I talk to over Telegram. It is the project where I make architectural decisions with nobody's constraints but my own, which is why it is worth reading closely.

The premise came from a specific irritation with assistants that advertise memory. They keep it in a store you cannot read, diff, grep or take with you. Your long-term memory becomes the most valuable thing you own inside the product and the least portable. I wanted the opposite, stated as a requirement: the memory has to outlive the runtime. Delete the application tomorrow and everything it knows should still open in a text editor.

Second constraint: a fixed budget under twenty dollars a month. That turns out to be a better design constraint than an unlimited one, because it forces every "just call the model" instinct to justify itself against a cheaper deterministic alternative.

Architecture

Everything follows from one split: memory and runtime are separate, and memory wins.

Memory and runtime, split on purposeRequests arrive over Telegram or the web UI into a FastAPI runtime that dispatches to a lane per domain: memory, finance and calendar. Every lane's irreversible actions pass through a human-in-the-loop gate before touching persistence. Long-term memory is a Git-versioned Markdown vault the runtime reconciles with rather than overwrites; transactional state is Postgres, guarded by restore-tested backups.RUNTIMEPERSISTENCEMeTelegram · web UIFastAPI runtimechannel + orchestrationMemory lanesearch · read · editFinance lanestatements · balancesCalendar laneevents · tasksHuman-in-the-loopgatenothing irreversibleVault (Git)Markdown · pull-firstPostgrestransactional stateBackups + restoredrilldestructive ops blocked
Memory and runtime, split on purpose

Long-term memory is a Markdown vault in Git. Plain text files with front-matter, linked to each other, in a repository with history. The application keeps a local clone and treats it like someone else's repo: pull before write, push after, never assume it owns the working copy. The vault can be edited by me in an editor, by a coding agent in a terminal, or by the application, and none of the three needs to know about the others. Git is the reconciliation mechanism, and it is one I already trust with everything else.

A runtime on top, FastAPI with Postgres for transactional state and a Next.js front end, dispatching to one lane per domain: memory, finance, calendar. Lanes are separate because their failure modes differ. A memory operation that goes wrong writes a bad note. A finance operation that goes wrong corrupts a record of real money.

Everything irreversible passes a human-in-the-loop gate. Not a confirmation dialog bolted on at the end, a stage every write path goes through, where the proposed action is described to me and waits. Sending a message, editing a financial record, cancelling an event: all gated. Reads are not. The line is drawn at reversibility rather than importance, because reversibility is the property you can evaluate mechanically.

The hard part

The best work here came from a near-miss.

Building the finance side meant the system held records of real transactions I rely on. Working through a schema change, I realised how close it sat to a specific bad outcome: a migration against the wrong target, or a test run pointed at the wrong database, could destroy financial history with nothing to restore from. Not a hypothetical, a concrete path from a plausible mistake to unrecoverable loss, in a system whose whole selling point is that I can trust what it remembers.

Three things went in, and the third is the one I argue for hardest:

  1. Automatic backups, on a schedule, off the machine holding the primary.
  2. A hard guard on destructive operations. The code refuses to truncate or drop unless the target database name marks it as a test database. Cheap, and it makes the dangerous case structurally impossible rather than discouraged. Same shape as the single-entry-point fix in my RAG project, arrived at independently.
  3. A restore drill. Backups were restored from scratch and verified. The difference between "we take backups" and "we have restored and know it works" is the difference between a belief and a fact. This is the one everybody skips.

A smaller story I like as much: CI has four required gates, lint, format, type check and tests, and the type check is the one I kept forgetting to run. It had main red for three commits before I made it a gate rather than a habit. A rule you have to remember is not a rule.

Trade-offs

A hand-written scheduler instead of an orchestrator. I run Dagster at work and did not reach for it here. A handful of periodic jobs does not justify a tool built for much larger scale, whose own service, upgrade path and failure modes become a tax on a system operated by me on a weekend.

Markdown is slower to query than a database. Accepted deliberately, and tested rather than assumed: I built a graph-based retrieval layer over the vault, ran it head to head against plain grep on real queries, and it did not win. The graph layer was dropped. Deleting your own more sophisticated implementation when the boring one measures the same is the more useful habit.

Local-first means it is down when my machine is down. No cloud fallback. For a personal assistant that is not a real cost, and it is what keeps the bill under the ceiling.

One user means N=1 forever. Nothing is multi-tenant. That closed off a whole category of design work and made everything else simpler, and I made the call explicitly, in writing, rather than drifting into it.

Results

Four required CI gates before anything merges. Hundreds of tests that have caught real regressions on the way to production, including a batch of nineteen only the full suite surfaced because the individual files passed in isolation. Backups run automatically and have been restored under drill conditions. Cloud spend stays under twenty dollars a month by design.

Personal project, so unlike the four Mercaldas systems nothing here is held back.

Technical metrics

  • 4 CI gates

    Lint, format, type check and tests, all required

  • Hundreds

    Tests, and they have caught real regressions

  • Pull before write

    The runtime reconciles with the vault, never overwrites