fix(gateway): stream consumer first message drops thread context (salvage of #13077)#23437
Merged
Conversation
Cherry-picked from PR #13077 commits: - 5500c7d8 fix(gateway): stream consumer first message drops thread context - e84403b9 test(gateway): add regression tests for stream consumer thread routing Fixes: Streaming first message drops thread/topic context in Feishu group topics, Slack threads, Telegram forum topics. Adds initial_reply_to_id ctor arg to GatewayStreamConsumer, threaded through _send_or_edit and _send_new_chunk. Also fixes Feishu _send_raw_message fallback path (reply -> create) to use receive_id_type='thread_id' so the new message lands in the correct topic instead of the main channel. Authored by hrygo via PR #13077 (re-attributed from the bot-authored salvage commit on the original branch).
…path When the first streamed message exceeds the platform length limit and gets split into chunks, _send_new_chunk was called with self._message_id (which is None on first send), dropping thread routing entirely. Fallback to self._initial_reply_to_id so overflow chunks land in the correct topic/thread. Also fix a fragile test assertion that could be silently skipped.
The contributor's regression test for Feishu fallback thread routing asserted on attributes specific to the real lark SDK builder (call_args.body, body.receive_id). In test environments without the lark SDK installed, the in-tree fallback (gateway/platforms/feishu.py _build_create_message_request) returns a SimpleNamespace using .request_body instead of .body, causing AttributeError. Now reads via getattr fallback and also verifies receive_id_type is 'thread_id' (not 'chat_id') as a stronger contract check.
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-argument-type |
8 |
unresolved-attribute |
3 |
unsupported-operator |
3 |
unresolved-import |
1 |
not-subscriptable |
1 |
First entries
tests/agent/test_codex_cloudflare_headers.py:163: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str & ~AlwaysFalsy`, `int & ~AlwaysFalsy` in union `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 3 union elements`
run_agent.py:7160: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `str | dict[Unknown, Unknown] | Any | ... omitted 3 union elements`
tests/agent/test_codex_cloudflare_headers.py:181: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["originator"]` and `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 3 union elements`
run_agent.py:13287: [invalid-argument-type] invalid-argument-type: Argument to function `len` is incorrect: Expected `Sized`, found `(str & ~AlwaysFalsy) | (dict[Unknown, Unknown] & ~AlwaysFalsy) | (Any & ~AlwaysFalsy) | ... omitted 3 union elements`
tests/agent/test_codex_cloudflare_headers.py:163: [unresolved-attribute] unresolved-attribute: Attribute `startswith` is not defined on `dict[str, str]` in union `Unknown | str | dict[str, str]`
tests/run_agent/test_provider_attribution_headers.py:155: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["X-OpenRouter-Cache"]` and `Unknown | str | dict[str, str] | ... omitted 3 union elements`
run_agent.py:13284: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `str | dict[Unknown, Unknown] | Any | ... omitted 3 union elements`
tests/gateway/test_stream_consumer_thread_routing.py:13: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_provider_attribution_headers.py:90: [unresolved-attribute] unresolved-attribute: Attribute `startswith` is not defined on `dict[str, str]` in union `Unknown | str | dict[str, str]`
tests/run_agent/test_provider_attribution_headers.py:154: [not-subscriptable] not-subscriptable: Cannot subscript object of type `int` with no `__getitem__` method
run_agent.py:2339: [invalid-argument-type] invalid-argument-type: Argument to function `query_ollama_num_ctx` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | (Any & ~AlwaysFalsy) | ... omitted 4 union elements`
run_agent.py:2590: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 4 union elements`
run_agent.py:6989: [invalid-argument-type] invalid-argument-type: Argument to function `_codex_cloudflare_headers` is incorrect: Expected `str`, found `Unknown | str | dict[str, str] | ... omitted 3 union elements`
run_agent.py:2593: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 4 union elements`
tests/run_agent/test_provider_attribution_headers.py:156: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["X-OpenRouter-Cache-TTL"]` and `Unknown | str | dict[str, str] | ... omitted 3 union elements`
run_agent.py:2641: [invalid-argument-type] invalid-argument-type: Argument to function `get_model_context_length` is incorrect: Expected `str`, found `str | dict[str, str] | Any | ... omitted 3 union elements`
✅ Fixed issues (9):
| Rule | Count |
|---|---|
invalid-argument-type |
8 |
unresolved-attribute |
1 |
First entries
run_agent.py:2339: [invalid-argument-type] invalid-argument-type: Argument to function `query_ollama_num_ctx` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | (Any & ~AlwaysFalsy) | ... omitted 5 union elements`
run_agent.py:2641: [invalid-argument-type] invalid-argument-type: Argument to function `get_model_context_length` is incorrect: Expected `str`, found `str | dict[str, str] | Any | ... omitted 4 union elements`
run_agent.py:7160: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `str | dict[Unknown, Unknown] | Any | ... omitted 4 union elements`
run_agent.py:13284: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `str | dict[Unknown, Unknown] | Any | ... omitted 4 union elements`
run_agent.py:2593: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 5 union elements`
run_agent.py:2590: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 5 union elements`
run_agent.py:13287: [invalid-argument-type] invalid-argument-type: Argument to function `len` is incorrect: Expected `Sized`, found `(str & ~AlwaysFalsy) | (dict[Unknown, Unknown] & ~AlwaysFalsy) | (Any & ~AlwaysFalsy) | ... omitted 4 union elements`
run_agent.py:6989: [invalid-argument-type] invalid-argument-type: Argument to function `_codex_cloudflare_headers` is incorrect: Expected `str`, found `Unknown | str | dict[str, str] | dict[Unknown, Unknown] | Divergent`
tests/agent/test_codex_cloudflare_headers.py:163: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str & ~AlwaysFalsy` in union `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | dict[Unknown, Unknown] | Divergent`
Unchanged: 4239 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
This was referenced May 10, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a user messages the bot in a Telegram forum topic / Feishu group topic / Slack thread and the bot's first streamed reply runs, the reply used to drop into the wrong topic (or General) because the
GatewayStreamConsumer's firstadapter.send()carried noreply_to. After this PR, the original message id is threaded through and the streamed first message lands in the correct topic.What was wrong
gateway/stream_consumer.pyconstructs the consumer at message-handling time but the constructor took noinitial_reply_to_id. The first_send_or_editcall therefore went out withreply_to=None, which on Telegram forums / Feishu topics / Slack threads means "main channel." Subsequent edits inherited the wrong message id chain.A second site — the overflow split-send path on the first message — passed
self._message_id(which isNoneon first send) instead of falling back toinitial_reply_to_id.The Feishu adapter's
_send_raw_messagereply→create fallback usedchat_idinstead ofthread_ideven when athread_idwas available in metadata, leaking topic-bound replies into the main channel after an upstream reply failure.Fix
initial_reply_to_idctor arg toGatewayStreamConsumer; threaded through_send_or_editand_send_new_chunk.self._message_id or self._initial_reply_to_id._send_raw_message: when reply→create falls back AND metadata carries athread_id, build the create request withreceive_id_type='thread_id'so the new message lands in the topic.Salvage notes
5500c7d8,e84403b9, plus follow-up3b11463b).hermes-agent <hermes-agent@users.noreply.github.com>(a bot/placeholder); re-attributed tohrygo <26729613+hrygo@users.noreply.github.com>so the contributor gets release-notes credit.gateway/stream_consumer.py:362-380against the just-landed fix(gateway): streaming mode silently drops final response when already_sent is true #10748 fix (PR fix(gateway): only mark final response sent when split-overflow chunks actually land #23420). Both changes are complementary: fix(gateway): streaming mode silently drops final response when already_sent is true #10748 tracks per-chunk delivery to set_final_response_sentcorrectly; this PR threadsinitial_reply_to_idto the same chunk loop. Merged as one block —reply_to = self._message_id or self._initial_reply_to_id, with delivery tracking preserved.call_args.body) and SDK-absent (call_args.request_bodySimpleNamespace fallback) paths. Added a stronger contract check thatreceive_id_type == 'thread_id'.Validation
tests/gateway/test_stream_consumer.py+tests/gateway/test_stream_consumer_thread_routing.py: 93/93 pass in 3.8s.Note on issue #7355
This PR closes failure path C (stream consumer first message). Failure path B (auto-reset notice fallback in
gateway/run.py:6930) is addressed in a sibling salvage of #7404. Path A (final response chunk) was already fixed on main via the_status_thread_metadataplumbing.Closes #7355 (via this + #7404 salvage).
Authored by @hrygo via PR #13077; cleanup commits and AUTHOR_MAP entry follow.