Skip to content

fix(qqbot): cap Resume retries and fall back to Identify on stale session - #22384

Closed
wesleysimplicio wants to merge 1 commit into
NousResearch:mainfrom
wesleysimplicio:fix/qqbot-resume-stale-session-fallback
Closed

fix(qqbot): cap Resume retries and fall back to Identify on stale session#22384
wesleysimplicio wants to merge 1 commit into
NousResearch:mainfrom
wesleysimplicio:fix/qqbot-resume-stale-session-fallback

Conversation

@wesleysimplicio

@wesleysimplicio wesleysimplicio commented May 9, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

When the QQ gateway accepts a Resume frame but immediately closes the WebSocket (the typical signal that the session_id has expired server-side), the adapter previously kept reconnecting and retrying Resume forever with the same expired session_id. The bot stays effectively offline until the process is restarted.

Root cause

When the QQ gateway accepts a Resume frame but immediately closes the WebSocket (the typical signal that the session_id has expired server-side), the adapter previously kept reconnecting and retrying Resume forever with the same expired session_id. The bot stays effectively offline until the process is restarted.

Fix

Track _resume_attempts on the adapter. Each op-10 Hello that picks Resume increments it; after MAX_RESUME_ATTEMPTS = 3 consecutive unconfirmed attempts, discard _session_id/_last_seq and fall back to a fresh Identify. The counter resets on RESUMED dispatch and on successful READY (handled in _handle_ready), so a healthy reconnect cycle is unaffected.

The cap is exposed as MAX_RESUME_ATTEMPTS in gateway/platforms/qqbot/constants.py so future tuning is a one-line change.

Why this shape

This shape mirrors #29640 so reviewers can quickly compare scope, root cause, fix, tests, and related context without having to decode a custom PR description.

Tests

  • Veja a descrição original preservada abaixo para detalhes de validação, testes e notas de verificação.
Original body

Related PRs / issues

Closes #22179

Original body

Summary

When the QQ gateway accepts a Resume frame but immediately closes the WebSocket (the typical signal that the session_id has expired server-side), the adapter previously kept reconnecting and retrying Resume forever with the same expired session_id. The bot stays effectively offline until the process is restarted.

What Changed

  • Standardized this PR body to the current Hermes Turbo template.
  • Preserved the original detailed description below for reference.

Fluxo

A mudança continua seguindo o fluxo original descrito na seção preservada abaixo, sem ampliar o escopo funcional deste PR.

Visão

A padronização melhora a revisão, reduz ruído e evita deriva de formatação entre PRs abertos.

Test Plan

  • Veja a descrição original preservada abaixo para detalhes de validação, testes e notas de verificação.
Original body

What does this PR do?

Problem

When the QQ gateway accepts a Resume frame but immediately closes the WebSocket (the typical signal that the session_id has expired server-side), the adapter previously kept reconnecting and retrying Resume forever with the same expired session_id. The bot stays effectively offline until the process is restarted.

Root cause

gateway/platforms/qqbot/adapter.py op-10 Hello handler picks Resume whenever _session_id and _last_seq are non-None, with no notion of how many consecutive Resume attempts have not been confirmed by a RESUMED/READY dispatch. A server that accepts Resume then closes the socket leaves both fields populated, so every reconnect picks Resume again.

Fix

Track _resume_attempts on the adapter. Each op-10 Hello that picks Resume increments it; after MAX_RESUME_ATTEMPTS = 3 consecutive unconfirmed attempts, discard _session_id/_last_seq and fall back to a fresh Identify. The counter resets on RESUMED dispatch and on successful READY (handled in _handle_ready), so a healthy reconnect cycle is unaffected.

The cap is exposed as MAX_RESUME_ATTEMPTS in gateway/platforms/qqbot/constants.py so future tuning is a one-line change.

Tests

tests/gateway/test_qqbot_resume_loop_fallback.py drives op-10 Hello through _dispatch_payload and asserts:

  • counter starts at 0
  • Resume increments the counter without discarding the session
  • (cap+1)-th Hello discards _session_id/_last_seq
  • RESUMED and READY both reset the counter
  • absence of _session_id keeps the counter at 0 (Identify path)
  • after fallback, a fresh session restarts the cycle from 0
  • a source-level guard keeps MAX_RESUME_ATTEMPTS configurable

Verified failing on main (8/8) and passing on this branch (8/8). Full tests/gateway/ suite still green (5050 passed). Pre-existing unrelated failures elsewhere in the suite (tests/tools/test_file_read_guards, tests/hermes_cli/test_gateway_wsl, etc.) reproduce on main without this change and are out of scope.

Closes #22179

Solution Sketch

  • fix the root cause in the touched subsystem instead of layering a broad workaround around the symptom
  • keep surrounding behavior stable and avoid unrelated refactors while the area is under review
  • prove the change with focused checks on the exact path that regressed

Related Issue

Closes #22179

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • preserved the existing technical rationale and validation notes inside the template body
  • scoped this PR description to the implementation already present on the branch
  • aligned the delivery format with .github/PULL_REQUEST_TEMPLATE.md

How to Test

  1. Review the existing validation notes preserved in this PR body.
  2. Run the focused checks for the touched area.
  3. Confirm the scoped change still behaves as described above.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform:

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

  • N/A.

Generated by Hermes Turbo


Generated by Hermes Turbo

…sion

Problem
-------
When the QQ gateway accepts a Resume frame but immediately closes the
WebSocket (the typical signal that the session_id has expired
server-side), the adapter previously kept reconnecting and retrying
Resume forever with the same expired session_id. Bot stays effectively
offline until the process is restarted.

Root cause
----------
The op-10 Hello handler picks Resume whenever ``_session_id`` and
``_last_seq`` are non-None, with no notion of how many consecutive
Resume attempts have not been confirmed by a RESUMED/READY dispatch.
A server that accepts Resume then closes the socket leaves both fields
populated, so every reconnect picks Resume again.

Fix
---
Track ``_resume_attempts`` on the adapter. Each op-10 Hello that picks
Resume increments it; after ``MAX_RESUME_ATTEMPTS = 3`` consecutive
unconfirmed attempts, discard ``_session_id``/``_last_seq`` and fall
back to a fresh Identify. The counter resets on RESUMED dispatch and
on successful READY (handled in ``_handle_ready``), so a healthy
reconnect cycle is unaffected.

Tests
-----
``tests/gateway/test_qqbot_resume_loop_fallback.py`` drives op-10
Hello through ``_dispatch_payload`` and asserts:
  * counter starts at 0
  * Resume increments the counter without discarding the session
  * (cap+1)-th Hello discards ``_session_id``/``_last_seq``
  * RESUMED and READY both reset the counter
  * absence of ``_session_id`` keeps the counter at 0 (Identify path)
  * after fallback, a fresh session restarts the cycle from 0
  * a source-level guard keeps ``MAX_RESUME_ATTEMPTS`` configurable

Verified failing on ``main`` (8/8) and passing on this branch (8/8).
Full ``tests/gateway/`` suite still green (5050 passed). Pre-existing
unrelated failures in ``tests/tools/test_file_read_guards``,
``tests/hermes_cli/test_gateway_wsl``, etc. reproduce on ``main``
without this change and are out of scope.

Closes NousResearch#22179

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 9, 2026 07:47

Copilot AI 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.

Pull request overview

This PR fixes a QQ Bot gateway reconnection failure mode where an expired session_id could cause the adapter to endlessly retry Resume and never fall back to a fresh Identify, leaving the bot effectively offline until restart.

Changes:

  • Add MAX_RESUME_ATTEMPTS (default 3) to cap consecutive unconfirmed Resume attempts.
  • Track _resume_attempts in QQAdapter and discard stale session state after the cap, forcing Identify.
  • Add a regression test suite to validate the retry cap and counter reset behavior on READY/RESUMED.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
gateway/platforms/qqbot/adapter.py Adds _resume_attempts tracking and Hello-time fallback from Resume to Identify after capped failures.
gateway/platforms/qqbot/constants.py Introduces configurable MAX_RESUME_ATTEMPTS constant used by the adapter.
tests/gateway/test_qqbot_resume_loop_fallback.py Adds regression tests covering the stale-session resume loop and fallback/reset conditions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 794 to +798
else:
if self._resume_attempts >= MAX_RESUME_ATTEMPTS:
logger.warning(
"[%s] Resume failed %d times — discarding stale session_id and re-identifying",
self._log_tag,
Comment on lines +17 to +18
import pytest

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/qqbot QQ Bot adapter labels May 9, 2026
@wesleysimplicio

Copy link
Copy Markdown
Contributor Author

Closing — PR has merge conflicts that can't be auto-resolved. The codebase has evolved past this fix. Re-opening with a fresh rebase welcome if the issue is still open.

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/qqbot QQ Bot adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(qqbot): Reconnect loop with stale session — adapter never falls back to fresh Identify

3 participants