ADR 002: Cloudflare Worker replaces launchd for evening check-in¶
Status: accepted Date: 2026-05-04 Supersedes: —
Context¶
The evening check-in flow originally lived on Casey's laptop:
evening_checkin.py+ launchdcom.casey.warehouse-checkinat 8pm — read intervals.icu for today's planned events, checkcompletion_log.jsonlfor what's logged, send Telegram if gaps existtelegram_listener.py+ launchdcom.casey.warehouse-listenerat 8:30pm/9:30pm/6:30am (next day) — poll Telegram for replies, parse via Anthropic, write to completion_log- GitHub Actions
evening-checkin.yml— laptop-independence stopgap that ran a stripped-down evening_checkin.py from CI when the Mac was asleep
Three problems with this:
- Mac-dependence — laptop closed at 8pm = no check-in. The GHA stopgap fired but couldn't read Casey's local completion_log, so it asked about everything every time
- Polling — telegram_listener did 3 launchd-fired polls instead of 1 push-based webhook. Wasteful + delayed
- Three places for one feature — evening_checkin.py + telegram_listener.py + GHA workflow all had pieces of the truth
Decision¶
Replace all three with one Cloudflare Worker (otq-checkin),
using:
- Durable Object for state (
checkins+processed_replies+daily_summariesSQL tables) - Worker cron for scheduled fires (
0 3 * * *UTC = 8pm PDT, later30 15 * * *for morning summary) - Webhook mode for Telegram replies (push, not poll)
- R2 for shared state with the laptop (completion_log_worker_* files merged on next daily_sync)
Code at ~/garmin-warehouse/cloudflare/otq-checkin/. Live at
https://otq-checkin.manoscasey.workers.dev.
Consequences¶
Good:
- No Mac-dependence — fires at 8pm regardless of laptop state
- Push webhook = instant reply parsing (vs polling 8:30/9:30/6:30)
- One codebase to maintain (TypeScript + Anthropic SDK)
- DO state survives restarts
- Worker observability (CF dashboard) gives live tail + cron history
- Once verified working, retires evening_checkin.py +
telegram_listener.py + GHA workflow (all 3 tagged .disabled
after teardown)
Tradeoffs:
- Requires a Cloudflare account with Workers + DO billing
- Wrangler OAuth token rotates ~6mo (re-login needed periodically)
- DO SQL is sqlite-flavored, slightly different from DuckDB used elsewhere
- Anthropic SDK in TypeScript doesn't have .parse(); uses
tool_use with Zod for structured output
- Local laptop still has fallback launchd plists during shadow mode
(~1 week) — cloudflare/otq-checkin/teardown-laptop.sh retires
them after verified clean
Shadow-mode protocol¶
Both paths run in parallel for ~1 week. Both write to
completion_log.jsonl (the laptop side) and
state/<date>/completion_log_worker_<msgId>.jsonl (Worker side).
daily_sync.sh merges Worker-captured replies into the local log
with dedup. As long as both sides agree on what got logged each
day for ~7 days, the laptop side is safe to retire.
Alternatives considered¶
- Keep laptop-based but improve reliability — doesn't solve Mac-dependence. Pmset wakeorpoweron helps but isn't bulletproof.
- Move to a cheap VPS — works but $5/mo + more infra to maintain than CF Worker. CF Worker is ~$0/mo at this scale.
- AWS Lambda + DynamoDB — works but Casey doesn't have an AWS account in heavy use; CF account is already load-bearing.
- Inngest / Trigger.dev for the cron + webhook — adds a third-party service for what's a simple Worker.
CF Worker won on: already-have-account, lowest cost, simplest mental model (one codebase, one platform), best DX with MCPs configured.
Notes¶
The 8:30am morning summary + 2-way reply (corrections / questions / chat) was added to the same Worker on the same night (2026-05-04). That feature didn't replace anything — it's net new. But it benefits from the same Worker plumbing (DO state, R2 cache, webhook router).
References¶
- Spec:
~/garmin-warehouse/docs/otq-checkin-agent-spec.md - System:
systems/otq-checkin-worker.md - Runbook:
runbooks/worker-deploy.md - Cron table:
reference/cron-schedules.md