lifetail · Rust daemon · CLI · MCP · NixOS module
lifetail
lifetail is a daemon that transcribes what a microphone hears into a searchable journal, continuously. It listens through whichever microphone is available, keeps only the speech, transcribes it over a long-lived realtime session, and appends each utterance to the journal, which you can follow live from another machine, search, or give to an agent as a record of what was said.
remind me to water the basil before Friday
the build is green, ship it after lunch
okay so the meeting moved to three
and the slides are in the shared
Illustrative output; the format is RFC3339 UTC ⇥ text, one utterance per line.
Pipeline
The daemon is a set of supervised loops: capture, segmentation, transcription, journalling and archiving, each with its own failure handling, and a systemd watchdog over all of them. Voice activity detection runs locally, at about 34 ms of CPU per minute of audio, so only speech is sent upstream; the realtime socket stays open, kept alive with pings, whether or not anyone is talking.
lifetail -f # follow, from any host; reconnects and resumes
lifetail -n 20 # the last twenty
lifetail search -i 'basil' --since 3d # unbounded history
lifetail status # exit 3 if unhealthy or stale
lifetail mcp stdio # tail, search, after and status for agents
Choosing a microphone
Sources are a priority list, a phone's microphone first, a laptop's next, each on any sound server on the network. If the source in use refuses, exits, stalls or (optionally) goes digitally silent, capture moves to the next one immediately and the failed one backs off. A preferred source that comes back is probed and, after it has streamed steadily for the settle time, takes over without a gap.
Failure handling
Transcription is a ladder, not a single dependency. The realtime models are tried in order at every reconnect, sessions are rotated while idle before they get old, and a batch ladder catches what realtime can't. Every failure path ends in the spool, and the spool is retried for ever.
| Journal write fails | retried with backoff; status reports journal writes failing |
|---|---|
| A connect or batch request wedges | abandoned after its timeout plus 30 s and retried |
| The Opus encoder wedges | killed and retried; archiving never gates the watchdog |
| A capture, STT or journal loop wedges | the systemd watchdog (60 s) restarts the unit, with no start limit |
| A crash leaves a torn final line | moved aside beside its day's file, never discarded |
| A follower's stream dies | 15 s server pings, a 45 s client deadline, and a reconnect with after=<seq>: no gaps, no duplicates |
The journal
Every utterance is one JSON line, numbered with a sequence, with its speech start and end, its text, the source it came from and the model that transcribed it. Lines go into one file per day, and nothing is ever pruned. Each utterance's speech audio is kept beside it as 16 kb/s Opus, encoded at the lowest priority for about 0.4 seconds of CPU per minute of speech.
Only one daemon may write a journal; everyone else reads it through the API. Every route except health needs a bearer token.
| Route | Returns |
|---|---|
/v1/tail?n= | the last n records |
/v1/after?after= | records past a sequence number |
/v1/search?q= | matches, paged backwards in time |
/v1/stream | a WebSocket of records and partials |
/v1/status | sources, session, backlog, problems |
It shares its transcription providers, authentication and model names with stt, whose core it uses for realtime sessions, and it ships as a NixOS module that runs a Type=notify user unit with a watchdog and Restart=always.