Skip to content

fix(gateway): suppress lifecycle status emails on email platform - #12610

Open
ibrahimokdadov wants to merge 3 commits into
NousResearch:mainfrom
ibrahimokdadov:fix/email-gateway-suppress-status-emails
Open

fix(gateway): suppress lifecycle status emails on email platform#12610
ibrahimokdadov wants to merge 3 commits into
NousResearch:mainfrom
ibrahimokdadov:fix/email-gateway-suppress-status-emails

Conversation

@ibrahimokdadov

Copy link
Copy Markdown

Summary

  • The gateway's _status_callback_sync sends every lifecycle event (rate limit retries, fallback attempts, error messages) through adapter.send()
  • On chat platforms (Discord, Telegram, Slack) these are quick inline messages that get edited or disappear
  • On email, each status update becomes a separate email, flooding the user's inbox with internal retry noise like:
    • ⏱️ Rate limit reached. Waiting 2.73s before retry (attempt 2/3)...
    • ⚠️ Max retries (3) exhausted — trying fallback...
    • ❌ Rate limited after 3 retries — HTTP 429: ...
  • Fix: skip lifecycle events for the email adapter and log them instead

Change

gateway/run.py_status_callback_sync():

if source.platform == Platform.EMAIL and event_type == "lifecycle":
    logger.info("[Email] status (%s): %s", event_type, message)
    return

Test plan

  • Trigger a rate limit error while using the email gateway → verify no status emails are sent
  • Verify the lifecycle events still appear in the gateway log
  • Verify status messages still work normally on Discord/Telegram/other platforms

On startup, the email adapter marks ALL inbox UIDs as seen,
causing unread emails received while the machine was off to be
silently ignored. Change the IMAP search from ALL to SEEN so
only already-read messages are skipped, allowing unread emails
from allowed users to be processed on the next poll cycle.
The email adapter only tracked processed UIDs in an in-memory set
but never marked them as SEEN on the IMAP server. This caused
emails to reappear as UNSEEN on subsequent poll cycles, resulting
in duplicate replies being sent for a single email.
The status_callback sends every lifecycle event (rate limit retries,
fallback attempts, error messages) through adapter.send(). On chat
platforms these are quick inline messages, but on email each one
becomes a separate email, flooding the user's inbox with internal
retry noise. Skip lifecycle events for the email adapter and log
them instead.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for identifying the inbox-noise problem. The exact Email lifecycle rule is still not present on current main, although current main now selectively suppresses retry/fallback noise through gateway/run.py:440 and gateway/run.py:17812.

Problems

  • gateway/platforms/email.py:363 marks a message \\Seen before the fetch at line 365. A failed fetch is then skipped after restart because this PR initializes its seen set from server-side SEEN messages. Mark it seen only after successful retrieval and handoff.
  • Please add a regression test for the Email lifecycle behavior. Current main's shared filter suppresses only selected noisy strings; run_agent.py:875 still emits all statuses as lifecycle.
  • The PR includes two unrelated IMAP behavior changes alongside the lifecycle fix. They should be split for independent review.

Suggested changes

  • Port the lifecycle policy to current main's centralized callback/filter path (gateway/run.py:17812) and test it with Platform.EMAIL.
  • Move the server-side \\Seen update after successful processing.

Automated hermes-sweeper review.


# Mark as read on the server so it won't reappear
# as UNSEEN on the next poll or after a restart.
imap.uid("store", uid, "+FLAGS", "\\Seen")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This marks the UID \\Seen before the fetch at line 365. If that fetch fails, the message is skipped after restart because connect() now seeds _seen_uids from all server-side SEEN messages. Mark it seen only after successful fetch and handoff.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/email Email (IMAP/SMTP) adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants