Skip to content

fix(gateway): don't mark an entire chat dead on thread/message-level not_found (salvage #55780) - #56225

Merged
kshitijk4poor merged 2 commits into
NousResearch:mainfrom
kshitijk4poor:salvage-55780
Jul 1, 2026
Merged

fix(gateway): don't mark an entire chat dead on thread/message-level not_found (salvage #55780)#56225
kshitijk4poor merged 2 commits into
NousResearch:mainfrom
kshitijk4poor:salvage-55780

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

Salvage of #55780 by @r266-tech (rebased onto current main + AUTHOR_MAP entry). Stops the gateway from marking an entire chat dead when a send fails with a thread/topic/message-level not_found — a deleted forum topic or an edited-away message must not black-hole every future delivery to the parent chat.

Original PR was 194 commits behind main; @r266-tech's commit is cherry-picked verbatim (authorship preserved). I added one AUTHOR_MAP chore commit under my identity so check-attribution resolves their noreply email.

The bug

classify_send_error collapses several distinct Telegram failures into a single "not_found" kind:

  • chat-levelchat not found (the chat/user/group itself is gone → truly dead)
  • thread/message-levelmessage thread not found, topic_deleted, message to edit/reply not found, message_id_invalid (the parent chat is still reachable)

The dead-target path treats any "not_found" as a dead target, so a single deleted forum topic or edited-away message marks the whole chat dead and short-circuits all future deliveries to it.

The fix

  • Add is_chat_level_not_found() in gateway/platforms/base.py, splitting the existing six not_found substrings into _CHAT_LEVEL_NOT_FOUND_SUBSTRINGS and _SUBCHAT_NOT_FOUND_SUBSTRINGS (byte-identical set to before — pure refactor of classify_send_error, no string added/dropped/re-bucketed).
  • Gate the dead-marking in gateway/delivery.py::_classify_dead_from_error_text: only a chat-level not_found marks a target dead; sub-chat not_found does not. When both markers are present, the sub-chat reading wins (conservative — never kill a chat that may still be reachable).

Review (this salvage)

Ran our review workflow (classifier-correctness / false-negative hunt) + my own trace. Clean, no findings.

  • No new false-negatives: the six substrings are byte-for-byte identical to main; the chat-genuinely-dead errors (user is deactivated, bot was blocked, not a member) route to the separate forbidden bucket, which is not gated and still marks dead. Casing is safe (both functions lowercase).
  • Scope is narrow: the gate is if kind == "not_found" only — forbidden and every other dead kind pass through unchanged.
  • Informational (pre-existing, NOT this diff): PEER_ID_INVALID / group chat was migrated classify as unknown and are never dead-marked → infinite retries. This is a latent gap on current main, out of scope here; happy to open a separate follow-up if wanted.

Tests

pytest tests/gateway/test_dead_targets.py -q
25 passed

New tests cover: chat-level not_found marks dead; each of the five sub-chat not_found variants does not mark the parent chat dead; the is_chat_level_not_found helper (chat-level True, sub-chat False, both-present → False); and the _classify_dead_from_error_text gate.

Supersedes #55780. Full credit to @r266-tech.

r266-tech and others added 2 commits July 1, 2026 14:29
…not_found

NousResearch#55115 added the dead-target registry so confirmed-dead delivery targets are
short-circuited. Its documented scope (gateway/dead_targets.py) is deliberately
narrow: only *whole-chat* deaths -- the `forbidden` and chat-level `not_found`
(`chat not found`) kinds -- should be recorded; "Thread/topic-level not_found is
NOT recorded here ... a deleted topic does not mean the parent chat is dead."

But the implementation doesn't honor that scope. classify_send_error collapses
chat-level "chat not found" AND thread/message-level not_found ("thread not
found", "topic_deleted", "message_id_invalid", "message to edit/reply not
found") into one "not_found" kind, _DEAD_ERROR_KINDS contains "not_found"
wholesale, and deliver()'s except marks the PARENT chat_id dead. So a single
deleted Telegram topic or edited-away message permanently marks the entire chat
(and every future scheduled / cron / agent delivery to it) dead -- silently. The
adapter self-heal the docstring relies on only covers the non-private-group
thread retry; named-DM-topic and message-level failures propagate to deliver()'s
except and wrongly kill the whole chat.

Add is_chat_level_not_found() (factoring the not_found substrings into chat-level
vs sub-chat-level constants) and gate the delivery dead-path: a "not_found" only
marks the target dead when it is chat-level. classify_send_error's public
contract is unchanged (still returns "not_found" for every shape); only the
mark_dead decision is refined, restoring the registry's documented scope.

Cross-platform: telegram/slack/discord delivery all flow through
classify_send_error -> mark_dead. Adds regression tests through the real
deliver() path plus helper/classifier units.
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) July 1, 2026 09:31
@kshitijk4poor
kshitijk4poor merged commit 8f1d22d into NousResearch:main Jul 1, 2026
31 checks passed
@kshitijk4poor

Copy link
Copy Markdown
Collaborator Author

Ran the full hermes-pr-review Phase 2c (4-part structured review + hermes-agent-dev checks). One real finding folded as a follow-up commit (d7c3a5d): classify_send_error and is_chat_level_not_found built their error-text blob divergently (one included the exception class name, the other didn't) — extracted a shared _error_blob helper as the single source of truth, aligned the sibling signatures to (exc, error_text), and added a mutation-verified regression guard. No behavior change to the classifier's current call sites; 26 tests pass.

@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 1, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.
Salvage of #55780 (@r266-tech), rebased onto current main. Related, not a duplicate: this corrects a regression of the merged dead-target registry (#55115). The predecessor #55780 can be closed in favor of this rebased version.

kshitijk4poor added a commit that referenced this pull request Jul 1, 2026
Follow-up to the #55780 dead-target not_found blast-radius fix (merged in
#56225). classify_send_error and is_chat_level_not_found each built their own
lowercased error blob, but divergently: classify_send_error appended the
exception CLASS NAME while is_chat_level_not_found did not. A caller passing
exc= to both could get inconsistent answers on the same failure.

- Extract _error_blob(exc, error_text) as the single source of truth both
  classifiers use (str(exc) when non-empty + class name; no stray leading
  space).
- Align is_chat_level_not_found's signature to (exc, error_text), matching
  classify_send_error, removing the swapped-positional footgun; update the
  sole caller and the three tests to keyword form.
- Add a regression guard asserting _error_blob keeps the class name.

Surfaced by the hermes-pr-review Phase 2c structured review of #56225.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
Follow-up to the NousResearch#55780 dead-target not_found blast-radius fix (merged in
NousResearch#56225). classify_send_error and is_chat_level_not_found each built their own
lowercased error blob, but divergently: classify_send_error appended the
exception CLASS NAME while is_chat_level_not_found did not. A caller passing
exc= to both could get inconsistent answers on the same failure.

- Extract _error_blob(exc, error_text) as the single source of truth both
  classifiers use (str(exc) when non-empty + class name; no stray leading
  space).
- Align is_chat_level_not_found's signature to (exc, error_text), matching
  classify_send_error, removing the swapped-positional footgun; update the
  sole caller and the three tests to keyword form.
- Add a regression guard asserting _error_blob keeps the class name.

Surfaced by the hermes-pr-review Phase 2c structured review of NousResearch#56225.
Jasper6439 pushed a commit to Jasper6439/hermes-agent that referenced this pull request Jul 5, 2026
Follow-up to the NousResearch#55780 dead-target not_found blast-radius fix (merged in
NousResearch#56225). classify_send_error and is_chat_level_not_found each built their own
lowercased error blob, but divergently: classify_send_error appended the
exception CLASS NAME while is_chat_level_not_found did not. A caller passing
exc= to both could get inconsistent answers on the same failure.

- Extract _error_blob(exc, error_text) as the single source of truth both
  classifiers use (str(exc) when non-empty + class name; no stray leading
  space).
- Align is_chat_level_not_found's signature to (exc, error_text), matching
  classify_send_error, removing the swapped-positional footgun; update the
  sole caller and the three tests to keyword form.
- Add a regression guard asserting _error_blob keeps the class name.

Surfaced by the hermes-pr-review Phase 2c structured review of NousResearch#56225.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
Follow-up to the NousResearch#55780 dead-target not_found blast-radius fix (merged in
NousResearch#56225). classify_send_error and is_chat_level_not_found each built their own
lowercased error blob, but divergently: classify_send_error appended the
exception CLASS NAME while is_chat_level_not_found did not. A caller passing
exc= to both could get inconsistent answers on the same failure.

- Extract _error_blob(exc, error_text) as the single source of truth both
  classifiers use (str(exc) when non-empty + class name; no stray leading
  space).
- Align is_chat_level_not_found's signature to (exc, error_text), matching
  classify_send_error, removing the swapped-positional footgun; update the
  sole caller and the three tests to keyword form.
- Add a regression guard asserting _error_blob keeps the class name.

Surfaced by the hermes-pr-review Phase 2c structured review of NousResearch#56225.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
Follow-up to the NousResearch#55780 dead-target not_found blast-radius fix (merged in
NousResearch#56225). classify_send_error and is_chat_level_not_found each built their own
lowercased error blob, but divergently: classify_send_error appended the
exception CLASS NAME while is_chat_level_not_found did not. A caller passing
exc= to both could get inconsistent answers on the same failure.

- Extract _error_blob(exc, error_text) as the single source of truth both
  classifiers use (str(exc) when non-empty + class name; no stray leading
  space).
- Align is_chat_level_not_found's signature to (exc, error_text), matching
  classify_send_error, removing the swapped-positional footgun; update the
  sole caller and the three tests to keyword form.
- Add a regression guard asserting _error_blob keeps the class name.

Surfaced by the hermes-pr-review Phase 2c structured review of NousResearch#56225.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
Follow-up to the NousResearch#55780 dead-target not_found blast-radius fix (merged in
NousResearch#56225). classify_send_error and is_chat_level_not_found each built their own
lowercased error blob, but divergently: classify_send_error appended the
exception CLASS NAME while is_chat_level_not_found did not. A caller passing
exc= to both could get inconsistent answers on the same failure.

- Extract _error_blob(exc, error_text) as the single source of truth both
  classifiers use (str(exc) when non-empty + class name; no stray leading
  space).
- Align is_chat_level_not_found's signature to (exc, error_text), matching
  classify_send_error, removing the swapped-positional footgun; update the
  sole caller and the three tests to keyword form.
- Add a regression guard asserting _error_blob keeps the class name.

Surfaced by the hermes-pr-review Phase 2c structured review of NousResearch#56225.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
Follow-up to the NousResearch#55780 dead-target not_found blast-radius fix (merged in
NousResearch#56225). classify_send_error and is_chat_level_not_found each built their own
lowercased error blob, but divergently: classify_send_error appended the
exception CLASS NAME while is_chat_level_not_found did not. A caller passing
exc= to both could get inconsistent answers on the same failure.

- Extract _error_blob(exc, error_text) as the single source of truth both
  classifiers use (str(exc) when non-empty + class name; no stray leading
  space).
- Align is_chat_level_not_found's signature to (exc, error_text), matching
  classify_send_error, removing the swapped-positional footgun; update the
  sole caller and the three tests to keyword form.
- Add a regression guard asserting _error_blob keeps the class name.

Surfaced by the hermes-pr-review Phase 2c structured review of NousResearch#56225.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
Follow-up to the NousResearch#55780 dead-target not_found blast-radius fix (merged in
NousResearch#56225). classify_send_error and is_chat_level_not_found each built their own
lowercased error blob, but divergently: classify_send_error appended the
exception CLASS NAME while is_chat_level_not_found did not. A caller passing
exc= to both could get inconsistent answers on the same failure.

- Extract _error_blob(exc, error_text) as the single source of truth both
  classifiers use (str(exc) when non-empty + class name; no stray leading
  space).
- Align is_chat_level_not_found's signature to (exc, error_text), matching
  classify_send_error, removing the swapped-positional footgun; update the
  sole caller and the three tests to keyword form.
- Add a regression guard asserting _error_blob keeps the class name.

Surfaced by the hermes-pr-review Phase 2c structured review of NousResearch#56225.
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 P1 High — major feature broken, no workaround 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