EP 15

Looking Out for You Before You Ask


August 2026·6 min read·#proactive#cron#push

The first of the two things Pepper has to do, from EP 01:

"Proactively look out for us. Before I even ask."

If the Vault (EP 14) was half of "do what I ask," this is the other half — the side that takes care of things without being asked.


Brief — what arrives first in the morning

What the Brief tab shows in the morning:

  • Weather — today.
  • Calendar — all connected accounts, merged. primary and secondary queried in parallel, deduped by iCalUID.
  • Gmail analysis — this is the core. Not just "N emails," but the ones that actually need a reply, filtered by an LLM.

In EP 01 I wrote: "Of those emails, which ones actually require a reply and an action?" What implements that is Priority Senders + an LLM filter. You register the senders who matter (CRUD), their mail gets prioritized, and the LLM screens for "this needs action."

Generating the briefing fresh every time is slow and expensive. So it's cached as a 6 a.m. KST snapshot, and pull-to-refresh forces a regeneration.


But you still have to open the tab

Here I hit the real wall of "proactive." However good the briefing is, you have to open the app and go to the Brief tab to see it. That's not being looked after — that's still me going to it.

So I built two daily pushes. Weekdays only.

  • 9 a.m. — ☀️ Your briefing has arrived → opens the Brief tab
  • 2 p.m. — ✅ Action item check → opens the Action tab

This is EP 01's "instead of asking — to be taken care of" in the form of a notification that actually lands on the phone at 9 a.m. every morning.


What to run the scheduler on — this was the real decision

To send two pushes on time, you need a scheduler. Naturally I reached for Vercel Cron, since the API already lives on Vercel.

Blocked. For two reasons.

  1. Vercel Hobby allows at most 2 crons. Both were already used by drive-sync (EP 14) and chat-cleanup. No room.
  2. Vercel Hobby crons are best-effort. No guarantee it fires at exactly 9:00. A briefing that arrives at 9:40 isn't a briefing.

So I went with Supabase pg_cron. The architecture ended up like this:

pg_cron (inside Postgres, fires on time)
  → calls a Vercel route via pg_net (CRON_SECRET header)
  → the route reads all push_tokens
  → sends a batch of Expo pushes

The sending logic itself stays on Vercel (apps/api); pg_cron only pulls the trigger. Cron expressions are in UTC, so 0 0 * * 1-5 (9 a.m. KST), 0 5 * * 1-5 (2 p.m. KST). The secret is read from Supabase Vault, never hardcoded into the SQL file.

I had no idea "where you keep time" would be this big a decision. For a non-developer, "just send it at 9" is one line — but in practice it was an architecture choice tangled up with punctuality, quota limits, and secret management.


And the cold-start deep link bug

The pushes went out fine. But tapping them didn't move to the tab.

The strange part: chat pushes navigated to the room just fine when tapped. Only the briefing and action pushes failed to switch tabs.

The difference was app state. Chat pushes usually arrive while you're using the app, but the briefing arrives when the app is fully killed (9 a.m.). In other words, only the cold-start path was broken.

The cause was an absurd unit mismatch.

iOS notification.date  →  seconds (timeIntervalSince1970)
Date.now()             →  milliseconds

There's a 2-minute recency guard on cold start that decides "did this push just arrive?" — and subtracting seconds from milliseconds directly always exceeded 2 minutes. So it always ignored the deep link.

const ts = raw < 1e12 ? raw * 1000 : raw  // convert seconds to ms

One line. Fixing it fixed the chat-room cold-start deep link too.


When Google announced Information Agents (24/7 background agents) in EP 04, I was thrilled that it matched the monitoring/proactive-worker structure I'd designed that same morning — "the direction is right." The direction was right. But building it, the hard part turned out not to be the agent concept — it was scheduler punctuality, and one line about seconds versus milliseconds.

Being proactive isn't a concept. It's a notification that shows up at exactly 9 a.m.