Fat Sai LifeOS
AI Engineering
A steward that runs on a schedule instead of on command, so the things I forget to ask about still get looked at.
01The problem
I run marketing for three companies as an associate director and operate three businesses of my own, plus a household and a brokerage account. The work was never the bottleneck; the coordination overhead was. Mail that needed a reply sat unanswered, commitments made in a meeting were gone within a day, receivables aged quietly, and money moved through accounts I had no consolidated view of.
The approach I had was a reactive assistant, which means I had to remember to ask — so the things I forgot stayed forgotten. Every tool I owned covered exactly one slice: an invoicing app, a calendar, a broker app, a transcription tool. None of them talked to each other and none of them volunteered anything. The reframe that started this project was simple to state and awkward to build: move from an assistant that acts on command to a steward that runs the picture proactively.
02What I built
A personal steward that runs unattended on a Mac and reports into a private cockpit. Scheduled jobs wake a headless model agent on a fixed calendar, each one carrying a written brain that tells it what to gather, how to judge severity, and what it is forbidden to do. Every judgement is written to a shared database as a durable, deduplicated row rather than a notification that scrolls away, so the cockpit is a ledger of what the steward noticed. Anything that would act outwardly in my name is written as a pending draft first.
- Four autonomous watcher domains — mail and calendar, receivables, meeting transcripts, portfolio — each on its own schedule with its own safety rules.
- An observation and action database with a one-to-five severity scale, source-keyed deduplication, and a run log that records every cycle including its failures.
- A one-page cockpit with no sidebar: portfolio state, a savings countdown, pending approvals, the steward timeline, watcher health and a project board.
- A money-flow view built from parsed bank and card statements, because Hong Kong banks expose no open API and the PDF is the only interface there is.
- A human-in-the-loop gate: drafts are created pending, and only rows explicitly marked approved are ever executed.
- A module registry that makes the cockpit a hub — a new domain is one registry entry plus one route group.
Drawn, not captured. One page, no sidebar: a portfolio band, then approvals waiting on a human, then the steward timeline with a mark per row, then watcher health. Every value, bar and mark shown is illustrative placeholder content.
03As a product
- Who buys it
- Operators who run several businesses at once and are the single point of coordination — founders, agency principals, family-office and portfolio managers. The same architecture fits internal chief-of-staff tooling inside a professional services firm.
- Value
- Most assistants wait to be asked, which means they only help with what you already remembered. This is a scheduled, opinionated watcher layer: it decides what deserves attention, keeps a permanent record of that judgement, and hands over a drafted action rather than a notification. The unit of output is a reviewed decision, not a chat reply.
- Positioning
- Deterministic automation tools fire on triggers but cannot judge whether an email actually needs a reply. Chat assistants can judge, but only when prompted, and they forget. Vertical software covers one domain each and never cross-references. This sits in the gap: model judgement on a cron, a database as memory, and a hard approval boundary on anything outward.
- Status
- Internal tool, built for one operator, not commercialised and never priced. The watcher-plus-observation-database pattern is the part that would license; the domain integrations are personal.
04How it works
- Stack
- Next.js and TypeScript on Vercel behind a cookie session gate, with Supabase Postgres as shared memory. Watchers are launchd calendar jobs invoking a headless model agent, with Python and Bash grabbers for broker, statement and ledger data. Document generation runs a dual-mode headless browser; historical files sit in object storage behind presigned URLs.
- Shape
- A loop, not a pipeline. Every cycle opens and closes a run row, so silence is detectable rather than invisible, and every outward action stops at a pending row until a human flips it.
Width uniform throughout — nothing in this loop has ever been counted per branch, so every band is drawn at equal width and no quantity is claimed.
Nothing goes out in my name unapproved. An outward action is written as a pending row and the executor only ever picks up rows a human has already flipped; everything else is logged and stops there, and a source that is offline produces no observations at all rather than a warning.
The decision I spent longest on
Putting a language model in the judgement seat of an unattended cron job. Rules cannot tell a newsletter from a client who is actually waiting on you, but a model in a loop has no natural bound — it can hang, hit a usage cap, or spend an hour on one message. The mitigation had to be architectural rather than a better prompt: a run row that opens and closes, a wall-clock watchdog that kills an overrun so the next slot starts clean, and turn limits that cap the agent independently of time. The second call was making outward actions structurally impossible instead of discouraged. The portfolio watcher never touches the broker’s trade-unlock path and never holds that password, so it cannot place an order even if instructed to.
05Retrieval architecture
A steward that runs on a clock has a different retrieval problem from an assistant that answers questions. Nobody types anything, so there is no query to interpret, and nothing accumulates into a body of knowledge to search: each cycle goes back to the source and reads its domain again from scratch.
What is left is one lookup, repeated on every item, and it turns out to be the only one the system needs. The database sits downstream of the judgement instead of upstream of it — which is why the layer that would normally be the centre of a retrieval architecture is the one that is missing.
ModelAt query time only. The model runs inside every scheduled cycle and nowhere else; nothing was ever embedded, summarised or precomputed ahead of it.
- Corpus
- Live sources, re-read in full each cycle: a fixed mail query and the day’s calendar window, the receivables ledger, transcript files that appeared on disk since the last run, and a read-only snapshot of the brokerage book. Nothing is copied into a store, so nothing can go stale between runs; the price is that every cycle pays for its own gathering.
- Ingestion
- Calendar entries under launchd, the busiest domain firing every thirty minutes across the working day. Each entrypoint applies its own guard window before it does anything, working hours and skip Sunday, and wraps the run in a wall-clock watchdog, because the failure this system actually had was not a bad answer but a process that hung and looked healthy for about forty-four hours. The mail sweep is capped at twenty-five messages a cycle, and a source that is offline yields zero observations rather than a warning.
- Index
- None. There is no corpus to index. A watcher never searches a body of knowledge; it re-reads its own domain, so the only lookup a cycle performs is whether an observation already exists for this item’s source key: a mail identifier, an invoice number, a dated snapshot prefix. That single equality check carries the entire deduplication burden, and it is what stops the steward reporting the same email four times a day. An early migration did create an embeddings table with a cosine similarity index, on the assumption that recalling past work would need semantic search. Nothing ever wrote to it and nothing has ever read it, which is a fair measure of how much of a retrieval problem this turned out not to be.
- Query
- Structured filters, never language. The cockpit reads by domain, status, date and severity, and the portfolio view takes the last sixty snapshots, so a request is a set of column predicates before it reaches the database. Rows marked private are excluded by row-level security and then stripped a second time on the way out, because one gate that can be misconfigured is not a gate.
- Selection
- The mechanism is an ordinary predicate: the filters above, ordered by recency and severity and capped at what a page can show. The shaping happens before selection rather than after it. Severity is assigned once, at judgement time, on a closed one-to-five scale, so by the time anything is read back the ranking already exists as a stored column. Nothing is scored at read time and nothing is reordered by a second pass, which means every line the cockpit shows can be traced to the cycle that wrote it.
- Grounding
- The model is allowed to judge and forbidden to act. Anything that would go out in my name is written as a pending row, and the executor step only ever picks up rows a human has already flipped to approved. Where the stake is higher the restriction is structural rather than procedural: the portfolio watcher never calls the broker’s trade-unlock path and does not hold that password, so it cannot place an order even if it were instructed to. Errors are recorded in the run row and never as an alert, because the previous generation of this system was retired for pushing false alarms to a phone.
Present — this layer exists and runs.Absent by decision — the layer is not there, and the sentence beside it is the reason. Every one of the six is answered on every system in this chapter, so the rows can be read across pages.
06Numbers
| Measure | Figure | Basis |
|---|---|---|
| Commits, first to latest | 122 | Verified |
| Cockpit built and shipped, single day | 32 commits | Verified |
| Autonomous watcher domains live | 4 | Verified |
| Bank and card statement formats parsed | 7 | Verified |
| Transactions categorised without a human, 13 months | 99.1% | Verified |
| Longest undetected outage, before the watchdog | about 44 hrs | Verified |
| Typical cycle, against a 900-second kill timeout | about 50 s | Verified |
| Manual triage displaced, per day | 30-45 min | Projected |
| Marginal running cost, per month | Tens of USD | Projected |
Verified — counted from the repository, the run log, and in-code constants written at the time of each incident. Projected — no time-tracking study was run, so the triage figure is an informed estimate from the schedule and scope of the four domains. Model usage is covered by an existing subscription rather than metered per run, so the cost is a band inferred from the stack’s tiers, not an invoice.
07Timeline
- 2026-04Repository begins. The earliest work is an unrelated app skeleton that was later shelved.
- 2026-05The first-generation always-on system is deliberately retired — gateway, hosted dashboard, message processor and its tables all removed after it kept pushing false alerts to a phone.
- 2026-07The steward reframe is written down, then shipped: the observation database, the approval interface and the first watcher, with the remaining three domains following within days.
- 2026-07The whole thing moves from a local machine to a cloud cockpit in one day, thirty-two commits.
- 2026-08A security audit closes two live data-exposure holes and adds an automated guard; the silent-outage class of bug is fixed with a watchdog and a missed-slot detector.
08Looking back
What broke
A hung agent looks exactly like a healthy one. When all four watchers hit a usage cap at the same time, each process blocked waiting for the cap to reset instead of exiting, and because the scheduler will not start a second instance of a job that is still running, the entire steward went dark for roughly forty-four hours with silent logs. Turn limits did not help: they bound turns, not wall-clock time. Before that, the scheduler itself lied — interval-based jobs simply never fired on that machine while calendar-based ones on the same box fired reliably, and two watchers recorded zero runs for two days with no error anywhere. The audit found a third: a database view running under default ownership bypassed row-level security on its base table entirely, and a default-privileges rule was quietly granting broad access on every new object.
What it changed
The pattern is that absence of an error is not evidence of health. The outage detector now counts missed scheduled slots rather than measuring time since the last success, which is the only version of the check that can notice nothing happening. It also settled two product rules that are not preferences: the system never places a trade or moves money, and it never chases a client for payment. Both are relationship or risk decisions, and both are enforced by the agent simply not having the capability, not by an instruction telling it to behave.
Where it stands
Live and running unattended. Four watcher domains on schedule, the cockpit deployed and auth-gated, one user, no revenue. The most recent work is on financial reconciliation accuracy rather than new domains, and the honest next step is a second account holder — which is where the assumptions get tested instead of restated.