Summary
PR #25014 wired tools.lazy_deps.ensure() into the check_*_requirements() functions for Slack, Matrix, DingTalk, and Feishu, matching the existing Discord/Telegram pattern. The plumbing is correct, but for three of the four adapters the module-level globals that the adapter actually uses are not rebound after lazy-install succeeds. A user whose deps are missing on first start will see lazy_deps.ensure() install everything fine, check_*_requirements() return True, and then the adapter blow up at runtime with NameError / TypeError: 'NoneType' object is not callable because the names still point at stubs (or are unbound) from the original module-level try: ... except ImportError: block.
DingTalk is the gold-standard reference — it explicitly rebinds every name it uses. Slack/Feishu/Matrix should match.
The bug is masked in Docker because the gateway typically restarts after first-run install, picking up real imports cleanly on the second start. But for any long-lived gateway process (most non-Docker deployments), this surfaces immediately.
1. Slack — aiohttp not rebound (NameError)
gateway/platforms/slack.py lines 21–31 import four names at module top: AsyncApp, AsyncSocketModeHandler, AsyncWebClient, and aiohttp. PR #25014 specifically added aiohttp==3.13.3 to LAZY_DEPS["platform.slack"] so the lazy-install would pull it. But the new check_slack_requirements() (lines 75–102) declares:
global SLACK_AVAILABLE, AsyncApp, AsyncSocketModeHandler, AsyncWebClient
— aiohttp is missing from global AND from the rebind block. Because the original try raises ImportError on import slack_bolt BEFORE reaching import aiohttp, the except branch leaves aiohttp unbound at module scope. The first call into _handle_file_upload-style code (gateway/platforms/slack.py:464 uses aiohttp.ClientSession(), line 468 uses aiohttp.ClientTimeout(...)) raises NameError: name 'aiohttp' is not defined.
Minimal repro of the pattern:
try:
import nonexistent_pkg as fake_slack_bolt # raises
import json as aiohttp # never reached
except ImportError:
pass
def check_slack_requirements():
global AsyncApp # forgot aiohttp
AsyncApp = object
return True
check_slack_requirements()
aiohttp.ClientSession # NameError
NameError: name 'aiohttp' is not defined
Fix
global SLACK_AVAILABLE, AsyncApp, AsyncSocketModeHandler, AsyncWebClient, aiohttp
...
from slack_sdk.web.async_client import AsyncWebClient as _Client
import aiohttp as _aiohttp
...
aiohttp = _aiohttp
Related: pyproject.toml slack extra divergence
slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1"] does NOT include aiohttp, but LAZY_DEPS["platform.slack"] now does. pip install hermes-agent[slack] still produces a slack adapter that NameErrors on file uploads — only the lazy-install path includes aiohttp. Pre-existing inconsistency, but easy to fix in the same PR — add aiohttp==3.13.3 to the slack extra.
2. Feishu — lark_oapi symbols never rebound (TypeError on adapter init)
gateway/platforms/feishu.py lines 86–124 import ~25 names from lark_oapi (lark, CreateFileRequest, CreateMessageRequest, GetMessageRequest, EventDispatcherHandler, FeishuWSClient, FEISHU_DOMAIN, LARK_DOMAIN, CallBackCard, P2CardActionTriggerResponse, AccessTokenType, HttpMethod, BaseRequest, …). On ImportError the except branch sets all of them to None.
The new check_feishu_requirements() only rebinds FEISHU_AVAILABLE:
global FEISHU_AVAILABLE
...
import lark_oapi # noqa: F401
FEISHU_AVAILABLE = True
return True
So lark_oapi ends up imported into sys.modules, but the local module's globals (lark, CreateMessageRequest, etc.) stay bound to None. FeishuAdapter instantiation hits:
gateway/platforms/feishu.py:4377 — FeishuWSClient(...) → TypeError: 'NoneType' object is not callable
gateway/platforms/feishu.py:4380 — lark.LogLevel.INFO → AttributeError: 'NoneType' object has no attribute 'LogLevel'
gateway/platforms/feishu.py:4409 — lark.Client.builder() → same
gateway/platforms/feishu.py:4507 — if "GetMessageRequest" in globals(): return GetMessageRequest.builder()... — the guard returns True (the name IS in globals, bound to None), so the .builder() call hits AttributeError: 'NoneType' object has no attribute 'builder'.
Fix
After lazy-install, rebind every name the adapter uses. Roughly:
global FEISHU_AVAILABLE, lark, CreateFileRequest, CreateFileRequestBody, CreateImageRequest, CreateImageRequestBody, CreateMessageRequest, CreateMessageRequestBody, GetChatRequest, GetMessageRequest, GetMessageResourceRequest, P2ImMessageMessageReadV1, ReplyMessageRequest, ReplyMessageRequestBody, UpdateMessageRequest, UpdateMessageRequestBody, AccessTokenType, HttpMethod, FEISHU_DOMAIN, LARK_DOMAIN, BaseRequest, CallBackCard, P2CardActionTriggerResponse, EventDispatcherHandler, FeishuWSClient, GetApplicationRequest
...
import lark_oapi as _lark
from lark_oapi.api.application.v6 import GetApplicationRequest as _GAR
from lark_oapi.api.im.v1 import (CreateFileRequest as _CFR, ...)
...
lark = _lark
GetApplicationRequest = _GAR
CreateFileRequest = _CFR
# ...etc
FEISHU_AVAILABLE = True
(Or factor the imports into a helper that returns a dict and globals().update(...).)
3. Matrix — mautrix.types enums stay as stubs
gateway/platforms/matrix.py lines 42–94 import a set of types from mautrix.types (EventType, RoomID, EventID, ContentURI, SyncToken, UserID, PaginationDirection, PresenceState, RoomCreatePreset, TrustState). On ImportError they're bound to stub strings/classes. The new check_matrix_requirements() (lines 226–270) verifies with import mautrix # noqa but does not rebind any of those names.
MatrixAdapter._run calls client.add_event_handler(EventType.ROOM_MESSAGE, ...) (line 814) — after lazy-install in the same process, this passes the stub class's "m.room.message" string instead of the real mautrix.types.EventType.ROOM_MESSAGE enum member. Whether this functions depends on whether mautrix accepts the string by value, but at minimum:
TrustState.UNVERIFIED (line 697) — stub has UNVERIFIED = 0, real value comes from a different code path; mismatch.
RoomCreatePreset.PRIVATE (line 2271–2274) — stub returns the string "private_chat", mautrix expects its enum.
This is less catastrophic than slack/feishu because some matrix paths re-import locally inside methods (from mautrix.api import HTTPAPI at line 551), but the type-level checks throughout the long-running adapter event loop will misbehave.
Fix
Same shape as Feishu — declare and rebind every imported name from mautrix.types after the lazy-install succeeds.
4. (Bonus) Refactor opportunity
The six check_*_requirements() functions have grown into copy-paste variants of the same lazy-install template, and this issue is the direct consequence of "remembered to rebind name X but forgot name Y." A helper in tools/lazy_deps.py like:
def ensure_and_bind(key: str, importer: Callable[[], dict[str, Any]],
target_globals: dict, *, prompt: bool = False) -> bool:
"""ensure(key); then run importer() → dict of name→value;
target_globals.update(dict); return True on success."""
would centralize the pattern and make "did you remember every name" a one-line check rather than a per-platform audit.
Suggested test
tests/gateway/test_lazy_install_paths.py (new). For each of slack/feishu/matrix/dingtalk:
- Pre-poison the module's globals to
None / undefined for the names the adapter uses.
- Monkeypatch
tools.lazy_deps.ensure to a no-op (or have it actually run in CI).
- Call
check_*_requirements().
- Assert: every name listed in the test is bound to a non-
None value in the module's globals.
This would have caught the slack aiohttp and feishu lark gaps before merge, and gives a clean contract for any future platform added to LAZY_DEPS.
Repro priority
- Slack — guaranteed
NameError on first file upload after lazy-install, no Docker-restart workaround inside the same process.
- Feishu — guaranteed
TypeError on FeishuAdapter instantiation after lazy-install (the adapter is built immediately after check_feishu_requirements() returns True, so this fires before any user message is processed).
- Matrix — partial; depends on which code paths the deployment exercises.
DingTalk is unaffected — its rebind block is correct and serves as the reference.
Related
Summary
PR #25014 wired
tools.lazy_deps.ensure()into thecheck_*_requirements()functions for Slack, Matrix, DingTalk, and Feishu, matching the existing Discord/Telegram pattern. The plumbing is correct, but for three of the four adapters the module-level globals that the adapter actually uses are not rebound after lazy-install succeeds. A user whose deps are missing on first start will seelazy_deps.ensure()install everything fine,check_*_requirements()returnTrue, and then the adapter blow up at runtime withNameError/TypeError: 'NoneType' object is not callablebecause the names still point at stubs (or are unbound) from the original module-leveltry: ... except ImportError:block.DingTalk is the gold-standard reference — it explicitly rebinds every name it uses. Slack/Feishu/Matrix should match.
The bug is masked in Docker because the gateway typically restarts after first-run install, picking up real imports cleanly on the second start. But for any long-lived gateway process (most non-Docker deployments), this surfaces immediately.
1. Slack —
aiohttpnot rebound (NameError)gateway/platforms/slack.pylines 21–31 import four names at module top:AsyncApp,AsyncSocketModeHandler,AsyncWebClient, andaiohttp. PR #25014 specifically addedaiohttp==3.13.3toLAZY_DEPS["platform.slack"]so the lazy-install would pull it. But the newcheck_slack_requirements()(lines 75–102) declares:—
aiohttpis missing fromglobalAND from the rebind block. Because the originaltryraisesImportErroronimport slack_boltBEFORE reachingimport aiohttp, theexceptbranch leavesaiohttpunbound at module scope. The first call into_handle_file_upload-style code (gateway/platforms/slack.py:464usesaiohttp.ClientSession(), line 468 usesaiohttp.ClientTimeout(...)) raisesNameError: name 'aiohttp' is not defined.Minimal repro of the pattern:
Fix
Related:
pyproject.tomlslackextra divergenceslack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1"]does NOT includeaiohttp, butLAZY_DEPS["platform.slack"]now does.pip install hermes-agent[slack]still produces a slack adapter that NameErrors on file uploads — only the lazy-install path includes aiohttp. Pre-existing inconsistency, but easy to fix in the same PR — addaiohttp==3.13.3to theslackextra.2. Feishu —
lark_oapisymbols never rebound (TypeError on adapter init)gateway/platforms/feishu.pylines 86–124 import ~25 names fromlark_oapi(lark,CreateFileRequest,CreateMessageRequest,GetMessageRequest,EventDispatcherHandler,FeishuWSClient,FEISHU_DOMAIN,LARK_DOMAIN,CallBackCard,P2CardActionTriggerResponse,AccessTokenType,HttpMethod,BaseRequest, …). OnImportErrortheexceptbranch sets all of them toNone.The new
check_feishu_requirements()only rebindsFEISHU_AVAILABLE:So
lark_oapiends up imported intosys.modules, but the local module's globals (lark,CreateMessageRequest, etc.) stay bound toNone.FeishuAdapterinstantiation hits:gateway/platforms/feishu.py:4377—FeishuWSClient(...)→TypeError: 'NoneType' object is not callablegateway/platforms/feishu.py:4380—lark.LogLevel.INFO→AttributeError: 'NoneType' object has no attribute 'LogLevel'gateway/platforms/feishu.py:4409—lark.Client.builder()→ samegateway/platforms/feishu.py:4507—if "GetMessageRequest" in globals(): return GetMessageRequest.builder()...— the guard returnsTrue(the name IS in globals, bound toNone), so the.builder()call hitsAttributeError: 'NoneType' object has no attribute 'builder'.Fix
After lazy-install, rebind every name the adapter uses. Roughly:
(Or factor the imports into a helper that returns a dict and
globals().update(...).)3. Matrix —
mautrix.typesenums stay as stubsgateway/platforms/matrix.pylines 42–94 import a set of types frommautrix.types(EventType,RoomID,EventID,ContentURI,SyncToken,UserID,PaginationDirection,PresenceState,RoomCreatePreset,TrustState). OnImportErrorthey're bound to stub strings/classes. The newcheck_matrix_requirements()(lines 226–270) verifies withimport mautrix # noqabut does not rebind any of those names.MatrixAdapter._runcallsclient.add_event_handler(EventType.ROOM_MESSAGE, ...)(line 814) — after lazy-install in the same process, this passes the stub class's"m.room.message"string instead of the realmautrix.types.EventType.ROOM_MESSAGEenum member. Whether this functions depends on whethermautrixaccepts the string by value, but at minimum:TrustState.UNVERIFIED(line 697) — stub hasUNVERIFIED = 0, real value comes from a different code path; mismatch.RoomCreatePreset.PRIVATE(line 2271–2274) — stub returns the string"private_chat", mautrix expects its enum.This is less catastrophic than slack/feishu because some matrix paths re-import locally inside methods (
from mautrix.api import HTTPAPIat line 551), but the type-level checks throughout the long-running adapter event loop will misbehave.Fix
Same shape as Feishu — declare and rebind every imported name from
mautrix.typesafter the lazy-install succeeds.4. (Bonus) Refactor opportunity
The six
check_*_requirements()functions have grown into copy-paste variants of the same lazy-install template, and this issue is the direct consequence of "remembered to rebind name X but forgot name Y." A helper intools/lazy_deps.pylike:would centralize the pattern and make "did you remember every name" a one-line check rather than a per-platform audit.
Suggested test
tests/gateway/test_lazy_install_paths.py(new). For each of slack/feishu/matrix/dingtalk:None/ undefined for the names the adapter uses.tools.lazy_deps.ensureto a no-op (or have it actually run in CI).check_*_requirements().Nonevalue in the module's globals.This would have caught the slack
aiohttpand feishularkgaps before merge, and gives a clean contract for any future platform added to LAZY_DEPS.Repro priority
NameErroron first file upload after lazy-install, no Docker-restart workaround inside the same process.TypeErroronFeishuAdapterinstantiation after lazy-install (the adapter is built immediately aftercheck_feishu_requirements()returnsTrue, so this fires before any user message is processed).DingTalk is unaffected — its rebind block is correct and serves as the reference.
Related
.venvpermissions fix that made lazy-install viable in Dockertools/lazy_deps.py—LAZY_DEPSregistrygateway/platforms/discord.py— reference implementation (rebinds all 4 names)gateway/platforms/dingtalk.py— reference implementation introduced in fix(gateway): add lazy_deps.ensure() to slack, matrix, dingtalk, feishu adapters #25014 (rebinds all 6 names correctly)