What changed, and why
Each W6 monitor activity used to recompute the budget period (YYYY-MM) from its own wall-clock. So a single monitor pass that straddled UTC midnight could hand the alert thread, the dedupe ledger key, and the W7 offer id three different periods — re-forking the overspend conversation, the same orphaning #18 fixed, reached by another route. It is unreachable at the current 13:00 UTC schedule, but it is a latent footgun and cuts against SPEC §0.5 (decide clocks in the workflow, via workflow.now()).
The fix is small: read the clock once in the workflow and thread that one period into every activity, so they can never disagree.
The workflow decides the period, once
run reads workflow.now() a single time, derives the period from it (and the month-position clock too), then passes that period into each activity it calls. workflow.now() is the replay-safe clock, so the period is deterministic and identical for every step in the pass.
One now → one period (and the clock), decided in the workflow.
src/ynab_agent/workflow/monitor_workflow.py · 102 lines
⋯ 39 lines hidden (lines 1–39)
⋯ 49 lines hidden (lines 54–102)
The same period flows into load, send, save, and the W7 offer.
src/ynab_agent/workflow/monitor_workflow.py · 102 lines
⋯ 59 lines hidden (lines 1–59)
The activities take the period as input
Each activity now accepts period instead of reading the clock for it. The _period_of helper and the per-activity datetime.now() reads are gone; only the cosmetic "days left" still reads the wall-clock at send time. Shown here on load_prior_alert; send_overspend_alert, save_alert, and start_balance_offer change the same way.
period is a parameter; the ledger query uses it verbatim.
src/ynab_agent/workflow/monitor_activities.py · 158 lines
⋯ 51 lines hidden (lines 1–51)
⋯ 74 lines hidden (lines 85–158)
Test: one period reaches every activity
The monitor test captures the period each of the four activities received and asserts a single, well-formed value reached all of them — the discriminating guard for "one period per pass." On the old code these were four independent wall-clock reads; the assertion pins them to one.
len == 4, one distinct value, matches YYYY-MM.