fix(telegram): recover exhausted request pool - #98094
Merged
teknium1 merged 3 commits intoAug 30, 2026
Merged
Conversation
helix4u
marked this pull request as ready for review
August 29, 2026 19:29
Contributor
|
Hi helix4u, this is @AideYu, author of the original #68983. Reviewed the diff — this is a faithful rebase onto current main, and the generalization to also drain the general request pool on classifier-confirmed timeouts ( |
This was referenced Aug 31, 2026
kshitijk4poor
added a commit
that referenced
this pull request
Aug 31, 2026
_drain_polling_connections still bounded its shutdown()/initialize() with asyncio.wait_for (#66377), while its sibling the general-pool drain moved to _await_with_thread_deadline (#98094). httpcore's pool close runs under AsyncShieldCancellation, so a cancellation-resistant close keeps wait_for pending forever even after its timeout fires — the tracked _polling_error_task wedges and every escalation gate behind it stalls. Use the same wall-clock deadline helper (cancel + abandon, no cancel-await) on both polling-drain awaits, and add a regression test whose close swallows cancellation — the shape the existing cancellable-hang test cannot catch.
kshitijk4poor
added a commit
that referenced
this pull request
Aug 31, 2026
_looks_like_connect_timeout and _looks_like_pool_timeout carried two copies of the same 15-line DFS skeleton (seen-set, stack, __cause__/ __context__ descent) differing only in the one-line match predicate — follow-up to the #98094 review. Extract _iter_exception_graph() and collapse both classifiers onto it. Behavior is byte-identical (subprocess parity vs origin/main on real PTB error fixtures: 6/6 identical), and the two classifiers gain direct unit tests for the first time, including the cycle/diamond chain shapes the inline copies had no coverage for.
joojalre
pushed a commit
to joojalre/hermes-agent-almorshednet
that referenced
this pull request
Aug 31, 2026
_drain_polling_connections still bounded its shutdown()/initialize() with asyncio.wait_for (NousResearch#66377), while its sibling the general-pool drain moved to _await_with_thread_deadline (NousResearch#98094). httpcore's pool close runs under AsyncShieldCancellation, so a cancellation-resistant close keeps wait_for pending forever even after its timeout fires — the tracked _polling_error_task wedges and every escalation gate behind it stalls. Use the same wall-clock deadline helper (cancel + abandon, no cancel-await) on both polling-drain awaits, and add a regression test whose close swallows cancellation — the shape the existing cancellable-hang test cannot catch.
joojalre
pushed a commit
to joojalre/hermes-agent-almorshednet
that referenced
this pull request
Aug 31, 2026
_looks_like_connect_timeout and _looks_like_pool_timeout carried two copies of the same 15-line DFS skeleton (seen-set, stack, __cause__/ __context__ descent) differing only in the one-line match predicate — follow-up to the NousResearch#98094 review. Extract _iter_exception_graph() and collapse both classifiers onto it. Behavior is byte-identical (subprocess parity vs origin/main on real PTB error fixtures: 6/6 identical), and the two classifiers gain direct unit tests for the first time, including the cycle/diamond chain shapes the inline copies had no coverage for.
joojalre
added a commit
to joojalre/hermes-agent-almorshednet
that referenced
this pull request
Sep 1, 2026
* chore: sync fork with upstream through a9c783f * fix(dashboard-auth): url-encode the PKCE cookie value so strict proxy hops stop dropping it (NousResearch#99176) The PKCE payload is a flat 'provider=...;state=...;verifier=...;next=...' string. A raw ';' is a cookie-attribute terminator, so Python's http.cookies emits the value in RFC 6265 quoted form with each ';' escaped as the backslash-octal '\073'. Mainstream browsers echo that form back verbatim and Python parsers decode it — the browser round trip is fine. But '"' and '\' are outside the plain cookie-octet set, and non-Python hops that re-serialize the Cookie header reject the value and drop the cookie entirely: Go's net/http (Traefik middleware, Authentik outposts, other gateways) refuses any cookie value containing a backslash. The OIDC callback then 400s with "Missing PKCE state cookie" even though the browser sent the cookie. Field reproduction: support thread "Still unable to use Authentik for signin with traefik" — devtools showed the browser sending the intact quoted \073 cookie on /auth/callback while Hermes logged missing_pkce_cookie behind a Traefik+Authentik chain. Fix: URL-encode the whole payload in set_pkce_cookie (quote(payload, safe='') — ';' becomes '%3B') so the wire value contains only cookie-octets and no parser in the chain has anything to reject, and decode through a single shared inverse, cookies.parse_pkce_payload(), in BOTH readers: the OAuth /auth/callback and the native password-login path (routes.login_submit), whose broker/provider binding check would otherwise parse zero segments from the newly-encoded value and silently disable itself. Regression coverage: the wire-shape test pins the full cookie-octet set (the '"'/'\' assertions are the ones a Go-parser hop fails pre-fix), the round-trip tests drive the real /auth/login → /auth/callback path, and the next= test pins the exact post-login redirect byte shape. Native-flow broker assertions updated to decode through parse_pkce_payload instead of substring-matching the raw wire value. Salvaged from NousResearch#84065 (rebased onto current main, which gained the SameSite=None PKCE attrs and the RFC 8252 native password flow since the PR branched): kept main's _pkce_attrs cookie shape, extended the fix to the login_submit reader the original PR predated, and reframed the rationale — browsers do NOT truncate at the first ';' (there is no literal ';' on the wire in the quoted form); the failing hop is a strict middlebox cookie parser. Closes NousResearch#83832 Co-authored-by: Kailigithub <12250313+Kailigithub@users.noreply.github.com> * fix(telegram): bound polling drain with wall-clock deadline _drain_polling_connections still bounded its shutdown()/initialize() with asyncio.wait_for (NousResearch#66377), while its sibling the general-pool drain moved to _await_with_thread_deadline (NousResearch#98094). httpcore's pool close runs under AsyncShieldCancellation, so a cancellation-resistant close keeps wait_for pending forever even after its timeout fires — the tracked _polling_error_task wedges and every escalation gate behind it stalls. Use the same wall-clock deadline helper (cancel + abandon, no cancel-await) on both polling-drain awaits, and add a regression test whose close swallows cancellation — the shape the existing cancellable-hang test cannot catch. * test: settle generation verifier in shielded-close drain test * refactor(telegram): share exception-graph walk across classifiers _looks_like_connect_timeout and _looks_like_pool_timeout carried two copies of the same 15-line DFS skeleton (seen-set, stack, __cause__/ __context__ descent) differing only in the one-line match predicate — follow-up to the NousResearch#98094 review. Extract _iter_exception_graph() and collapse both classifiers onto it. Behavior is byte-identical (subprocess parity vs origin/main on real PTB error fixtures: 6/6 identical), and the two classifiers gain direct unit tests for the first time, including the cycle/diamond chain shapes the inline copies had no coverage for. * test: stabilize Telegram deadline assertion on Windows * fix: address hosted room review findings * fix: close hosted room publication races * fix(relay): resolve fresh-final unfurl decision per chat, not per primary identity (NousResearch#99206) The stream consumer called prefers_fresh_final_streaming(text, metadata=...) only, and no metadata producer stamps a platform key — so RelayAdapter's hook always fell back to the PRIMARY descriptor's platform (the scalar-vs-per-chat capability seam, third occurrence). Two failure directions on multiplexed relays with platforms.relay.extra.slack.unfurl_links/media: true (NousResearch#97957): - Slack primary fronting Telegram/Discord: every link-bearing streamed final on the non-Slack chats finalized as a fresh send with no delete op advertised -> the answer delivered TWICE (orphaned preview). - Non-Slack primary fronting Slack: the hook returned False, leaving the force-on unfurl feature dark on exactly the chats it shipped for. Pass chat_id=self.chat_id from the consumer; the relay hook already accepted it and resolves via _platform_by_chat + the per-platform negotiated descriptor. Graduated TypeError fallback keeps the single-platform hook signatures (Telegram, base class) and legacy test doubles working unchanged. Both regression tests verified RED against the unfixed consumer, GREEN with the fix; single-platform relays are unaffected (NousResearch#97957's own 30 tests unchanged-green). * fix(bot-mode): preserve UTF-8 local DM delivery on Windows * fix(hosted-rooms): close remaining lifecycle races * fix(bot-mode): use subprocess env factory for peer delivery * test: accept asynchronous stop settlement * test: replace fixed waits with lifecycle conditions * fix(groups): close hosted room review races * fix: close hosted room authority review gaps * fix: close hosted room retry review gaps * fix(groups): refresh demotion stop fence on retry * fix(groups): defer deleted profiles before admission * fix(tui): recover long hosted room ids for prompt fence * test(groups): assert fresh stop per demotion attempt * fix(groups): fence profiles deleted after admission * fix(groups): close hosted room review races * fix(groups): account authority loss storage * fix(groups): keep cross-process stops pending * fix(telegram): quarantine abandoned polling shutdown * test(groups): align demotion fixture with process owner * fix(hosted-rooms): harden stop ownership and terminal recovery * fix(groups): close hosted-room concurrency invariants * fix(groups): close terminal recovery races * fix(groups): close remaining hosted room review gaps * fix(groups): preserve concurrent disband and legacy approvals --------- Co-authored-by: Ben Barclay <ben@nousresearch.com> Co-authored-by: Kailigithub <12250313+Kailigithub@users.noreply.github.com> Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
EduardoSolanas
pushed a commit
to EduardoSolanas/hermes-agent
that referenced
this pull request
Sep 2, 2026
_drain_polling_connections still bounded its shutdown()/initialize() with asyncio.wait_for (NousResearch#66377), while its sibling the general-pool drain moved to _await_with_thread_deadline (NousResearch#98094). httpcore's pool close runs under AsyncShieldCancellation, so a cancellation-resistant close keeps wait_for pending forever even after its timeout fires — the tracked _polling_error_task wedges and every escalation gate behind it stalls. Use the same wall-clock deadline helper (cancel + abandon, no cancel-await) on both polling-drain awaits, and add a regression test whose close swallows cancellation — the shape the existing cancellable-hang test cannot catch.
EduardoSolanas
pushed a commit
to EduardoSolanas/hermes-agent
that referenced
this pull request
Sep 2, 2026
_looks_like_connect_timeout and _looks_like_pool_timeout carried two copies of the same 15-line DFS skeleton (seen-set, stack, __cause__/ __context__ descent) differing only in the one-line match predicate — follow-up to the NousResearch#98094 review. Extract _iter_exception_graph() and collapse both classifiers onto it. Behavior is byte-identical (subprocess parity vs origin/main on real PTB error fixtures: 6/6 identical), and the two classifiers gain direct unit tests for the first time, including the cycle/diamond chain shapes the inline copies had no coverage for.
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
_drain_polling_connections still bounded its shutdown()/initialize() with asyncio.wait_for (NousResearch#66377), while its sibling the general-pool drain moved to _await_with_thread_deadline (NousResearch#98094). httpcore's pool close runs under AsyncShieldCancellation, so a cancellation-resistant close keeps wait_for pending forever even after its timeout fires — the tracked _polling_error_task wedges and every escalation gate behind it stalls. Use the same wall-clock deadline helper (cancel + abandon, no cancel-await) on both polling-drain awaits, and add a regression test whose close swallows cancellation — the shape the existing cancellable-hang test cannot catch.
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
_looks_like_connect_timeout and _looks_like_pool_timeout carried two copies of the same 15-line DFS skeleton (seen-set, stack, __cause__/ __context__ descent) differing only in the one-line match predicate — follow-up to the NousResearch#98094 review. Extract _iter_exception_graph() and collapse both classifiers onto it. Behavior is byte-identical (subprocess parity vs origin/main on real PTB error fixtures: 6/6 identical), and the two classifiers gain direct unit tests for the first time, including the cycle/diamond chain shapes the inline copies had no coverage for.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Telegram polling recovery currently resets only PTB's dedicated
getUpdatesrequest pool. When the triggering error is a confirmed pool timeout, that is insufficient:start_polling()performs Bot API bootstrap calls through PTB's separate general request pool before polling begins, so an exhausted general pool causes every reconnect attempt to fail before it can reachgetUpdates.This change resets the general request pool only for classifier-confirmed pool timeouts, then continues through the existing polling-pool drain and restart path. Generic network errors remain polling-only so ordinary recovery does not interrupt concurrent sends. General-pool shutdown and initialization are bounded with the adapter's existing wall-clock deadline helper so a cancellation-resistant transport cannot freeze the reconnect ladder.
This salvages #68983 by @AideYu onto current
main; the original commit authorship is preserved.Related Issue
Supersedes #68983, which no longer applies cleanly to current
main.Type of Change
Changes Made
getUpdatespool.How to Test
scripts/run_tests.sh tests/gateway/test_telegram_network_reconnect.py -q -j 4.start_polling().Local execution was not performed under the workstation test policy. Static verification completed: both changed Python files parse,
git diff --checkpasses, andscripts/check-windows-footguns.py --diff origin/mainpasses. Focused execution is left to CI.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
N/A. The regression is covered with mocked PTB request pools and lifecycle behavior.