Repository navigation
Conversation
|
Codex review: needs changes before merge. Reviewed August 29, 2026, 7:00 PM ET / 23:00 UTC. ClawSweeper reviewWhat this changesThe PR sends Feishu replies with more than five Markdown tables through post delivery instead of a static card that Feishu rejects. Regression provenancePossible regression — suspected (reviewed change). No predecessor PR is attributed. Merge readinessKeep this PR open: its static-card fallback addresses a real Feishu delivery failure, but the new card-wide counter also rejects deliverable font-wrapped context cards. The prior P2 finding remains unfixed at the exact reviewed head. Priority: P1 Review scores
Verification
How this fits togetherThe Feishu plugin turns agent replies and action presentations into either static interactive cards or post messages. This change selects post delivery when a static card would exceed Feishu’s table-component limit. flowchart LR
A[Agent reply or action] --> B[Feishu plugin]
B --> C{Static card eligible?}
C -->|Within table limit| D[Interactive card]
C -->|Over limit| E[Post renderer]
D --> F[Feishu chat]
E --> F
Before merge
Findings
Agent review detailsSecurityNone. PR surfaceSource +92, Tests +386. Total +478 across 8 files. View PR surface stats
Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge-risk optionsMaintainer options:
Copy recommended automerge instructionTechnical reviewBest possible solution: Keep the shared static-card guard, but make its card-level accounting preserve the renderer-owned font-wrapped context exception and lock it down through both presentation delivery routes. Do we have a high-confidence way to reproduce the issue? No direct current-main run was performed in this read-only review; source and the supplied real Feishu API evidence establish the static-card failure and the introduced context-card regression path. Is this the best way to solve the issue? No. The shared parser-backed guard is the right owner boundary, but its card-level collector must distinguish the renderer-produced context markup that Feishu accepts. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 1d1722655987. LabelsLabel justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (19 earlier review cycles; latest 8 shown)
|
|
ClawSweeper PR egg: ✨ hatched 🌱 uncommon Gilded Lint Imp. Rarity: 🌱 uncommon. Trait: stacks clean commits. DetailsShare on X: post this hatch
About:
|
ec62ef2 to
358e997
Compare
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
e050211 to
183dddf
Compare
|
🦞🧹 I asked ClawSweeper to review this item again. |
This comment was marked as spam.
This comment was marked as spam.
|
@clawsweeper review We accept Option 2: narrow the PR to static cards only.
The PR already stated "What was not tested: Streaming card path". Please re-evaluate with this scope clarification. |
|
🦞👀 Command router queued. I will update this comment with the next step. Re-review progress:
|
1126b95 to
e383d72
Compare
e383d72 to
ed78035
Compare
|
Thanks for the detailed review! Addressing each item: 1. [P2] Closing reference narrowed — Done. The closing footer 2. [P1] Dirty branch / rebase — Done. The branch has been fully rewritten on top of current main (not rebased commit-by-commit, since the Feishu delivery code was restructured in the meantime): the guard is re-applied to the current 3. [P1] Release-note context for the explicit-card compatibility change — Added a dedicated "Release note" section to the PR body: messages with >5 tables now render as posts (tables converted to code blocks) instead of failing delivery, including when 4. [P1] Streaming CardKit path — This PR intentionally guards only static cards, but we have since empirically verified that the CardKit streaming path is not subject to this limit. The full streaming-card API sequence ( Happy to adjust scope or wording as maintainers prefer. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
ed78035 to
28a2edb
Compare
|
Addressing the presentation-card findings from the re-review: Guard generated presentation cards (P1) — Done. Real behavior proof for the action-bearing path — Added to the PR body. We reproduced the failure at the API level: Regressions — Added in Explicit-card fallback coverage (P2) — Retained: the explicit All local checks pass (272 feishu extension tests + extension type check). Requesting a fresh review. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Two updates since the last review: 1. The message-action presentation path is now guarded too. In addition to 2. Regression coverage added for the new path. The earlier items remain addressed: @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Addressing the three P1 findings from the previous review: 1. Count tables with the canonical GFM parser (P1) — Done. Counting is now parser-backed: Why this matters in practice: we verified against the real Feishu API that pipe-less tables are rejected exactly like piped ones — 6 pipe-less GFM tables fail with 2. Route failed streaming recovery through post delivery (P1) — Done. 3. Inspect Markdown in presentation blocks (P1) — Done. The presentation paths now use a card-level check ( After-fix proof — Added inspectable evidence to the PR body ("Real behavior evidence"): the same 6-table content through the post fallback ( Full feishu extension suite: 1593 tests passed; extension type check clean. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
…limit Feishu rejects static interactive cards with more than 5 table components (ErrCode 11310: card table number over limit), which silently drops the reply into the delivery queue. Tables are counted with the shared GFM markdown parser (the same parser used for post-mode table conversion), so every parser-recognized table form is caught — including pipe-less GFM tables and alignment-colon delimiters — while fenced code, thematic breaks, and plain pipes in prose are not counted. Feishu was verified to reject pipe-less tables the same way as piped ones (ErrCode 11310 in both cases). Guards are applied at every static-card decision point: - presentation-card.ts: withinCardTableLimit (parser-backed) plus feishuCardWithinTableLimit, which counts tables across every markdown element of a built card - outbound.ts: guard sendOutboundText and sendText payload routing; buildFeishuPayloadCard returns undefined when the built card exceeds the limit so generated presentation cards fall back to the text path - channel.ts: the message-action presentation path rejects the generated card when it exceeds the limit and routes delivery through the existing presentationFellBack -> sendPayload text fallback - reply-dispatcher.ts: guard useStaticCard only (useStreamingCard is left unchanged), and route streaming recovery through post delivery when the recovered content exceeds the limit, instead of retrying a static card the API would reject again Streaming CardKit cards are intentionally left unchanged: they push content into a markdown element rather than table components, so the 5-table component limit does not apply there. Opaque native Feishu cards supplied by the caller are not inspected. Part of openclaw#43690
|
Updates addressing the remaining blocker (inspectable real behavior proof) and two CI failures: 1. Exact-path after-fix proof added (PR body → "Evidence") — The patched code paths were executed against the real Feishu API; each delivery receipt part shows which transport was actually used:
Pre-fix failure captures (static piped, static pipe-less, generated presentation card, message-action send, original delivery-queue failure) remain in the same section. 2. CI failure fixed: 3. CI failure fixed: @clawsweeper re-review |
|
🦞👀 Command router queued. I will update this comment with the next step. |
|
Re the remaining P1 ("Count context tables before adding Feishu markup"): this scenario was tested directly against the real Feishu API, and it does not fail delivery — so no guard is needed there, and adding one would be a regression rather than a fix. A Feishu accepts the card. The Consequence: the card-level check intentionally counts what Feishu counts — bare table components — and leaves font-decorated content unguarded. Counting semantic context text "before decoration" would block cards that Feishu delivers fine, degrading working card rendering to post fallback for no delivery benefit. This is now documented in the PR body's scope note plus evidence item 9. The other three items in the previous round were the proof request (now supplied, per the updated Proof confidence) and maintainer-acceptance notes already covered by the Release note section. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
Add focused tests for the parser-backed table counting and the guards at every static-card decision point. presentation-card.test.ts: - withinCardTableLimit covers piped tables, pipe-less GFM tables, alignment-colon delimiters, fenced code exclusion, and thematic breaks / plain pipes in prose at the 5-table boundary - feishuCardWithinTableLimit sums tables across card markdown elements and enforces the 5-table ceiling outbound.test.ts: - 5 tables with auto mode route to structured card - 6 tables fall back to post mode (auto, explicit card, and pipe-less GFM tables) - tables inside code blocks are excluded from the count - 6 tables with an action-bearing presentation refuse the generated card and deliver via the post path; 5 tables still build the card - a presentation text block embedding 6 tables refuses the card channel.test.ts: - message-action send with 6-table text (fallback and text-block forms) refuses the generated card and routes to the sendPayload fallback reply-dispatcher.test.ts: - 5 tables with streaming off route to static card - 6 tables fall back to post mode (auto and explicit card) - over-limit streaming content that was never accepted recovers through the post path instead of a rejected static card Refs: openclaw#43690
|
Thanks @name5566 for the original diagnosis, implementation, and extensive real Feishu proof. The canonical repair has now landed through #136382 as 7e21eeb and closed #43690. The landed change incorporates the parser-backed five-table ceiling across outbound text, generated presentations, message actions, ordinary replies, and failed-stream recovery. It also preserves font-wrapped context cards that Feishu accepts and active CardKit streams whose markdown is not subject to the static-card component limit, with focused coverage for both boundaries. The squash commit preserves your incorporated source contribution with co-author credit. This older conflicting branch is now superseded, so I'm closing it without merge while leaving its discussion and proof visible as project history. |
What Problem This Solves
When an agent's reply contains more than 5 Markdown tables, the Feishu plugin detects tables and automatically sends the reply as an interactive card. However, the Feishu card API limits each card to at most 5 table components (
ErrCode: 11310; card table number over limit). Messages exceeding this limit fail delivery and enter the delivery queue — the user sees nothing in Feishu while the reply sits in the control UI. This PR makes every static-card decision point respect the 5-table ceiling and fall back to the existing post path instead of failing.The failure affects every static-card decision point:
reply-dispatcher.ts) —useStaticCarddecisionoutbound.ts) —sendOutboundText/sendTextcard decisionoutbound.ts) —buildFeishuPayloadCardchannel.ts) —send/thread-replywith apresentationparamreply-dispatcher.ts) —ensureVisibleStreamingDeliveryre-send after an invisible CardKit resultThe issue also occurs when
renderModeis explicitly set to"card"(not just"auto").Solution
Tables are now counted with the shared GFM markdown parser (
markdownToIRWithMeta, the same parser used for post-mode table conversion) instead of a delimiter-row regex, so every parser-recognized table form is caught — including pipe-less GFM tables (a | b/--- | ---) and alignment-colon delimiters (|:--|--:|) — while fenced code, thematic breaks, and plain pipes in prose are not counted. Feishu was empirically verified to reject pipe-less tables exactly like piped ones (both fail withErrCode 11310at 6 tables), so both forms must be counted.The counting helpers live in one place (
presentation-card.ts, next toisFeishuCardWithinEnvelope) and are used by every static-card decision point:withinCardTableLimit(text)— parser-backed ≤5 check for a single markdown payloadfeishuCardWithinTableLimit(card)— counts tables across every markdown element of a built card, covering generated cards whose tables come from fallback text as well as presentationtext/contextblocksWhen card mode is rejected, delivery falls back to post mode (
convertMarkdownTables(text, "code")— a reliable, already existing code path).Scope note: streaming CardKit cards are intentionally not guarded
Only static interactive cards (
im/v1/messages,msg_type: "interactive") are guarded. Streaming CardKit cards (cardkit/v1/cards) render content inside a markdown element (PUT .../elements/content/content) rather than as table components, so the 5-table component limit does not apply there — see "Streaming CardKit verification" below for empirical evidence. TheuseStreamingCarddecision is left unchanged; only the static-card recovery re-send after an invisible streaming result is guarded (it would otherwise retry the same over-limit content as a static card and fail again).Generated presentation cards are static interactive cards: their elements are markdown, subject to the same limit. They are guarded at the card-construction boundary with the card-level check. Opaque native Feishu cards supplied by the caller are not inspected (they are returned before the guard runs).
One nuance, verified empirically: a
contextpresentation block renders its text wrapped in<font color='grey'>…</font>, and tables inside that decoration are not parsed by Feishu as table components — a card whose only content is a font-wrapped 6-table block is accepted by the real API (code 0), so it cannot hit the 5-component limit. The card-level check therefore intentionally counts what Feishu counts (bare table components) and leaves font-decorated content unguarded; blocking it would degrade cards that Feishu delivers fine.Changes
extensions/feishu/src/presentation-card.ts: parser-backedwithinCardTableLimitplus card-levelfeishuCardWithinTableLimit(single shared implementation).extensions/feishu/src/reply-dispatcher.ts:useStaticCardchecks the table limit before card mode (applies torenderMode: "card"and"auto"alike);useStreamingCardunchanged; the private regex copy is removed in favor of the shared helper;ensureVisibleStreamingDelivery(streaming recovery after an invisible CardKit result) delivers over-limit content through the post path instead of retrying a static card.extensions/feishu/src/outbound.ts: guard applied at the static-card decision points insendOutboundTextandsendText;buildFeishuPayloadCardreturnsundefinedwhen the built card exceeds the limit (card-level check), so generated presentation cards fall back to the existing text path. Opaque native Feishu cards are not altered.extensions/feishu/src/channel.ts: the message-action path (send/thread-replywith apresentationparam) rejects the generated card when the card-level check fails and routes delivery through the existingpresentationFellBack→sendPayloadtext fallback.Release note
Testing
textblock refuse the generated card and deliver via post (bothsendPayloadand message-action paths); 5 tables still build the cardEvidence (real Feishu API, exact patched paths)
After-fix proof through the patched code paths — the patched
feishuOutbound.sendTextandfeishuOutbound.sendPayloadwere executed against the real Feishu API (production tenant, websocket account) with over-limit content; each receipt partkindshows which transport was used:sendTextwith 6 tables → guarded out of card mode, delivered via post (kind: "text", tables converted to code blocks):sendTextwith 5 tables → card mode preserved at the boundary, delivered as card (kind: "card"):sendPayloadwith an action-bearing presentation + 6-table text → generated card refused at the construction boundary, delivered via thepresentationFellBackpost path:Pre-fix failures (all
HTTP 400 / code 230099 / ErrCode: 11310):sendPayloadwith an action-bearing presentation, sent as-is to the real API) — rejected (2026-08-06)<font color='grey'>+ 6 tables +</font>— the exact bytespresentation-card.tsemits for acontextblock — was accepted by the real API:HTTP 200 / code 0 / success. Font-decorated tables are not counted by Feishu, so a context block cannot trip the 5-table limit.Feishu official docs confirm the limit: https://open.feishu.cn/document/feishu-cards/card-json-v2-components/content-components/table?lang=zh-CN — "单张卡片最多支持放置五个表格组件"
Streaming CardKit verification (real Feishu API)
To determine whether the CardKit streaming path is also subject to the 5-table limit, the full streaming-card API sequence was exercised against the real Feishu API (same sequence as
streaming-card.ts:POST cardkit/v1/cards→ send message with card reference → incrementalPUT .../elements/content/content→PATCH .../settingsto close streaming):im/v1/messages, inline card JSON)ErrCode: 11310ErrCode: 11310The streaming card carries its content in a single markdown element, so Feishu does not count table components. The original report in #43690 (v2026.3.8, March 2026) predates the current streaming implementation, which creates the card with placeholder content and streams content through the CardKit element API.
Additionally verified end-to-end in a production OpenClaw instance (v2026.5.7): with Feishu streaming enabled and the table guard bypassed, a 6-table agent reply was delivered through a streaming card and rendered correctly; with streaming disabled, the same reply failed with the 11310 error on the static-card path.
Backward Compatibility
renderMode: "auto"behavior with code blocks (still uses card mode)renderMode: "card"messages with >5 tables render as posts instead of failing delivery — see Release note above