telegram-forum-dead-thread-id-masked-by-no-thread-retry
Diagnosis and verified solution written to ~/my-project/SOLUTION.md (copy at /workspace/solution.md), with a machine-checked control-flow reduction at ~/my-project/repro/thread_fallback_repro.py.
The environment contained no hermes source or logs (only /workspace/problem.json), so I reduced the retry control flow to a runnable fake-bot and verified bug vs. fix.
The standalone sender has two terminal fallbacks that don't compose:
- _is_parse_mode_error → resend as plain text, keeping message_thread_id
- _is_thread_error → resend without message_thread_id
When a message hits a dead thread and a parse-mode failure, the parse branch runs first, retries with the still-dead id, raises again inside the except, and never reaches the no-thread branch. Result: ~80% silently land in General (one retrying-without-message_thread_id warning per OK delivery), ~20% hard-fail (each preceded ≤1s by Parse-mode-fallen-back-to-plain-text).
Replace the two terminal returns with one guarded loop where each fallback re-enters the same guard (at most one parse drop + one thread drop, so ≤3 attempts, always terminates):
mode, thread_id = parse_mode, message_thread_id
dropped_parse_mode = dropped_thread_id = False
while True:
try:
return await self.bot.send_message(
chat_id=chat_id, text=text,
message_thread_id=thread_id, parse_mode=mode)
except TelegramError as exc:
if mode and not dropped_parse_mode and _is_parse_mode_error(exc):
dropped_parse_mode, mode = True, None
logger.warning("Parse-mode-fallen-back-to-plain-text"); continue
if thread_id is not None and not dropped_thread_id and _is_thread_error(exc):
dropped_thread_id, thread_id = True, None
logger.warning("retrying-without-message_thread_id"); continue
raise
This works in both orders and keeps a live thread id during a parse fallback. Operator fixes: A supply a live topic id (pause gateway → getUpdates → extract message_thread_id), or B blank the thread id to retarget General (no-op for readers).
The reduction output:
buggy calls=[(12345,'Markdown'), (12345,None)] -> HARD-FAIL(thread not found)
fixed calls=[(12345,'Markdown'), (12345,None), (None,None)] -> DELIVERED(no-thread=True)
ASSERT live-thread parse fallback keeps thread id: OK
ALL ASSERTIONS PASSED
Post-deploy acceptance: with fix A, retrying-without-message_thread_id count → 0 and messages return to the correct topic; with code-only/threadless, hard fails → 0, deliveries land in General, and one warning per delivery is expected. Add a permanent alert for Parse-mode-fallen-back-to-plain-text immediately followed by a delivery failure — that ordering must never occur in a healthy deployment. Full commands, classifiers, rollback, and acceptance table are in §3–§5 of the document.
# Evidence - Problem class: telegram-forum-dead-thread-id-masked-by-no-thread-retry - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T00:16:05.355Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Forum-topic deliveries to a DEAD Telegram message_thread_id report ~80pct OK because hermes-agent standalone sender retries WITHOUT message_thread_id (lands in forum General); the ~20pct hard failures are messages that ALSO trip a parse-mode fallback, whose plain-text retry keeps the dead id and never recovers. Diagnosis (re-runnable): grep DELIVER lines per project in ~/.hermes/coding-hermes/scheduler.log; grep -h retrying-without-message_thread_id in ~/.hermes/logs/agent.log* \u2014 dead id = one warning per ok delivery (1:1 pairing), live id = zero; every hard fail preceded <=1s by a Parse-mode-fallen-back-to-plain-text line. Proven 2026-09-15 my-project GAP-050: thread 12345 dead; 13 no-thread-retry warnings map 1:1 to 13 ok deliveries; 37/192 hard fails each preceded by parse-mode fallback. Fixes: A) operator supplies a live topic id (Bot API has no topic-list, getUpdates unavailable while gateway polls); B) retarget to threadless chat (no-op for readers); durable = make parse-mode fallback thread-aware like the no-thread retry. Full recipe: ~/my-project/docs/verification.md section 7.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "telegram-forum-dead-thread-id-masked-by-no-thread-retry", "provider": "openrouter", "solved_at": "2026-09-16T00:16:05.355Z", "version": ""}Diagnosis and verified solution written to ~/my-project/SOLUTION.md (copy at /workspace/solution.md), with a machine-checked control-flow reduction at ~/my-project/repro/thread_fallback_repro.py.
The environment contained no hermes source or logs (only /workspace/problem.json), so I reduced the retry control flow to a runnable fake-bot and verified bug vs. fix.
The standalone sender has two terminal fallbacks that don't compose:
- _is_parse_mode_error → resend as plain text, keeping message_thread_id
- _is_thread_error → resend without message_thread_id
When a message hits a dead thread and a parse-mode failure, the parse branch runs first, retries with the still-dead id, raises again inside the except, and never reaches the no-thread branch. Result: ~80% silently land in General (one retrying-without-message_thread_id warning per OK delivery), ~20% hard-fail (each preceded ≤1s by Parse-mode-fallen-back-to-plain-text).
Replace the two terminal returns with one guarded loop where each fallback re-enters the same guard (at most one parse drop + one thread drop, so ≤3 attempts, always terminates):
mode, thread_id = parse_mode, message_thread_id
dropped_parse_mode = dropped_thread_id = False
while True:
try:
return await self.bot.send_message(
chat_id=chat_id, text=text,
message_thread_id=thread_id, parse_mode=mode)
except TelegramError as exc:
if mode and not dropped_parse_mode and _is_parse_mode_error(exc):
dropped_parse_mode, mode = True, None
logger.warning("Parse-mode-fallen-back-to-plain-text"); continue
if thread_id is not None and not dropped_thread_id and _is_thread_error(exc):
dropped_thread_id, thread_id = True, None
logger.warning("retrying-without-message_thread_id"); continue
raise
This works in both orders and keeps a live thread id during a parse fallback. Operator fixes: A supply a live topic id (pause gateway → getUpdates → extract message_thread_id), or B blank the thread id to retarget General (no-op for readers).
The reduction output:
buggy calls=[(12345,'Markdown'), (12345,None)] -> HARD-FAIL(thread not found)
fixed calls=[(12345,'Markdown'), (12345,None), (None,None)] -> DELIVERED(no-thread=True)
ASSERT live-thread parse fallback keeps thread id: OK
ALL ASSERTIONS PASSED
Post-deploy acceptance: with fix A, retrying-without-message_thread_id count → 0 and messages return to the correct topic; with code-only/threadless, hard fails → 0, deliveries land in General, and one warning per delivery is expected. Add a permanent alert for Parse-mode-fallen-back-to-plain-text immediately followed by a delivery failure — that ordering must never occur in a healthy deployment. Full commands, classifiers, rollback, and acceptance table are in §3–§5 of the document.
# Evidence - Problem class: telegram-forum-dead-thread-id-masked-by-no-thread-retry - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T00:16:05.355Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Forum-topic deliveries to a DEAD Telegram message_thread_id report ~80pct OK because hermes-agent standalone sender retries WITHOUT message_thread_id (lands in forum General); the ~20pct hard failures are messages that ALSO trip a parse-mode fallback, whose plain-text retry keeps the dead id and never recovers. Diagnosis (re-runnable): grep DELIVER lines per project in ~/.hermes/coding-hermes/scheduler.log; grep -h retrying-without-message_thread_id in ~/.hermes/logs/agent.log* \u2014 dead id = one warning per ok delivery (1:1 pairing), live id = zero; every hard fail preceded <=1s by a Parse-mode-fallen-back-to-plain-text line. Proven 2026-09-15 my-project GAP-050: thread 12345 dead; 13 no-thread-retry warnings map 1:1 to 13 ok deliveries; 37/192 hard fails each preceded by parse-mode fallback. Fixes: A) operator supplies a live topic id (Bot API has no topic-list, getUpdates unavailable while gateway polls); B) retarget to threadless chat (no-op for readers); durable = make parse-mode fallback thread-aware like the no-thread retry. Full recipe: ~/my-project/docs/verification.md section 7.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "telegram-forum-dead-thread-id-masked-by-no-thread-retry", "provider": "openrouter", "solved_at": "2026-09-16T00:16:05.355Z", "version": ""}