Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion gateway/platforms/feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,10 @@ def _build_event_handler(self) -> Any:
lambda data: self._on_reaction_event("im.message.reaction.deleted_v1", data)
)
.register_p2_card_action_trigger(self._on_card_action_trigger)
# Suppress noisy SDK errors for unhandled events (#4789)
.register_p2_im_chat_access_event_bot_p2p_chat_entered_v1(
lambda data: None # Ignore bot p2p chat entered events
)
.build()
)

Expand Down Expand Up @@ -1477,14 +1481,36 @@ def format_message(self, content: str) -> str:
def _on_message_event(self, data: Any) -> None:
"""Normalize Feishu inbound events into MessageEvent."""
if self._loop is None:
logger.warning("[Feishu] Dropping inbound message before adapter loop is ready")
# WebSocket callback fires before the main loop is ready.
# Queue the event and retry after a short delay.
if not hasattr(self, "_pending_events"):
self._pending_events = []
self._pending_events.append(data)
threading.Thread(target=self._drain_pending_events, daemon=True).start()
return
future = asyncio.run_coroutine_threadsafe(
self._handle_message_event_data(data),
self._loop,
)
future.add_done_callback(self._log_background_failure)

def _drain_pending_events(self) -> None:
"""Wait for the main loop, then replay queued events."""
import time
deadline = time.time() + 5
while time.time() < deadline:
loop = getattr(self, "_loop", None)
if loop:
for ev in getattr(self, "_pending_events", []):
asyncio.run_coroutine_threadsafe(
self._handle_message_event_data(ev), loop
)
self._pending_events = []
return
time.sleep(0.3)
logger.error("[Feishu] Loop never ready — dropped %d queued messages", len(getattr(self, "_pending_events", [])))
self._pending_events = []

async def _handle_message_event_data(self, data: Any) -> None:
"""Shared inbound message handling for websocket and webhook transports."""
event = getattr(data, "event", None)
Expand Down
1 change: 1 addition & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2628,6 +2628,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str):
"platform": source.platform.value if source.platform else "",
"user_id": source.user_id,
"session_id": session_entry.session_id,
"chat_id": source.chat_id,
"message": message_text[:500],
}
await self.hooks.emit("agent:start", hook_ctx)
Expand Down
9 changes: 9 additions & 0 deletions hermes_cli/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,15 @@ def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False):
"""
sys.path.insert(0, str(PROJECT_ROOT))

# Prevent multiple concurrent gateway instances (protects Feishu WebSocket)
# Multiple processes share lark_oapi global state → only last one receives events
if not replace and os.environ.get("HERMES_GATEWAY_REPLACING") != "1":
existing_pids = find_gateway_pids()
if existing_pids:
print(f"⚠️ Gateway already running (PIDs: {existing_pids}). Aborting.")
print(f" To replace it, run: hermes gateway run --replace")
sys.exit(1)

from gateway.run import start_gateway

print("┌─────────────────────────────────────────────────────────┐")
Expand Down