Skip to content

Make the daily pipeline resilient to Grain rate limits and Notion config drift - #9

Draft
dplakon wants to merge 1 commit into
mainfrom
fix/grain-rate-limits-and-notion-config
Draft

Make the daily pipeline resilient to Grain rate limits and Notion config drift#9
dplakon wants to merge 1 commit into
mainfrom
fix/grain-rate-limits-and-notion-config

Conversation

@dplakon

@dplakon dplakon commented Aug 1, 2026

Copy link
Copy Markdown

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 today failed twice with HTTP Error 429: Too Many Requests. Two compounding causes:

  • It called 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:

  • All Grain requests now throttle between calls and retry on 429 with exponential backoff, honoring the retry-after header. Tunable via GRAINIAC_GRAIN_MIN_INTERVAL (default 2.5) and GRAINIAC_GRAIN_MAX_RETRIES (default 6).
  • 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.
  • Refactored the transcript fetch onto the same shared request path, so it gets throttling and retries too (previously it had its own bare urlopen).

2. Hardcoded UTC offsets could silently process the wrong day (latent)

The timezone table mapped America/Los_Angeles to -8 year-round, with the comment # PST (close enough; PDT is -7). It is currently -7 under 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 hardcoded Company default or a stale GRAINIAC_NOTION_TITLE_PROPERTY, producing an opaque validation error.

Changes:

  • New 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.
  • A 404 on a database path now explains the two real causes — wrong ID vs. database not shared with the integration — and shows how to list databases the token can actually reach.
  • Updated the orchestrator skill so future runs check /search and confirm a candidate database by its page bodies matching references/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_ID is set to 3a043263616d803d98de000c5b916204, which 404s as both a database and a page. The real Account Tracking database is 3a043263-616d-80bd-a6da-f099fa90796a.
  • GRAINIAC_NOTION_TITLE_PROPERTY is set to Name, but the database's title property is Account 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).
  • Bounded pagination issues 1 request where the unbounded path issues 30 (fake-paged unit check).
  • DST check: resolver returns the correct date across 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:00 the old table assumed.
  • Title resolution recovers from the wrong Name env var and resolves Account name; find_company_page locates 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, -7 and -8 happen 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.

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant