Skip to content

fix(gateway): offload handoff-watcher SQLite calls to avoid blocking the async heartbeat (#40695) - #51890

Merged
kshitijk4poor merged 1 commit into
mainfrom
salvage/40695-handoff-watcher-async
Jun 24, 2026
Merged

kshitijk4poor merged 1 commit into
mainfrom
salvage/40695-handoff-watcher-async

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Contributor

Summary

Closes #40695 — the Discord gateway heartbeat is blocked by synchronous handoff SQLite polling (discord.gateway: Shard ID None heartbeat blocked for more than N seconds).

_handoff_watcher (an async def background task) polls SessionDB synchronously every 2s on the platform asyncio loop. A slow/contended SQLite call stalls the loop and starves Discord heartbeats.

Verified still live on current main (gateway/run.py:5950): no to_thread/run_in_executor anywhere in the watcher.

Fix

Offload all four blocking SessionDB calls inside the watcher loop via asyncio.to_thread so they run off the event loop:

  • list_pending_handoffs(), claim_handoff(...), complete_handoff(...), fail_handoff(...)

These four are the only sync self._session_db.* calls inside the watcher loop (verified). The maybe_auto_prune_and_vacuum call runs in __init__ before the loop starts, so it is not a heartbeat hazard and is intentionally left alone.

Salvage / attribution

Salvaged from #40782 (@r266-tech), whose asyncio.to_thread approach was correct but based on an older line location; _handoff_watcher has since moved to gateway/run.py:5950. Re-applied to the current location, authorship preserved. (Supersedes the #40695 cluster: #40853, #40974. Note: #43504's handoff_state DB index is not the fix here — the query already plans fast; the hazard is event-loop blocking under contention — so it's not part of this PR.)

Tests

tests/gateway/test_handoff_watcher_async_db.py — a recording SessionDB asserts each call runs on a worker thread (not the event-loop thread), plus a to_thread spy, plus the error path (fail_handoff). Mutation-checked (reverting the list_pending_handoffs wrap fails the off-loop assertions). 3 pass.

Fixes #40695

… loop

The Discord gateway heartbeat stalled ('Shard ID None heartbeat blocked
for more than N seconds') because _handoff_watcher polled the synchronous,
blocking SQLite-backed SessionDB directly on the asyncio event loop every
2s. Each list_pending/claim/complete/fail call performed blocking disk I/O
on the loop thread, starving the Discord heartbeat coroutine.

Wrap every blocking SessionDB call inside the watcher loop in
asyncio.to_thread(...) so the SQLite work runs on a worker thread and the
event loop (and heartbeat) stays responsive. These four call sites are the
only synchronous self._session_db.* calls inside the watcher loop body.

Adds tests/gateway/test_handoff_watcher_async_db.py asserting the watcher
offloads its SessionDB calls via asyncio.to_thread (mutation-survivable:
reverting any to_thread wrap fails the corresponding assertion).

Fixes #40695

Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: salvage/40695-handoff-watcher-async vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11181 on HEAD, 11178 on base (🆕 +3)

🆕 New issues (3):

Rule Count
unresolved-attribute 2
unresolved-import 1
First entries
run_agent.py:2984: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/gateway/test_handoff_watcher_async_db.py:20: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`

✅ Fixed issues (1):

Rule Count
invalid-assignment 1
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

Unchanged: 5904 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

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

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Related: salvage of #40782 (@r266-tech), re-applied to the current gateway/run.py location. Competes with the open #40695 offload cluster (#40782/#40853/#40974, same asyncio.to_thread fix) and #43504 (different mechanism: a handoff_state index). Maintainer should pick one -- this one preserves authorship and targets the current line location.

@kshitijk4poor
kshitijk4poor enabled auto-merge June 24, 2026 13:36
@kshitijk4poor
kshitijk4poor merged commit d68a133 into main Jun 24, 2026
48 of 50 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/40695-handoff-watcher-async branch June 24, 2026 13:40
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…-handoff-watcher-async

fix(gateway): offload handoff-watcher SQLite calls to avoid blocking the async heartbeat (NousResearch#40695)
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…-handoff-watcher-async

fix(gateway): offload handoff-watcher SQLite calls to avoid blocking the async heartbeat (NousResearch#40695)
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…-handoff-watcher-async

fix(gateway): offload handoff-watcher SQLite calls to avoid blocking the async heartbeat (NousResearch#40695)
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…-handoff-watcher-async

fix(gateway): offload handoff-watcher SQLite calls to avoid blocking the async heartbeat (NousResearch#40695)
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…-handoff-watcher-async

fix(gateway): offload handoff-watcher SQLite calls to avoid blocking the async heartbeat (NousResearch#40695)
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…-handoff-watcher-async

fix(gateway): offload handoff-watcher SQLite calls to avoid blocking the async heartbeat (NousResearch#40695)
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 platform/discord Discord bot 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]: Discord gateway heartbeat can be blocked by synchronous handoff SQLite polling

3 participants