Skip to content

fix(feishu): comprehensive topic thread reply fix (A+B+C+D) - #18121

Closed
highland0971 wants to merge 4 commits into
NousResearch:mainfrom
highland0971:fix/feishu-topic-thread-comprehensive
Closed

fix(feishu): comprehensive topic thread reply fix (A+B+C+D)#18121
highland0971 wants to merge 4 commits into
NousResearch:mainfrom
highland0971:fix/feishu-topic-thread-comprehensive

Conversation

@highland0971

Copy link
Copy Markdown

What does this PR do?

Fixes Feishu/Lark bot replies creating new topics instead of staying inside the existing topic thread in topic-mode groups (话题模式群).

This is a comprehensive fix that addresses all four root-cause layers simultaneously, combining insights from 10 previous community PRs that each addressed only part of the problem.

Root Cause Analysis

The bug has four interrelated causes:

  1. Inbound (A): Feishu topic messages have root_id but not thread_id. The code only read thread_id, so topic context was silently lost.

  2. Outbound (B): When reply_to is absent (progress/status/approval messages), _send_raw_message falls through to im.v1.message.create with receive_id_type="chat_id", which creates a new topic instead of posting inside the existing one.

  3. Gateway (C): Mid-turn messages (stream consumer first-send, progress updates, status callbacks, interim commentary, background review, approval cards) all lack reply_to, so they can't use the reply API to stay in-thread.

  4. Defense (D): When a reply fails (message withdrawn, error codes 230011/231003), the code falls back to create with reply_to=None, which spawns a new topic in topic-mode groups.

Changes Made

A. Inbound: root_idthread_id fallback (feishu.py)

  • _process_inbound_message: extract root_id and use it as fallback for both thread_id (topic routing) and reply_to_message_id (reply anchor chain)

B. Outbound: om_/omt_ ID prefix handling (feishu.py)

  • _send_raw_message: distinguish Feishu's two ID prefixes:
    • om_ (message ID) → use as reply_to via reply API (most reliable in-thread delivery)
    • omt_ (topic/thread ID) → use receive_id_type="thread_id" via create API
    • no thread_id → fall back to chat_id (unchanged behavior)

C. Gateway: propagate reply_to to all mid-turn messages

  • stream_consumer.py: new initial_reply_to_id parameter on GatewayStreamConsumer; used in first-send, fallback-final, and commentary paths
  • run.py: pass event_message_id as initial_reply_to_id for Feishu topic threads; add _progress_reply_to for send_progress_messages and all status/callback sends

D. Defense: reply failure fallback stays in thread (feishu.py)

  • _feishu_send_with_retry: when reply fails with 230011/231003, if thread_id is an om_ ID, retry reply to it instead of falling back to create (which would spawn a new topic)

Related PRs

This supersedes and consolidates:

How to Test

  1. Create or use a Feishu group with topic mode enabled (话题模式)
  2. Open an existing topic and @mention the bot
  3. Verify the bot's final reply appears inside the same topic ✅
  4. Verify tool progress messages (e.g. "🔍 Searching...") appear inside the same topic ✅
  5. Verify streaming partial responses appear inside the same topic ✅
  6. Verify status/approval messages appear inside the same topic ✅
  7. Test in a P2P chat with topics — same behavior ✅
  8. Run: python -m pytest tests/gateway/test_feishu.py tests/gateway/test_stream_consumer.py -q

Test Results

tests/gateway/test_feishu.py: 189 passed
tests/gateway/test_stream_consumer.py: 81 passed

Real-world verification: Deployed to a production Hermes instance and tested in both group-topic and P2P-topic scenarios — all message types (final response, tool progress, streaming, approval cards) correctly stay inside the originating topic thread.

highland0971 and others added 4 commits April 28, 2026 11:16
- feat(feishu): add markdown table rendering support
- fix(feishu): silent message drop handling
- fix(feishu): ensure replies stay inside topic threads in group chats
- Set NO_COLOR=1 env var to disable colors in agent-browser CLI
- Add fallback regex to strip any remaining ANSI sequences
- Fixes JSON parse failures from escape codes like \x1b[A (cursor up)
Previous regex \x1b/[a-z]* was greedy and consumed trailing letters.
Fixed to \x1b/ which only matches the ESC+/ sequence.

Example: \x1b/tabs now correctly becomes 'tabs' instead of ''.

Also expanded pattern to handle:
- CSI sequences with private mode params (\x1b[?25h)
- OSC sequences (\x1b]...BEL)
- Single ESC+letter sequences (\x1bM)
- Non-standard ESC+/ sequences
Fixes Feishu bot replies creating new topics instead of staying inside
the existing topic thread in topic-mode groups.

A. Inbound: root_id → thread_id + reply_to_message_id fallback
B. Outbound: om_/omt_ ID prefix handling, thread_id delivery via create
C. Gateway: initial_reply_to_id in StreamConsumer, reply_to in callbacks
D. Defense: reply failure fallback stays in thread (no new topic spawn)
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/feishu Feishu / Lark adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants