Architecture
One Python package, three independent lanes, two persistence surfaces
(EventKit + JSON state files). Everything funnels through daemon.sync_once on a 5-second tick.
Module map
Ownership boundaries
| Surface | Owner | Notes |
|---|---|---|
Beads: <project> bodies | Daemon (most) + user (notes) | <bb:meta> + <bb:desc> are daemon-owned. <bb:notes> is user-owned and preserved across syncs. |
Beads: Activity · ! Beads: Readme | Daemon | Drift is overwritten next cycle. |
Beads: Projects · Beads: Settings rows | Daemon writes / user toggles | Body+title overwritten on drift; completion is the only signal the daemon reads back. |
Voice: <slug> header + brief | Daemon | User edits are clobbered. User responses go in new reminders. |
Voice: <slug> responses | User (or voice agent on behalf of user) | Drained via rbridge mailbox read. |
Claude: Sessions · Codex: Sessions | User (request) / daemon (lifecycle) | User writes the prompt; daemon marks completed / appends output / appends agent turns. |
State files
State files solve one problem: EventKit reminder IDs are not stable across iCloud sync churn, so the daemon needs a side-table linking bead IDs to reminder IDs. The same pattern applies to in-flight captures, chat sessions, and voice mailboxes — each is a short-lived contract between "this process I spawned" and "the reminder that triggered it".
Why three lanes and not one big sync
The three lanes (Beads / Sessions / Voice) only share the EventKit adapter and the activity log. Their data models, lifecycles, and failure modes are all different — coupling them would force the simpler lanes to inherit the complexity of the most complex one.
- Beads is field-level reconciliation against an
external source of truth (the
bdCLI). Heavy on diffing, light on lifecycle. - Sessions is a job runner. Each reminder is a unit of work that either kicks off a process (interactive / capture) or drives a long-lived conversation (chat). Heavy on process management.
- Voice is a message bus. The daemon places one message (the brief), the human places replies (responses). Heavy on format conventions, light on logic.
The daemon's job is to call each lane in sequence per cycle and isolate
failures: a panic in sessions.poll never breaks reconcile_project.
What's not in the daemon
- HTTP server. No incoming requests; the CLI is the
only entry.
api.py+events.pyexist as an outgoing client for beads-kanban but are unused by the daemon today. - Web UI. Apple Reminders is the UI.
- Background queue / DB. JSON files + the EventKit store are the only persistence.
- Event channel from beads. No webhook, no LISTEN/ NOTIFY. The 5-second poll is the freshness budget.
For the per-cycle flow inside sync_once, see Sync cycle.