# NousResearch/hermes-agent · Feishu channel_prompt wiring and None-guard fixes

## Open now

- Nothing open on GitHub.

These issues and PRs center on the Feishu adapter missing channel_prompt resolution that other adapters (Discord, Slack) already have, plus a related AttributeError when event.channel_prompt is None. Some members also touch Anthropic max_tokens fallback and Discord free_response as adjacent fixes in the same changesets.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #55958. Verify it addresses your case.
- **Recommended:** read PR #55958 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.34`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #12805: Feishu adapter missing channel_prompt resolution (unlike Discord/Slack)
- #23682: AttributeError: 'NoneType' object has no 'channel_prompt' attribute

## Prior & competing PRs

- **#23693: fix: guard event.channel_prompt access against None event**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `36bfe3a4490259eb8f89a84b7a9cad2a7c4de8ab`, `90a6336145cc48bcff945b159b7a9719dc933ef1`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#55958: fix(anthropic+feishu): model-gate max_tokens fallback; wire Feishu channel_prompt**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/chat_completion_helpers.py`, `plugins/platforms/feishu/adapter.py`, `tests/gateway/test_feishu_channel_prompts.py`
  - fix hunks:
  - `agent/chat_completion_helpers.py`

    ```diff
    @@ -741,14 +741,26 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
         if agent.provider_data_collection:
             _prefs["data_collection"] = agent.provider_data_collection
     
    -    # Claude max-output override on aggregators
    +    # Anthropic-compatible max-output fallback (last resort only — applied in
    +    # build_kwargs *after* ephemeral/user/profile max_tokens, never overriding
    +    # an explicit value).  Model-gated, not URL-gated: any chat-completions
    +    # proxy serving a Claude/MiniMax/Qwen3 model needs max_tokens, because the
    +    # Anthropic Messages API treats it as mandatory and proxies that omit it
    +    # (AWS Bedrock, NVIDIA, LiteLLM, vLLM, corporate gateways) default as low
    +    # as 4096 output tokens — easily exhausted by thinking + large tool calls
    +    # like write_file/patch.  OpenRouter/Nous were the only routes covered
    +    # before; gating on _ANTHROPIC_OUTPUT_LIMITS membership covers them all.
         _ant_max = None
    -    if (_is_or or _is_nous) and "claude" in (agent.model or "").lower():
    -        try:
    -            from agent.anthropic_adapter import _get_anthropic_max_output
    +    try:
    +        from agent.anthropic_adapter import (
    +            _get_anthropic_max_output,
    +            _ANTHROPIC_OUTPUT_LIMITS,
    +        )
    +        _model_norm = (agent.model or "").lower().replace(".", "-")
    +        if any(key in _model_norm for key in _ANTHROPIC_OUTPUT_LIMITS):
                 _ant_max = _get_anthropic_max_output(agent.model)
    -        except Exception:
    -            pass
    +    except Exception:
    +        pass
     
         # Qwen session metadata
         _qwen_meta = None
    ```
  - `plugins/platforms/feishu/adapter.py`

    ```diff
    @@ -2892,6 +2892,7 @@ async def _handle_reaction_event(self, event_type: str, data: Any) -> None:
                 source=source,
                 raw_message=data,
                 message_id=message_id,
    +            channel_prompt=self._resolve_channel_prompt(chat_id),
                 timestamp=datetime.now(),
             )
             logger.info("[Feishu] Routing reaction %s:%s on bot message %s as synthetic event", action, emoji_type, message_id)
    ```

    ```diff
    @@ -2954,6 +2955,7 @@ async def _handle_card_action_event(self, data: Any) -> None:
                 source=source,
                 raw_message=data,
                 message_id=token or str(uuid.uuid4()),
    +            channel_prompt=self._resolve_channel_prompt(chat_id),
                 timestamp=datetime.now(),
             )
             logger.info("[Feishu] Routing card action %r from %s in %s as synthetic command", action_tag, open_id, chat_id)
    ```

    ```diff
    @@ -3156,6 +3158,18 @@ def _clear_webhook_anomaly(self, remote_ip: str) -> None:
         # Inbound processing pipeline
         # =========================================================================
     
    +    def _resolve_channel_prompt(self, chat_id: str, parent_id: str | None = None) -> str | None:
    +        """Resolve a Feishu per-channel system prompt.
    +
    +        Mirrors the Discord/Slack behaviour so ``channel_prompts: {<chat_id>:
    +        "<prompt>"}`` in ``PlatformConfig.extra`` is honoured for Feishu chats
    +        instead of being silently ignored.
    +        """
    +        from gateway.platforms.base import resolve_channel_prompt
    +        _config = getattr(self, "config", None)
    +        _extra = getattr(_config, "extra", None) or {}
    +        return resolve_channel_prompt(_extra, chat_id, parent_id)
    +
         async def _process_inbound_message(
             self,
             *,
    ```

    ```diff
    @@ -3233,6 +3247,7 @@ async def _process_inbound_message(
                 media_types=media_types,
                 reply_to_message_id=reply_to_message_id,
                 reply_to_text=reply_to_text,
    +            channel_prompt=self._resolve_channel_prompt(chat_id, thread_id or None),
                 timestamp=datetime.now(),
             )
             await self._dispatch_inbound_event(normalized)
    ```
  - `tests/gateway/test_feishu_channel_prompts.py`

    ```diff
    @@ -0,0 +1,88 @@
    +"""Tests for Feishu per-channel prompt resolution.
    +
    +Feishu previously ignored ``channel_prompts`` config (unlike Discord/Slack).
    +These tests verify that ``_resolve_channel_prompt`` reads the adapter's
    +``config.extra`` and that the resolved prompt is attached to the dispatched
    +``MessageEvent`` for the inbound, reaction, and card-action paths.
    +"""
    +
    +import asyncio
    +import json
    +from types import SimpleNamespace
    +from unittest.mock import AsyncMock, Mock
    +
    +from gateway.config import PlatformConfig
    +
    +
    +def _build_adapter(extra=None):
    +    from plugins.platforms.feishu.adapter import FeishuAdapter
    +
    +    adapter = FeishuAdapter.__new__(FeishuAdapter)
    +    adapter.config = PlatformConfig(extra=extra or {})
    +    adapter._bot_open_id = "ou_bot"
    +    adapter._bot_user_id = ""
    +    adapter._bot_name = "Hermes"
    +    adapter._download_feishu_message_resources = AsyncMock(return_value=([], []))
    +    adapter._fetch_message_text = AsyncMock(return_value=None)
    +    adapter.get_chat_info = AsyncMock(return_value={"name": "Test Chat"})
    +    adapter._resolve_sender_profile = AsyncMock(
    +        return_value={"user_id": "u1", "user_name": "Alice", "user_id_alt": None}
    +    )
    +    adapter._resolve_source_chat_type = Mock(return_value="group")
    +    adapter.build_source = Mock(return_value=SimpleNamespace(thread_id=None))
    +    adapter._dispatch_inbound_event = AsyncMock()
    +    return adapter
    +
    +
    +def _run_inbound(adapter, chat_id="oc_chat"):
    +    message = SimpleNamespace(
    +        content=json.dumps({"text": "plain message"}),
    +        message_type="text",
    +        message_id="m",
    +        mentions=[],
    +        chat_id=chat_id,
    +        parent_id=None,
    +        upper_message_id=None,
    +        thread_id=None,
    +    )
    +    asyncio.run(
    +        adapter._process_inbound_message(
    +            data=message, message=message, sender_id=None, chat_type="group", message_id="m",
    +        )
    +    )
    +    return adapter._dispatch_inbound_event.call_args.args[0]
    +
    +
    +def test_resolve_channel_prompt_exact_match():
    +    adapter = _build_adapter({"channel_prompts": {"oc_chat": "Be terse."}})
    +    assert adapter._resolve_channel_prompt("oc_chat") == "Be terse."
    +
    +
    +def test_resolve_channel_prompt_parent_fallback():
    +    adapter = _build_adapter({"channel_prompts": {"oc_parent": "Inherit me."}})
    +    assert adapter._resolve_channel_prompt("oc_thread", "oc_parent") == "Inherit me."
    +
    +
    +def test_resolve_channel_prompt_no_match_returns_none():
    +    adapter = _build_adapter({"channel_prompts": {"oc_other": "Nope."}})
    +    assert adapter._resolve_channel_prompt("oc_chat") is None
    +
    +
    +def test_resolve_channel_prompt_missing_config_is_safe():
    +    # __new__ adapter without a config attribute (defensive getattr path).
    +    from plugins.platforms.feishu.adapter import FeishuAdapter
    +
    +    bare = FeishuAdapter.__new__(FeishuAdapter)
    +    assert bare._resolve_channel_prompt("oc_chat") is None
    +
    +
    +def test_inbound_event_carries_channel_prompt():
    +    adapter = _build_adapter({"channel_prompts": {"oc_chat": "Feishu role prompt."}})
    +    event = _run_inbound(adapter, chat_id="oc_chat")
    +    assert event.channel_prompt == "Feishu role prompt."
    +
    +
    +def test_inbound_event_no_prompt_when_unconfigured():
    +    adapter = _build_adapter({"channel_prompts": {"oc_other": "Different chat."}})
    +    event = _run_inbound(adapter, chat_id="oc_chat")
    +    assert event.channel_prompt is None
    ```
- **#12811: fix: Anthropic max_tokens + Feishu channel_prompt + Discord free_response**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#12790`, `#12805`, `#12750`, `#55958`, `#12790`, `#12805`, `#12750`, `#55958`

## Fix status

- Merged to main in PR #55958 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-01`.
- Subsystem: `gateway`
