Skip to content

fix(feishu): fall back to post mode when markdown tables exceed card limit - #86901

Closed
name5566 wants to merge 2 commits into
openclaw:mainfrom
name5566:fix/feishu-card-table-limit
Closed

name5566 wants to merge 2 commits into
openclaw:mainfrom
name5566:fix/feishu-card-table-limit

Conversation

@name5566

@name5566 name5566 commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

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:

  • Auto-reply (reply-dispatcher.ts) — useStaticCard decision
  • Outbound (outbound.ts) — sendOutboundText / sendText card decision
  • Generated presentation cards (outbound.ts) — buildFeishuPayloadCard
  • Message-action delivery (channel.ts) — send / thread-reply with a presentation param
  • Streaming recovery (reply-dispatcher.ts) — ensureVisibleStreamingDelivery re-send after an invisible CardKit result

The issue also occurs when renderMode is 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 with ErrCode 11310 at 6 tables), so both forms must be counted.

The counting helpers live in one place (presentation-card.ts, next to isFeishuCardWithinEnvelope) and are used by every static-card decision point:

  • withinCardTableLimit(text) — parser-backed ≤5 check for a single markdown payload
  • feishuCardWithinTableLimit(card) — counts tables across every markdown element of a built card, covering generated cards whose tables come from fallback text as well as presentation text/context blocks

When 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. The useStreamingCard decision 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 context presentation 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-backed withinCardTableLimit plus card-level feishuCardWithinTableLimit (single shared implementation).
  • extensions/feishu/src/reply-dispatcher.ts: useStaticCard checks the table limit before card mode (applies to renderMode: "card" and "auto" alike); useStreamingCard unchanged; 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 in sendOutboundText and sendText; buildFeishuPayloadCard returns undefined when 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-reply with a presentation param) rejects the generated card when the card-level check fails and routes delivery through the existing presentationFellBack → sendPayload text fallback.

Release note

Feishu: replies with more than 5 markdown tables now fall back to post rendering instead of failing card delivery (Feishu hard limit: 5 table components per card). This also applies when renderMode: "card" is explicitly configured: such messages render as posts with tables converted to code blocks. Table counting now recognizes all GFM table forms, including pipe-less tables.

Testing

  • Full feishu extension suite: 1593 tests passed, extension type check clean, knip dead-code checks clean
  • Parser-backed counting regression tests: piped tables, pipe-less GFM tables, alignment-colon delimiters, fenced-code exclusion, thematic breaks / plain pipes in prose
  • Card-level check tests: tables summed across multiple markdown elements; 5-table boundary accepted
  • Routing tests: 5 tables → card; 6 tables → post fallback (auto, explicit card, pipe-less GFM)
  • Presentation tests: 6 tables in fallback text or in a text block refuse the generated card and deliver via post (both sendPayload and message-action paths); 5 tables still build the card
  • Streaming recovery test: over-limit content that was never accepted recovers through the post path, not a rejected static card

Evidence (real Feishu API, exact patched paths)

After-fix proof through the patched code paths — the patched feishuOutbound.sendText and feishuOutbound.sendPayload were executed against the real Feishu API (production tenant, websocket account) with over-limit content; each receipt part kind shows which transport was used:

  1. sendText with 6 tables → guarded out of card mode, delivered via post (kind: "text", tables converted to code blocks):
messageId: om_x100b687c343ae53cb1a02a151b50f37   parts[0].kind: "text"
  1. sendText with 5 tables → card mode preserved at the boundary, delivered as card (kind: "card"):
messageId: om_x100b687c35ccaca8b20a9a7be054a36   parts[0].kind: "card"
  1. sendPayload with an action-bearing presentation + 6-table text → generated card refused at the construction boundary, delivered via the presentationFellBack post path:
messageId: om_x100b687c35dfaca4b1cb06cf3f4a73b   parts[0].kind: "text"

Pre-fix failures (all HTTP 400 / code 230099 / ErrCode: 11310):

  1. Production delivery-queue failure (2026-05-26, 18 tables):
[delivery-recovery] Retry failed: Feishu card send failed:
"feishu_msg": "Failed to create card content, ext=ErrCode: 11310;
ErrMsg: card table number over limit; ErrorValue: table;"
  1. Static card, 6 piped tables — re-confirmed against the real API (2026-08-06)
  2. Static card, 6 pipe-less GFM tables — the real API rejects these too (2026-08-06), which is why counting is parser-backed rather than regex-based
  3. Generated presentation card embedding 6 tables (built by sendPayload with an action-bearing presentation, sent as-is to the real API) — rejected (2026-08-06)
  4. Message-action presentation send with 6-table text (production gateway, 2026-08-06):
2026-08-06T16:30:24 Feishu card send failed: http_status:400, feishu_code:230099,
"ErrCode: 11310; ErrMsg: card table number over limit; ErrorValue: table"
  1. Font-wrapped context-block probe (2026-08-06): a card whose only element is a markdown element containing <font color='grey'> + 6 tables + </font> — the exact bytes presentation-card.ts emits for a context block — 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 → incremental PUT .../elements/content/content → PATCH .../settings to close streaming):

Scenario Result
Static card, 6 tables (im/v1/messages, inline card JSON) ❌ HTTP 400 / code 230099 / ErrCode: 11310
Static card, 6 pipe-less GFM tables ❌ HTTP 400 / code 230099 / ErrCode: 11310
Streaming card, 6 tables (create → send → 6 incremental updates → close) ✅ all API calls succeeded
Streaming card, 18 tables ✅ all API calls succeeded (all 18 updates)
Static card, 5 tables ✅ delivered (boundary check)

The 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

  • No change for messages with ≤5 tables
  • No change for messages without tables
  • No change for renderMode: "auto" behavior with code blocks (still uses card mode)
  • Behavior change (intended): explicit renderMode: "card" messages with >5 tables render as posts instead of failing delivery — see Release note above

@openclaw-barnacle openclaw-barnacle Bot added channel: feishu Channel integration: feishu size: XS triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 26, 2026
@clawsweeper

clawsweeper Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge. Reviewed August 29, 2026, 7:00 PM ET / 23:00 UTC.

ClawSweeper review

What this changes

The PR sends Feishu replies with more than five Markdown tables through post delivery instead of a static card that Feishu rejects.

Regression provenance

Possible regression — suspected (reviewed change). No predecessor PR is attributed.

Merge readiness

⚠️ Needs maintainer review before merge - 4 items remain

Keep 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
Reviewed head: bef9d7f357b7432514f094ea422f36a93d5e789b

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) Direct real-API proof supports the central fix, but the unresolved context-card regression blocks merge.
Proof confidence 🐚 platinum hermit (4/6) Sufficient (live_output): The supplied PR body records real Feishu API runs through the patched outbound owner: six bare tables delivered as a post, five as a card, and an action-bearing presentation with six fallback tables as a post. That proof covers the central fallback behavior, though not the introduced context-card regression.
Patch quality 🦐 gold shrimp (3/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (live_output): The supplied PR body records real Feishu API runs through the patched outbound owner: six bare tables delivered as a post, five as a card, and an action-bearing presentation with six fallback tables as a post. That proof covers the central fallback behavior, though not the introduced context-card regression.
Evidence reviewed 6 items Introduced trigger: The PR’s recursive collector adds every markdown element’s content to the generic table counter, including renderer-produced markup.
Context-card contract: Generated context blocks are wrapped in <font color='grey'>…</font>; the supplied real-Feishu evidence says six tables in that wrapper are accepted rather than counted as table components.
Observable regression route: A false card-limit result makes both generated-presentation routes abandon a deliverable card for post fallback.
Findings 1 actionable finding [P2] Preserve deliverable context cards
Security None None.

How this fits together

The 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
Loading

Before merge

  • Preserve deliverable context cards (P2) - The collector counts every markdown element, but generated context blocks are font-wrapped and the supplied Feishu evidence says six tables there are accepted without table components. This changes a deliverable context-only card into post fallback; exclude the renderer-owned context form and cover both presentation routes.
  • Resolve merge risk (P2) - A generated presentation containing only a six-table context block regresses from an accepted static card to post fallback.
  • Resolve merge risk (P1) - GitHub reports this branch as dirty against main and no pinned test merge is available, so the repaired head needs a rebase and exact-head review.
  • Complete next step (P2) - A bounded mechanical repair can preserve the established context-card behavior and add the missing route coverage.

Findings

  • [P2] Preserve deliverable context cards — extensions/feishu/src/presentation-card.ts:93-98
Agent review details

Security

None.

PR surface

Source +92, Tests +386. Total +478 across 8 files.

View PR surface stats
Area Files Added Removed Net
Source 4 113 21 +92
Tests 4 386 0 +386
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 8 499 21 +478

Review metrics

Metric Value Why it matters
Production versus test delta production +113/-21, tests +386/-0 Most growth is focused regression coverage; the remaining production change must retain the context rendering contract before merge.

Root-cause cluster

Relationship: fixed_by_candidate
Canonical: #43690
Summary: This PR is a concrete candidate fix for the static-card portion of the canonical Feishu table-limit report; it should remain open until repaired and merged.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge-risk options

Maintainer options:

  1. Preserve context-card delivery (recommended)
    Exclude renderer-produced font-wrapped context content from the static-card table count and add six-table context coverage for outbound and message-action presentations.
  2. Pause the branch
    Close or defer this branch if preserving context-card rendering is not worth reconciling the stale, dirty merge state.
Copy recommended automerge instruction
@clawsweeper automerge

Special instructions:
Preserve generated font-wrapped context cards in table-limit accounting, add focused coverage for both presentation routes, then rebase and request an exact-head review.

Technical review

Best 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:

  • [P2] Preserve deliverable context cards — extensions/feishu/src/presentation-card.ts:93-98
    The collector counts every markdown element, but generated context blocks are font-wrapped and the supplied Feishu evidence says six tables there are accepted without table components. This changes a deliverable context-only card into post fallback; exclude the renderer-owned context form and cover both presentation routes.
    Confidence: 0.98

Overall correctness: patch is incorrect
Overall confidence: 0.98

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 1d1722655987.

Labels

Label justifications:

  • P1: The PR targets a real Feishu reply-delivery failure that can otherwise leave users without a channel response.
  • merge-risk: 🚨 compatibility: The introduced counter changes valid context-card rendering into post fallback for existing presentation callers.
  • merge-risk: 🚨 message-delivery: The fallback routing determines whether Feishu recipients receive a card or a post message.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🐚 platinum hermit and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (live_output): The supplied PR body records real Feishu API runs through the patched outbound owner: six bare tables delivered as a post, five as a card, and an action-bearing presentation with six fallback tables as a post. That proof covers the central fallback behavior, though not the introduced context-card regression.
  • proof: sufficient: Contributor real behavior proof is sufficient. The supplied PR body records real Feishu API runs through the patched outbound owner: six bare tables delivered as a post, five as a card, and an action-bearing presentation with six fallback tables as a post. That proof covers the central fallback behavior, though not the introduced context-card regression.

Evidence

Acceptance criteria:

  • [P1] node scripts/run-vitest.mjs extensions/feishu/src/presentation-card.test.ts extensions/feishu/src/outbound.test.ts extensions/feishu/src/channel.test.ts.
  • [P1] node scripts/run-vitest.mjs extensions/feishu/src/reply-dispatcher.test.ts.
  • [P1] git diff --check.

What I checked:

Likely related people:

  • Vincent Koc: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • Peter Steinberger: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Preserve font-wrapped six-table context cards through both generated-presentation routes.
  • Rebase the repaired branch and request one exact-head ClawSweeper review.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
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.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (19 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-06T10:43:24.888Z sha c8d370640f5ea98c17b6d579bdcefd66ee300730 :: needs real behavior proof before merge. :: [P1] Count tables with the canonical GFM parser | [P1] Route failed streaming recovery through post delivery | [P1] Inspect Markdown in presentation blocks
  • reviewed 2026-08-06T12:42:15.716Z sha 7fa9bf879a08fda5e2d9ddcfd562771a61f17ec1 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-06T13:40:09.911Z sha 672e81db9c9fdcdc17d97b20c8a1bbe00428be61 :: needs changes before merge. :: [P1] Count context tables before adding Feishu markup
  • reviewed 2026-08-06T14:37:27.351Z sha 672e81db9c9fdcdc17d97b20c8a1bbe00428be61 :: needs changes before merge. :: [P2] Preserve pipe-less context cards
  • reviewed 2026-08-06T15:42:36.727Z sha 672e81db9c9fdcdc17d97b20c8a1bbe00428be61 :: needs changes before merge. :: [P2] Preserve deliverable context cards
  • reviewed 2026-08-07T05:38:27.634Z sha bef9d7f :: needs changes before merge. :: [P2] Preserve deliverable context cards
  • reviewed 2026-08-15T20:53:17.175Z sha bef9d7f :: needs changes before merge. :: [P2] Preserve deliverable context cards
  • reviewed 2026-08-23T22:03:04.883Z sha bef9d7f :: needs changes before merge. :: [P2] Preserve deliverable context cards

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 26, 2026
@clawsweeper

clawsweeper Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg: ✨ hatched 🌱 uncommon Gilded Lint Imp. Rarity: 🌱 uncommon. Trait: stacks clean commits.

Details

Share on X: post this hatch
Copy: My PR egg hatched a 🌱 uncommon Gilded Lint Imp in ClawSweeper.
Hatchability:

  • Merged PRs are hatchable.
  • Open PRs are hatchable when they are status: 👀 ready for maintainer look, status: 🚀 automerge armed, or labeled clawsweeper:automerge.
  • Closed unmerged PRs are hatchable only when one of those hatchable labels is still present in the durable record.

About:

  • Eggs appear after real-behavior proof passes. They are collectible flavor only.
  • Review momentum changes the shell state: follow-up work warms it, re-review makes it wobble, and a clean final review lets it hatch.
  • The hatch is seeded from this repository and PR number, so the same PR keeps the same creature; the reviewed head SHA can only change safe visual details.
  • Rarity is just collectible sparkle: 🥚 common, 🌱 uncommon, 💎 rare, ✨ glimmer, and 🌈 legendary.

@name5566
name5566 force-pushed the fix/feishu-card-table-limit branch from ec62ef2 to 358e997 Compare May 27, 2026 05:32
@clawsweeper

clawsweeper Bot commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. P2 Normal backlog priority with limited blast radius. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels May 27, 2026
@name5566
name5566 force-pushed the fix/feishu-card-table-limit branch from e050211 to 183dddf Compare May 27, 2026 06:32
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels May 27, 2026
@clawsweeper

clawsweeper Bot commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 27, 2026
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 27, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels May 27, 2026
@BingqingLyu

This comment was marked as spam.

@name5566

Copy link
Copy Markdown
Contributor Author

@clawsweeper review

We accept Option 2: narrow the PR to static cards only.

  • Static cards (im/v1/messages, msg_type: "interactive") — the 5-table limit is confirmed by Feishu docs and production logs (ErrCode 11310). This PR fixes that.
  • Streaming cards (cardkit/v1/cards) — separate API, unverified whether the same limit applies. Out of scope.

The PR already stated "What was not tested: Streaming card path". Please re-evaluate with this scope clarification.

@clawsweeper

clawsweeper Bot commented May 28, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

Re-review progress:

@clawsweeper clawsweeper Bot removed the rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. label May 28, 2026
@clawsweeper clawsweeper Bot added status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jul 16, 2026
@name5566
name5566 force-pushed the fix/feishu-card-table-limit branch from 1126b95 to e383d72 Compare July 18, 2026 00:53
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 18, 2026
@clawsweeper clawsweeper Bot added merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. and removed merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels Jul 31, 2026
@name5566
name5566 force-pushed the fix/feishu-card-table-limit branch from e383d72 to ed78035 Compare August 6, 2026 05:50
@name5566

name5566 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review! Addressing each item:

1. [P2] Closing reference narrowed — Done. The closing footer Closes #43690 has been replaced with Part of #43690. The PR is now explicitly scoped to the static-card path.

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 useStaticCard / sendOutboundText / sendText decision points, and the tests are rewritten against the current test helpers (this also fixes the previous CI failure, which was caused by a stale helper reference firstTypingDispatcherOptions left over from a bulk edit).

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 renderMode: "card" is explicit.

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 (POST cardkit/v1/cards → message send with card reference → incremental PUT .../elements/content/content → PATCH .../settings) was exercised against the real Feishu API with 6 and 18 markdown tables: all calls succeeded, while the equivalent static-card send fails with ErrCode 11310 at 6 tables. Streaming cards carry content in a single markdown element, so Feishu does not count table components there. Details and the verification table are in the PR body ("Streaming CardKit verification"). We also re-confirmed the static 11310 failure in a production gateway log on 2026-08-06, and verified end-to-end that the same 6-table reply delivers successfully through a streaming card when streaming is enabled. This has been left for maintainer judgement on the broader report rather than asserted as a reason to close it.

Happy to adjust scope or wording as maintainers prefer.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@name5566
name5566 force-pushed the fix/feishu-card-table-limit branch from ed78035 to 28a2edb Compare August 6, 2026 06:05
@clawsweeper clawsweeper Bot removed the P2 Normal backlog priority with limited blast radius. label Aug 6, 2026
@name5566

name5566 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressing the presentation-card findings from the re-review:

Guard generated presentation cards (P1) — Done. buildFeishuPayloadCard now applies the same withinCardTableLimit() check to the embedded fallback text and returns undefined when the limit is exceeded, so generated presentation cards (including action-bearing ones) fall back to the existing text path instead of failing with ErrCode 11310. The guard is at the card-construction boundary as suggested; opaque native Feishu cards supplied by the caller remain untouched (that branch returns before the guard).

Real behavior proof for the action-bearing path — Added to the PR body. We reproduced the failure at the API level: sendPayload with a 6-table text plus an action-bearing presentation builds an interactive card embedding all 6 tables; sending that exact card JSON to the real Feishu API returns HTTP 400 / code 230099 / ErrCode: 11310. After the fix the same scenario delivers via the post path (tables converted to code blocks).

Regressions — Added in outbound.test.ts ("presentation card table-limit"): 6 tables + presentation with buttons refuse the generated card (sendCardFeishu not called) and deliver via post mode; 5 tables still build the card. Plus the earlier sendPayload/post-fallback boundary tests.

Explicit-card fallback coverage (P2) — Retained: the explicit renderMode: "card" boundary tests remain, and the Release note section still documents the card→post change.

All local checks pass (272 feishu extension tests + extension type check). Requesting a fresh review.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@name5566

name5566 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Two updates since the last review:

1. The message-action presentation path is now guarded too. In addition to buildFeishuPayloadCard, the channel.ts message-action path (send / thread-reply with a presentation param) builds presentation cards directly via buildFeishuPresentationCard. We reproduced the failure there in production: sending a presentation message with 6-table text returned HTTP 400 / code 230099 / ErrCode: 11310. The generated card is now rejected when the embedded text exceeds the table limit and delivery routes through the existing presentationFellBack → sendPayload text fallback. withinCardTableLimit() is now a single shared exported helper in presentation-card.ts (next to isFeishuCardWithinEnvelope) used by all static-card decision points. Opaque native Feishu cards remain untouched.

2. Regression coverage added for the new path. channel.test.ts: message-action send with presentation + 6-table text refuses the generated card (sendCardFeishu not called) and routes to the sendPayload fallback; the existing presentation tests (card rendering, oversized fallbacks) still pass. Full feishu extension suite: 1581 tests passed, extension type check clean.

The earlier items remain addressed: Part of #43690 reference, branch current with main, Release note section, streaming CardKit verification (6 and 18 tables, all API calls succeeded vs static 11310 at 6 tables), and the action-bearing sendPayload presentation regression.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@name5566

name5566 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressing the three P1 findings from the previous review:

1. Count tables with the canonical GFM parser (P1) — Done. Counting is now parser-backed: countMarkdownTables uses markdownToIRWithMeta (the same shared 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. The delimiter-row regex is gone, and the single shared implementation in presentation-card.ts is now used by every static-card decision point; the reply-dispatcher copy was removed as requested.

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 HTTP 400 / code 230099 / ErrCode: 11310, the same as 6 piped tables. So the parser-backed count is required, not just preferable.

2. Route failed streaming recovery through post delivery (P1) — Done. ensureVisibleStreamingDelivery (the re-send after an invisible CardKit result) now checks the recovered content against the table limit and delivers over-limit content through the post path instead of retrying a static card the API would reject again. Covered by a regression test: streaming content with 6 tables that was never accepted recovers through sendMessageFeishu (post), and sendStructuredCardFeishu is not called. The useStreamingCard decision itself remains unchanged (streaming CardKit is not subject to this limit — see the verification table in the PR body).

3. Inspect Markdown in presentation blocks (P1) — Done. The presentation paths now use a card-level check (feishuCardWithinTableLimit) that counts tables across every markdown element of the built card — fallback text and text/context presentation blocks alike — rather than only the fallback text. Both buildFeishuPayloadCard (outbound) and the message-action path (channel.ts) reject the generated card at the construction boundary and fall back to the text path. Covered by regression tests: a presentation whose text block embeds 6 tables refuses the card and delivers via post on both paths.

After-fix proof — Added inspectable evidence to the PR body ("Real behavior evidence"): the same 6-table content through the post fallback (convertMarkdownTables → post) was delivered successfully against the real Feishu API (HTTP 200 / code 0, message_id: om_x100b6873165d1cacb22fa74e53d3f13), plus the five pre-fix failure captures (static piped, static pipe-less, generated presentation card, message-action send, original delivery-queue failure).

Full feishu extension suite: 1593 tests passed; extension type check clean.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

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
@name5566

name5566 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

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:

  • sendText with 6 tables → post fallback delivered, messageId: om_x100b687c343ae53cb1a02a151b50f37, parts[0].kind: "text"
  • sendText with 5 tables → card preserved at the boundary, messageId: om_x100b687c35ccaca8b20a9a7be054a36, parts[0].kind: "card"
  • sendPayload with action-bearing presentation + 6-table text → generated card refused, delivered via the presentationFellBack post path, messageId: om_x100b687c35dfaca4b1cb06cf3f4a73b, parts[0].kind: "text"

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: check-dependencies (knip) — countMarkdownTables was flagged as an unused export; it is now module-private (only withinCardTableLimit and feishuCardWithinTableLimit are exported) and its regression tests were rewritten against the exported boundary functions. Both knip lanes (--production and all-exports) verified clean locally.

3. CI failure fixed: PR context and evidence — The PR body now uses the required authored sections: "What Problem This Solves" and "Evidence".

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

@name5566

name5566 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

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 context presentation block renders its text wrapped in <font color='grey'>…</font> (presentation-card.ts). To test whether Feishu counts tables inside that decoration, I sent a card whose only element is a markdown element containing <font color='grey'> + 6 markdown tables + </font> — the exact bytes the context-block renderer emits — to the real im/v1/messages endpoint:

HTTP 200 / code 0 / msg success

Feishu accepts the card. The <font> wrapper keeps the table rows from being parsed as table components, so Feishu's 5-component counter never sees them and delivery succeeds — the same mechanism as the CardKit streaming path (content that isn't a table component isn't counted).

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

@clawsweeper

clawsweeper Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

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
@vincentkoc

Copy link
Copy Markdown
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: L status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants