cron: gate the default inbox-processor on unread messages - #290
Open
serxa wants to merge 2 commits into
Open
Conversation
The `messages` run gate required an explicit `sources` list, so the
built-in inbox-processor shipped ungated: it woke every 30 minutes even
with an empty inbox, and each idle poll still pays to re-prime the prompt
cache (the poll interval is far longer than the cache TTL).
Make `sources` optional. With none given, the gate fires when any source
the consumer already tracks has unread messages — a read-only check that
never initializes or advances a cursor. Ship the default inbox-processor
with `run_if: [{type: messages, consumer: inbox}]` so idle polls are
skipped out of the box, without hard-coding the user's connected sources.
Explicit-source gates and the legacy `skip_when_idle` shorthand are
unchanged.
The no-sources messages gate used list_consumer_cursors, which hides cursors past their 2-day TTL. A gated inbox that goes quiet long enough for its cursors to expire would then read as "nothing unread" even when new mail arrives — the job would wedge shut and never self-recover. Add a read-only, expiry-agnostic db.consumer_has_unread(consumer) and use it for the any-source check. It compares each tracked source's max rowid against the stored cursor even when expired, and never initializes or advances a cursor. Sources with no cursor row are still ignored (new consumers see only future messages).
serxa
marked this pull request as ready for review
August 9, 2026 14:47
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.
What
The
messagesrun gate required an explicitsourceslist, so the built-in inbox-processor shipped ungated — it woke every 30 minutes even when the inbox was empty. Each idle wake still spawns an agent and pays to re-prime the prompt cache (the poll interval is far longer than the cache TTL), so a large share of the cron's cost buys nothing.This generalizes the gate and turns it on by default:
messagesgatesourcesis now optional. With nosources, the gate fires when any source the consumer already tracks has unread messages, via a new read-only, expiry-agnosticdb.consumer_has_unread(consumer)— it compares each tracked source's max rowid against the stored cursor (even one past its TTL) and never initializes or advances a cursor.run_if: [{type: messages, consumer: inbox}]. Idle polls are skipped (no agent spawned) without hard-coding the particular set of connected sources, so it works for every user regardless of which sources they've enabled — the same waytask-planneralready ships atasksgate.Behavior / compatibility
sources: [gmail, github]) and the legacyskip_when_idleshorthand are unchanged. An empty/omittedskip_when_idlestill means "don't gate" (a gate is synthesized only when it's non-empty).list_consumer_cursorshides cursors past their 2-day TTL, so gating on it would let a long-quiet inbox wedge shut once its cursors expire;consumer_has_unreadavoids that.Testing
get_consumer_cursornorset_consumer_cursoris called);from_config/describe/ empty-list semantics; default inbox-processor ships the generic (source-less) gate and it builds.consumer_has_unread: true when behind, false when caught up, ignores an untracked source, and still reports a backlog when the cursor has expired (whilelist_consumer_cursorsreturns none).3029 passed(the single failure is a pre-existing, unrelatedtest_xmemory_bridgetest).