You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On Feishu, an agent-generated image or file reply is silently lost when the reply target has been withdrawn/recalled/deleted, because sendImageFeishu and sendFileFeishu do a bare client.im.message.reply(...) with none of the withdrawn/not-found top-level fallback that text and card replies get. A text reply to the same withdrawn target recovers by falling back to a top-level client.im.message.create, but the media reply throws and no message is ever delivered.
Surface: Feishu plugin outbound media delivery (extensions/feishu/src/media.ts)
Steps to reproduce
Agent replies into a Feishu chat quoting a specific message (replyToMessageId set), non-thread reply (or a reply where a top-level fallback is otherwise allowed).
The quoted message is withdrawn/recalled/deleted before the reply is sent, so Feishu returns code 230011 (The message was withdrawn.) or 231003 (The message is not found), or throws the equivalent SDK/Axios error.
If the reply is text or a card, delivery survives: the send falls back to a top-level im.message.create.
If the reply carries an image or file (sendMediaFeishu -> sendImageFeishu / sendFileFeishu), the send throws and the media is never delivered.
Expected
The media reply inherits the same withdrawn/not-found handling as text/card: when the reply target is unavailable and a top-level fallback is allowed, fall back to a top-level im.message.create so the image/file still reaches the chat.
Actual
sendImageFeishu / sendFileFeishu throw (Feishu image reply failed: ... / Feishu file reply failed: ...) and im.message.create is never called. The agent-generated media is dropped with no delivery.
Root cause
Text and card replies route through sendReplyOrFallbackDirect (extensions/feishu/src/send.ts:146), which both catches a withdrawn/not-found thrown error and inspects the response body:
sendMessageFeishu (send.ts) and sendCardFeishu (send.ts) both call it. The media path does not. sendImageFeishu (extensions/feishu/src/media.ts:517) and sendFileFeishu (extensions/feishu/src/media.ts:571) run a bare reply plus assertFeishuMessageApiSuccess with neither guard:
sendMediaFeishu forwards replyToMessageId straight into these unguarded paths, so a withdrawn/not-found target throws and the media is dropped.
Scope
Fires on non-thread replies, or when a top-level fallback is otherwise allowed. This is the same class the text/card fallback was built to recover, added in the merged PR fix(feishu): fall back from missing thread replies #80306 (which touched send.ts and reply-dispatcher.ts but not media.ts).
For a native reply_in_thread reply WITHOUT allowTopLevelReplyFallback, the text path also deliberately throws (replyTargetFallbackError, send.ts) rather than cross a thread boundary. So the fair claim is that plain group/DM media replies lose media where text recovers, not that text always recovers.
Feishu-only, one media message per event, and requires a withdrawn/deleted/not-found reply target, so this is a P2 data-loss bug rather than a broad outage.
Sibling surfaces
sendImageFeishu and sendFileFeishu share the same missing guard; both are covered above.
Behavior addressed: Feishu image/file replies to a withdrawn/not-found reply target are dropped where a text reply to the same target survives via top-level fallback.
Real environment tested: drove the real sendMessageFeishu, sendImageFeishu, and sendFileFeishu at commit 518937b, with only the Feishu network client stubbed at its narrowest seam (resolveFeishuSendTarget), so reply() returns/throws Feishu withdrawn code 230011 and create() succeeds; every send function, the fallback decision, and sendMediaFeishu routing stayed real.
Exact steps or command run after this patch: drive text, image, and file replies to the same withdrawn (code 230011) reply target and record whether the top-level create() was reached.
Evidence after fix:
# OBSERVED (buggy, origin/main 518937be4e)
TEXT reply withdrawn(230011): create called = 1, messageId = om_text_fallback
IMAGE reply withdrawn(230011): threw = "Feishu image reply failed: The message was withdrawn.", create called = 0
FILE reply withdrawn-throw(230011): threw = "Feishu file reply failed: {\"message\":\"The message was withdrawn.\"}", create called = 0
# EXPECTED (identical inputs, media path routed through the same withdrawn/not-found fallback)
IMAGE reply withdrawn(230011): create called = 1, messageId = om_image_fallback
Observed result after fix: on origin/main the text reply survives (top-level create reached) while the image and file replies throw and never reach create, so the media is lost; routing the media reply through the same fallback makes the image survive on identical inputs.
What was not tested: not exercised against the live Feishu Open Platform API (the withdrawn code and error shapes are modeled from the existing WITHDRAWN_REPLY_ERROR_CODES set and send.reply-fallback fixtures); native reply_in_thread without allowTopLevelReplyFallback is intentionally out of scope since the text path also throws there by design.
Summary
On Feishu, an agent-generated image or file reply is silently lost when the reply target has been withdrawn/recalled/deleted, because
sendImageFeishuandsendFileFeishudo a bareclient.im.message.reply(...)with none of the withdrawn/not-found top-level fallback that text and card replies get. A text reply to the same withdrawn target recovers by falling back to a top-levelclient.im.message.create, but the media reply throws and no message is ever delivered.Environment
extensions/feishu/src/media.ts)Steps to reproduce
replyToMessageIdset), non-thread reply (or a reply where a top-level fallback is otherwise allowed).The message was withdrawn.) or 231003 (The message is not found), or throws the equivalent SDK/Axios error.im.message.create.sendMediaFeishu->sendImageFeishu/sendFileFeishu), the send throws and the media is never delivered.Expected
The media reply inherits the same withdrawn/not-found handling as text/card: when the reply target is unavailable and a top-level fallback is allowed, fall back to a top-level
im.message.createso the image/file still reaches the chat.Actual
sendImageFeishu/sendFileFeishuthrow (Feishu image reply failed: .../Feishu file reply failed: ...) andim.message.createis never called. The agent-generated media is dropped with no delivery.Root cause
Text and card replies route through
sendReplyOrFallbackDirect(extensions/feishu/src/send.ts:146), which both catches a withdrawn/not-found thrown error and inspects the response body:sendMessageFeishu(send.ts) andsendCardFeishu(send.ts) both call it. The media path does not.sendImageFeishu(extensions/feishu/src/media.ts:517) andsendFileFeishu(extensions/feishu/src/media.ts:571) run a bare reply plusassertFeishuMessageApiSuccesswith neither guard:sendMediaFeishuforwardsreplyToMessageIdstraight into these unguarded paths, so a withdrawn/not-found target throws and the media is dropped.Scope
send.tsandreply-dispatcher.tsbut notmedia.ts).reply_in_threadreply WITHOUTallowTopLevelReplyFallback, the text path also deliberately throws (replyTargetFallbackError, send.ts) rather than cross a thread boundary. So the fair claim is that plain group/DM media replies lose media where text recovers, not that text always recovers.Sibling surfaces
sendImageFeishuandsendFileFeishushare the same missing guard; both are covered above.outbound.ts/reply-dispatcher.ts; this is a withdrawn/recalled reply-target fallback missing frommedia.ts. The prior withdrawn-reply fixes (fix(feishu): fall back from missing thread replies #80306, and closed fix(feishu): fall back to direct send when reply target is withdrawn #27614/Feishu: recover from withdrawn reply targets #29980/Feishu: fallback to create send when reply target is withdrawn #30444/fix(feishu): catch thrown SDK errors for withdrawn reply targets #33515) only ever touched the text/card path insend.ts.Real behavior proof
Behavior addressed: Feishu image/file replies to a withdrawn/not-found reply target are dropped where a text reply to the same target survives via top-level fallback.
Real environment tested: drove the real
sendMessageFeishu,sendImageFeishu, andsendFileFeishuat commit 518937b, with only the Feishu network client stubbed at its narrowest seam (resolveFeishuSendTarget), soreply()returns/throws Feishu withdrawn code 230011 andcreate()succeeds; every send function, the fallback decision, andsendMediaFeishurouting stayed real.Exact steps or command run after this patch: drive text, image, and file replies to the same withdrawn (code 230011) reply target and record whether the top-level
create()was reached.Evidence after fix:
Observed result after fix: on origin/main the text reply survives (top-level create reached) while the image and file replies throw and never reach create, so the media is lost; routing the media reply through the same fallback makes the image survive on identical inputs.
What was not tested: not exercised against the live Feishu Open Platform API (the withdrawn code and error shapes are modeled from the existing
WITHDRAWN_REPLY_ERROR_CODESset andsend.reply-fallbackfixtures); nativereply_in_threadwithoutallowTopLevelReplyFallbackis intentionally out of scope since the text path also throws there by design.