Skip to content

fix(email): split long cron output instead of truncating at 4,000 chars - #79842

Closed
troyhoffman-oss wants to merge 1 commit into
NousResearch:mainfrom
troyhoffman-oss:fix/email-split-long-cron-output
Closed

fix(email): split long cron output instead of truncating at 4,000 chars#79842
troyhoffman-oss wants to merge 1 commit into
NousResearch:mainfrom
troyhoffman-oss:fix/email-split-long-cron-output

Conversation

@troyhoffman-oss

Copy link
Copy Markdown

What & why

EmailAdapter never declares splits_long_messages, so gateway/delivery.py treats email as a non-chunking platform and applies MAX_PLATFORM_OUTPUT = 4000 (delivery.py:29, 488, 503). That cap is sized for Telegram's 4096-char API limit. Email has no such constraint — the adapter already defines MAX_MESSAGE_LENGTH = 50_000 (Gmail-safe body size, adapter.py:110) but never uses it.

Result: any scheduled-job output over 4,000 chars is silently cut before it reaches the mailbox, with no way to opt out (#61990).

Observed data loss. A 5,199-char cron memo was truncated to the 4,000-char envelope, severing an escalation bullet, a deadline statement, and a whole closing section. Nothing in the delivered mail indicated content was missing.

Why this isn't a duplicate of #62000 / #68900

Those PRs set splits_long_messages = True and stop there. That flag is a promise to the router that the adapter chunks — but upstream send() does no chunking at all:

async def send(self, chat_id, content, reply_to=None, metadata=None):
    """Send an email reply to the given address."""
    message_id = await loop.run_in_executor(None, self._send_email, chat_id, content, reply_to)

Setting the flag alone converts a 4,000-char truncation into an unbounded body handed to SMTP, past the 50,000-char limit the adapter itself declares. This PR implements the chunking the flag advertises — which is precisely why MAX_MESSAGE_LENGTH stops being dead code.

How it works

send() splits at MAX_MESSAGE_LENGTH via the shared truncate_message() helper — the same pattern every peer adapter uses (Slack, Discord, Telegram, Mattermost, Matrix, Teams, Feishu) — and the class declares splits_long_messages = True so the router hands over the full payload.

No shared code changed. The router's cap stays correct for platforms that genuinely have one; behavior for every other adapter is untouched.

How to test

pytest tests/gateway/test_email.py tests/gateway/test_delivery.py -v

4 new regression cases in tests/gateway/test_email.py:

Test Asserts
test_declares_native_chunking the flag is set, so the router skips its cap
test_send_delivers_long_content_whole 6,000 chars arrives as one intact email
test_send_splits_above_body_limit beyond 50,000 chars, splits across emails; every character survives
test_router_does_not_truncate_email_delivery end-to-end through the real DeliveryRouter: oversized payload arrives byte-identical, audit copy still written

Manual reproduction: configure an email platform, schedule a cron job whose output exceeds 4,000 chars, compare the delivered body against ~/.hermes/cron/output/<job_id>_*.txt (the audit copy is written whole — it is the delivered mail that loses the tail).

Platforms tested

Linux (Ubuntu 24.04, Python 3.11.15). No OS-specific code paths touched — the change is adapter-level string handling plus one class attribute.

Fixes #61990

The email adapter never declared `splits_long_messages`, so the delivery
router (gateway/delivery.py) treats it as a non-chunking platform and
applies MAX_PLATFORM_OUTPUT = 4000 — a cap sized for Telegram's 4096-char
API limit. Email has no such constraint: the adapter already defines
MAX_MESSAGE_LENGTH = 50_000 (Gmail-safe body size) but never uses it.

Observed: a 5,199-char scheduled-job memo was cut to the 4,000-char
envelope, severing an escalation bullet, a deadline statement, and a
whole closing section before it reached the mailbox. There is no config
knob to opt out (issue NousResearch#61990).

send() now splits at MAX_MESSAGE_LENGTH via the shared truncate_message()
helper — the same pattern every peer adapter uses (Slack, Discord,
Telegram, Mattermost, Matrix, Teams, Feishu) — and the class declares
splits_long_messages = True so the router hands over the full payload.

Note on scope vs. the existing open PRs for this bug (NousResearch#62000, NousResearch#68900):
declaring splits_long_messages alone is not sufficient. That flag is a
promise to the router that the adapter chunks; upstream send() does no
chunking, so the flag by itself converts a 4,000-char truncation into an
unbounded body handed to SMTP. This change implements the chunking the
flag advertises, which is why MAX_MESSAGE_LENGTH stops being dead code.

No shared code changed: the router's cap stays correct for platforms that
genuinely have one.

Tests: 4 regression cases in tests/gateway/test_email.py, including an
end-to-end pass through the real DeliveryRouter asserting an oversized
payload arrives byte-identical with the audit copy still written.

Tested on: Linux (Ubuntu 24.04, Python 3.11).

Fixes NousResearch#61990
@troyhoffman-oss
troyhoffman-oss force-pushed the fix/email-split-long-cron-output branch from 21bcd72 to d680fa3 Compare August 6, 2026 02:40
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins platform/email Email (IMAP/SMTP) adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 6, 2026
@troyhoffman-oss

Copy link
Copy Markdown
Author

Withdrawing — submitted in error against operator instruction. The analysis stands if a maintainer wants it: the 3 duplicate PRs set splits_long_messages=True without implementing chunking in send(), converting truncation into an unbounded SMTP body; this branch implements the chunking the flag advertises.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/email Email (IMAP/SMTP) adapter 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.

[Bug]: No way to override email delivery truncation

2 participants