Make the daily pipeline resilient to Grain rate limits and Notion config drift - #9
Draft
dplakon wants to merge 1 commit into
Draft
Make the daily pipeline resilient to Grain rate limits and Notion config drift#9dplakon wants to merge 1 commit into
dplakon wants to merge 1 commit into
Conversation
…fig drift
Today's run surfaced three failures. Fixes for each:
Grain 429s aborted the orchestrator. `fetch_daily_meetings.py` walked the
entire recording history (up to 100 pages) to find one day of meetings, and
`grain_client._get` had no retry logic, so the first HTTP 429 killed the run.
The API allows ~30 requests per window and every child agent calls it
concurrently.
- Add throttling plus retry with exponential backoff on 429 (honoring
`retry-after`) to all Grain requests, tunable via
GRAINIAC_GRAIN_MIN_INTERVAL / GRAINIAC_GRAIN_MAX_RETRIES.
- Add `list_all_recordings(stop_before_date=...)`, which stops paginating once
a page predates the target day. Recordings are returned newest-first, so
fetching one day now costs 1 list request instead of up to 100.
Hardcoded UTC offsets could resolve the wrong date. The offset table mapped
Pacific to -8 year-round with a "close enough" note, but it is -7 under DST.
An hour of error flips the resolved calendar date for runs near local midnight,
which would silently process the wrong day's meetings. Use stdlib `zoneinfo`
so DST is applied correctly, accept any IANA name, and warn and fall back
instead of silently using -8 for an unrecognized zone.
Notion title-property mismatches failed confusingly. The title property name
varies per database ('Name', 'Company', 'Account name', ...), and a stale
GRAINIAC_NOTION_TITLE_PROPERTY produced an opaque validation error.
- Add `resolve_title_property()` to read the real name from the database
schema, cached per database. An explicitly configured name that disagrees
now warns and the schema wins; unset auto-detects silently.
- Make the 404 on a database path explain the two real causes (wrong ID vs.
database not shared with the integration) and how to list reachable
databases.
Verified against the live Grain and Notion APIs: the previously failing
`fetch_daily_meetings.py today` now succeeds and returns the same 6 meetings,
bounded pagination issues 1 request where the old path issued 30, and title
resolution correctly recovers from a wrong env var.
Note for operators: GRAINIAC_NOTION_DATABASE_ID and
GRAINIAC_NOTION_TITLE_PROPERTY are currently misconfigured as team secrets.
These changes surface that clearly but cannot fix the secret values.
Co-Authored-By: Oz <oz-agent@warp.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Today's daily run (2026-07-31, 6 customer meetings) hit three separate failures. All 6 meetings were ultimately filed to Notion, but only after manual workarounds. This fixes the underlying causes so the scheduled run doesn't fail the same way tomorrow.
What broke, and what this changes
1. Grain rate limiting aborted the orchestrator (hard failure)
scripts/fetch_daily_meetings.py todayfailed twice withHTTP Error 429: Too Many Requests. Two compounding causes:list_all_recordings()with no bound, walking the entire recording history (up to 100 pages) just to find one day of meetings — even though the API returns recordings newest-first and the target day was on page 1.grain_client._get()had no retry logic, so the first 429 raised and killed the run.The API budget is ~30 requests per window (
x-ratelimit-limit: 30), and the orchestrator fans out one child agent per meeting, all calling Grain concurrently.Changes:
retry-afterheader. Tunable viaGRAINIAC_GRAIN_MIN_INTERVAL(default2.5) andGRAINIAC_GRAIN_MAX_RETRIES(default6).list_all_recordings(stop_before_date="YYYY-MM-DD")stops paginating once a page predates the target day. Fetching one day now costs 1 list request instead of up to 100.urlopen).2. Hardcoded UTC offsets could silently process the wrong day (latent)
The timezone table mapped
America/Los_Angelesto-8year-round, with the comment# PST (close enough; PDT is -7). It is currently-7under DST. Being an hour off flips the resolved calendar date for runs near local midnight, which would silently process the wrong day's meetings.Replaced with stdlib
zoneinfo, which applies the correct DST offset for the current date and accepts any IANA zone name. An unrecognized zone now warns and falls back explicitly instead of silently becoming-8.Note this raises the floor from Python 3.8 to 3.9 (
zoneinfo); the README claim has been updated to match.3. Notion title-property mismatch failed confusingly
Every Notion database has exactly one title property, but its name varies (
Name,Company,Account name, ...). The client trusted a hardcodedCompanydefault or a staleGRAINIAC_NOTION_TITLE_PROPERTY, producing an opaque validation error.Changes:
resolve_title_property()reads the actual name from the database schema, cached per database. Unset config auto-detects silently; an explicitly configured name that disagrees warns and the schema wins./searchand confirm a candidate database by its page bodies matchingreferences/notion-template.md, rather than judging by the database's columns.Operator action still required (not fixable in code)
Two team secrets are currently misconfigured. These changes surface both clearly but cannot correct the values:
GRAINIAC_NOTION_DATABASE_IDis set to3a043263616d803d98de000c5b916204, which 404s as both a database and a page. The real Account Tracking database is3a043263-616d-80bd-a6da-f099fa90796a.GRAINIAC_NOTION_TITLE_PROPERTYis set toName, but the database's title property isAccount name. With this PR it can simply be unset.Verification
Against the live Grain and Notion APIs:
python3 scripts/fetch_daily_meetings.py today— previously failed with 429, now exits 0 and returns the same 6 meetings as the manual workaround (identical recording ID sets).America/Los_Angeles,America/New_York,UTC,Europe/Zurich, and warns+falls back on a bogus zone. Confirmed the live Pacific offset is-7:00, not the-8:00the old table assumed.Nameenv var and resolvesAccount name;find_company_pagelocates existing pages with the env var unset.One caveat worth flagging for review: the fix for #2 is a latent-bug fix, not a reproduction of today's failure. With the current
0 5 * * *cron,-7and-8happen to resolve to the same date; the bug bites for runs in the hour around local midnight.Conversation: https://app.warp.dev/conversation/007331e2-9617-4eb0-903b-cdfcabcaee53
Run: https://oz.warp.dev/runs/019fbbb1-8aa1-79c5-9fbd-78ec2b9c3a4b
This PR was generated with Oz.