Skip to content

cron: gate the default inbox-processor on unread messages - #290

Open
serxa wants to merge 2 commits into
mainfrom
serxa/gate-idle-inbox-by-default
Open

cron: gate the default inbox-processor on unread messages#290
serxa wants to merge 2 commits into
mainfrom
serxa/gate-idle-inbox-by-default

Conversation

@serxa

@serxa serxa commented Aug 9, 2026

Copy link
Copy Markdown
Member

What

The messages run gate required an explicit sources list, 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:

  • messages gate sources is now optional. With no sources, the gate fires when any source the consumer already tracks has unread messages, via a new read-only, expiry-agnostic db.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.
  • The default inbox-processor now ships 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 way task-planner already ships a tasks gate.

Behavior / compatibility

  • When the inbox has new messages, the job runs exactly as before — no message is ever skipped, only idle wakes.
  • Explicit-source gates (sources: [gmail, github]) and the legacy skip_when_idle shorthand are unchanged. An empty/omitted skip_when_idle still means "don't gate" (a gate is synthesized only when it's non-empty).
  • Expiry-agnostic by design: list_consumer_cursors hides cursors past their 2-day TTL, so gating on it would let a long-quiet inbox wedge shut once its cursors expire; consumer_has_unread avoids that.

Testing

  • Gate unit tests: no-sources gate satisfied iff the consumer has unread; the path is read-only (asserts neither get_consumer_cursor nor set_consumer_cursor is called); from_config / describe / empty-list semantics; default inbox-processor ships the generic (source-less) gate and it builds.
  • DB tests for consumer_has_unread: true when behind, false when caught up, ignores an untracked source, and still reports a backlog when the cursor has expired (while list_consumer_cursors returns none).
  • Full suite: 3029 passed (the single failure is a pre-existing, unrelated test_xmemory_bridge test).

serxa added 2 commits August 9, 2026 14:27
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
serxa marked this pull request as ready for review August 9, 2026 14:47
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