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.

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.

From the room to the journal Lifetail's daemon. Audio comes from the highest-priority sound source that is streaming, through pacat. A local voice activity detector cuts it into utterances with a threshold, a hangover, a pre-roll and a maximum length; only speech goes further. Each utterance waits in a spool on disk until it is transcribed, first over a perpetual realtime session (a ladder of models tried in order), else by a batch ladder. Transcribed utterances are appended to a journal of one JSON line per utterance, per day, never pruned, and the speech audio is archived as 16 kb/s Opus. An HTTP and WebSocket API serves the journal to followers, searches and the MCP server. sourcesphone · laptop · …in priority order local VADthreshold · hangoverpre-roll · ≤ 30 s spooluntil transcribed realtime laddera perpetual sessionrung 1 → rung 2 batch ladderper utterance journalJSONL per day · seq archive16 kb/s Opus, nice 19 HTTP · WebSocketfollow · search · statusMCP pacat speech realtime down
A spool between hearing and writing. An utterance is on disk from the moment it ends until it is in the journal.
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.

Which microphone is listening Source failover over five minutes. The phone, the first-priority source, is capturing. It stalls, because nothing arrives within the stall timeout, and capture moves at once to the laptop, the next source; the phone backs off. The phone is probed periodically; once it has delivered audio for the settle time, thirty seconds, it pre-empts the laptop make-before-break, so capture never drops in between. 0:00 0:30 1:00 1:30 2:00 2:30 3:00 3:30 4:00 4:30 5:00 phone priority 1 laptop priority 2 capturing backing off · probed settling 30 s capturing again capturing stall: nothing for stall-timeout make-before-break
Make before break. The better source must prove itself for thirty seconds before the working one is dropped.

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.

Nothing said is lost What Lifetail does when transcription fails. An utterance goes to the current realtime rung. If the link drops, errors, is refused or stops acknowledging, it is discarded and a new one is opened, and every unfinished utterance is replayed to it in order. A rung that accepts sessions but fails two items in a row is demoted for ten minutes and the next rung takes over. If realtime has been down for a while, or an utterance has failed twice, the batch ladder transcribes it on its own. If everything is down, or the process crashes, utterances wait in the spool and are retried for ever, and recovered at the next start. A systemd watchdog restarts the service if any loop wedges. utterancein the spool realtime rungthe current model journalappended reconnectreplay unfinished, in order next rungafter 2 failed items: −10 min batch ladderrealtime down, or failed twice waiteverything down:retry for ever transcribed drop · 401 · no ack
Every arrow ends in the journal. The only question a failure decides is how long an utterance waits.
Other failures
Journal write failsretried with backoff; status reports journal writes failing
A connect or batch request wedgesabandoned after its timeout plus 30 s and retried
The Opus encoder wedgeskilled and retried; archiving never gates the watchdog
A capture, STT or journal loop wedgesthe systemd watchdog (60 s) restarts the unit, with no start limit
A crash leaves a torn final linemoved aside beside its day's file, never discarded
A follower's stream dies15 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.

RouteReturns
/v1/tail?n=the last n records
/v1/after?after=records past a sequence number
/v1/search?q=matches, paged backwards in time
/v1/streama WebSocket of records and partials
/v1/statussources, 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.