Running a part
A part is a command that prints the capture contract. millpond starts it, reads the lines it prints and banks the files it writes, and Flywheel’s tick later starts it the same way (215, 231). The contract is the one way captures reach an inbox, and millpond’s book describes it whole (the capture contract, and running adapters).
How a host starts a part
eddy’s Flywheel plugin declares each adapter part: its command, the shape of its configuration and the secrets it needs (228). millpond reads the same declaration Flywheel reads, and runs the part as
eddy capture wispr --raw-dir <dir> [--cursor <c>]
with the part’s configuration in the file the CAPTURE_CONFIG
environment variable names, and each declared secret in the environment
under its declared name. A part that signs in to its service keeps that
sign-in itself (A part per service).
millpond starts a part in one of two ways:
millpond run <part>makes a fresh raw directory, starts the part with that directory and the part’s last cursor, and lands what the part prints. It then keeps the new cursor and the run’s status: its exit, its failures and the tail of its standard error.millpond tickruns every part of an inbox whose schedule is due, so one timer on a host serves every part there. A schedule is five-field cron, and a run missed while the host slept is caught up once (231).
An inbox’s settings name the one host whose tick runs each part. An inbox checked out on two hosts therefore runs a meeting part once, on the host that holds its sign-in.
What a part prints
A part writes JSON Lines to standard output, and each line holds
exactly one of three keys: capture, cursor or done.
A run of the wispr part that lists three meetings, lands two and
fails to fetch the third prints four lines:
{"capture": {"key": "meeting/wispr/m42", "source": "meeting", "event_at": "2026-10-16T15:00:00Z", "title": "Inbox planning", "raw": ["m42/transcript.json", "m42/turns.txt"]}}
{"capture": {"key": "meeting/wispr/m43", "source": "meeting", "event_at": "2026-10-16T17:30:00Z", "title": "Release review", "raw": ["m43/transcript.json", "m43/turns.txt"]}}
{"cursor": "2026-10-17T04:30:05Z"}
{"done": {"listed": 3, "failed": [{"what": "meeting/wispr/m44", "why": "HTTP 502"}]}}
A meeting capture fills the contract’s fields this way:
| Field | A meeting part writes |
|---|---|
key | meeting/<service>/<meeting id> |
source | meeting |
event_at | when the meeting started |
title | the meeting’s title |
url | where the meeting lives on its service, when the service gives it an address |
raw | the transcript as served and the plain copy, as paths under the raw directory |
excerpt | never present |
A meeting capture never carries an excerpt. A meeting is not its own excerpt: what it says becomes signals only through a reader’s judgment (115). millpond therefore writes no signal for it and marks it unread, which charges a reader.
The cursor line is the part’s own and opaque to millpond. The done
line ends a complete run with its counts and the meetings that failed.
A run that fails
One failed meeting never stops the others. A part names it in the
done line’s failed list, with what failed and why, and carries on
with the next meeting.
A run that exits without done, or with a status other than 0, keeps
every capture already taken, since captures are idempotent. Its cursor
does not advance, and millpond records the run as failed. The next run
lists the same window again from the old cursor, and millpond passes
over every key it already holds.
What millpond does with each line
millpond checks each line and refuses one with a bad key, a missing
field or an empty excerpt, naming it in the run. It looks the key up in
every capture the inbox holds and passes over a key it already holds.
It banks the raw files, writes the capture with its pointer, marks the
capture unread, and records the run, which a fleet’s doctor reads from
millpond status --json. millpond’s book holds each of those steps
(landing a capture).