Skip to content

fix(feishu): stream CardKit text deltas#82419

Merged
frankekn merged 7 commits into
openclaw:mainfrom
hclsys:fix/feishu-cardkit-streaming-delta-82417
May 16, 2026
Merged

fix(feishu): stream CardKit text deltas#82419
frankekn merged 7 commits into
openclaw:mainfrom
hclsys:fix/feishu-cardkit-streaming-delta-82417

Conversation

@hclsys

@hclsys hclsys commented May 16, 2026

Copy link
Copy Markdown

Real behavior proof

  • Behavior or issue addressed: Feishu CardKit streaming updates were sent as the full accumulated assistant text on every /elements/content/content PUT. On clients where that endpoint appends streamed content, updates like A, AB, ABC render as AABABC. Fixes [Bug]: Feishu CardKit streaming card appends full accumulated text on each update instead of replacing #82417.
  • Real environment tested: Local OpenClaw checkout on commit a7c06bcfc11a5c9b993b5c716c237eab3fcbfffa, Node/Vitest through the repo toolchain, exercising FeishuStreamingSession.update() and the exact CardKit content-update request body emitted by extensions/feishu/src/streaming-card.ts with the network guard mocked at the Feishu HTTP boundary.
  • Exact steps or command run after this patch:
pnpm exec oxfmt --check --threads=1 extensions/feishu/src/streaming-card.ts extensions/feishu/src/streaming-card.test.ts
node scripts/run-vitest.mjs extensions/feishu/src/streaming-card.test.ts
node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.json extensions/feishu/src/streaming-card.ts extensions/feishu/src/streaming-card.test.ts
git diff --check
  • Evidence after fix: Terminal output from the focused Feishu streaming test run:
RUN  v4.1.6 /home/chenglunhu/code/openclaw

Test Files  1 passed (1)
Tests  9 passed (9)

Focused assertions now verify the emitted CardKit update payloads send only the appended suffix:

{"content":" small","sequence":2,"uuid":"s_card_1_2"}
{"content":"!","sequence":2,"uuid":"s_card_2_2"}
  • Observed result after fix: For accumulated snapshots, OpenClaw still tracks the full logical streaming text internally, but the CardKit streaming endpoint receives only the new suffix. The initial streaming content element is blank, so the first streamed chunk does not append after the old Thinking... placeholder.
  • What was not tested: I did not send a live Feishu message through the public Feishu API because this environment has no Feishu app credentials/chat configured. The local proof verifies the exact request body generated immediately before fetchWithSsrFGuard sends the CardKit update.

Implementation notes

  • extensions/feishu/src/streaming-card.ts
    • Adds a small append-content resolver for CardKit streaming updates.
    • Keeps state.currentText as the full logical accumulated text.
    • Sends only the suffix delta to /cardkit/v1/cards/{card_id}/elements/content/content.
    • Starts the streaming markdown element empty instead of with Thinking..., avoiding a stale placeholder prefix in append-mode clients.
  • extensions/feishu/src/streaming-card.test.ts
    • Updates the throttled and natural-boundary streaming tests to assert suffix-only CardKit content.

Pre-implement audits

  • Audit A (existing helper): No existing CardKit delta helper found. Reused existing mergeStreamingText for full logical text and added a private suffix resolver for the CardKit append payload.
  • Audit B (shared callers): updateCardContent is private to FeishuStreamingSession; public update()/close() contracts are preserved.
  • Audit C (broader rival): No open rival PR references [Bug]: Feishu CardKit streaming card appends full accumulated text on each update instead of replacing #82417. Broad/old Feishu streaming PRs exist, but none target this CardKit full-accumulated append bug or have maintainer-bot endorsement for this issue.

@openclaw-barnacle openclaw-barnacle Bot added channel: feishu Channel integration: feishu size: XS proof: supplied External PR includes structured after-fix real behavior proof. labels May 16, 2026
@clawsweeper

clawsweeper Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The PR changes Feishu CardKit streaming cards to send delivered text deltas, handle failed append/replacement updates, add focused streaming-card tests, and add a changelog entry.

Reproducibility: no. high-confidence live Feishu reproduction is available here. Source inspection does show current main sends accumulated CardKit content, and the PR source shows a remaining non-prefix streaming rewrite path that can still append the whole rewritten body.

Real behavior proof
Needs real behavior proof before merge: Needs contributor action: the PR supplies mocked terminal/test output but no live Feishu/CardKit after-fix proof; add redacted screenshots, recording, live output, or logs and update the PR body for re-review.

Next step before merge
Needs contributor or maintainer follow-up for live Feishu/CardKit proof plus the non-prefix streaming update fix; ClawSweeper should not queue a repair while the external PR's real-behavior proof gate is still missing.

Security
Cleared: The diff is limited to Feishu TypeScript source/tests and changelog text, keeps the existing guarded Feishu fetch path, and adds no concrete security or supply-chain regression.

Review findings

  • [P2] Replace non-prefix streaming updates instead of appending them — extensions/feishu/src/streaming-card.ts:456-462
Review details

Best possible solution:

Update the Feishu streaming path to replace non-prefix streaming rewrites as well as final rewrites, add regression coverage for that mid-stream case, and require redacted live Feishu/CardKit proof before merge.

Do we have a high-confidence way to reproduce the issue?

No high-confidence live Feishu reproduction is available here. Source inspection does show current main sends accumulated CardKit content, and the PR source shows a remaining non-prefix streaming rewrite path that can still append the whole rewritten body.

Is this the best way to solve the issue?

No, not yet. The prefix-growth delta path is narrow, but the streaming update path should use the same replacement branch as final closeout when the next visible body no longer starts with sentText.

Full review comments:

  • [P2] Replace non-prefix streaming updates instead of appending them — extensions/feishu/src/streaming-card.ts:456-462
    update() always sends appendContent through the append endpoint. When a later combined snapshot contains the previous answer but no longer starts with sentText, such as delayed reasoning text being prepended by buildCombinedStreamText(), the helper returns the whole rewritten body and CardKit append-mode clients render it after the old body. The final close path already switches to replaceCardContent() for this case; the streaming path needs the same branch and coverage.
    Confidence: 0.86

Overall correctness: patch is incorrect
Overall confidence: 0.84

Acceptance criteria:

  • node scripts/run-vitest.mjs extensions/feishu/src/streaming-card.test.ts extensions/feishu/src/reply-dispatcher.test.ts
  • node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.json extensions/feishu/src/streaming-card.ts extensions/feishu/src/streaming-card.test.ts
  • git diff --check
  • Redacted live Feishu/CardKit run showing streaming output no longer duplicates text, including a non-prefix rewrite or reasoning-preview case if possible.

What I checked:

  • current-main-full-text-update-path: Current main's Feishu streaming session still sends the full merged text to /elements/content/content, matching the linked bug's source-level report. (extensions/feishu/src/streaming-card.ts:321, 33685e14748b)
  • pr-delta-update-path: The PR head computes appendContent and always sends it through updateCardContent; for non-prefix rewrites the helper returns the whole rewritten body, which is still sent to the append-style endpoint. (extensions/feishu/src/streaming-card.ts:456, abd6ad22af13)
  • non-prefix-runtime-source: Feishu reply streaming can prepend reasoning text before the answer in buildCombinedStreamText(), so a later streaming snapshot may contain prior answer text without starting with the already-sent card body. (extensions/feishu/src/reply-dispatcher.ts:259, abd6ad22af13)
  • final-closeout-has-safer-branch: The PR already uses replaceCardContent() for non-prefix final closeout rewrites, which shows the same replacement branch is needed for non-prefix streaming updates before close. (extensions/feishu/src/streaming-card.ts:516, abd6ad22af13)
  • mock-only-real-proof: The PR body says no live Feishu message was sent and the supplied evidence is terminal output from focused tests with fetchWithSsrFGuard mocked at the Feishu HTTP boundary. (abd6ad22af13)
  • prior-review-follow-up: Earlier Codex P1 findings about non-OK append and replace responses were addressed by later commits, but they did not cover the remaining non-prefix streaming-update path. (extensions/feishu/src/streaming-card.ts:359, abd6ad22af13)

Likely related people:

  • m1heng: The Feishu package metadata describes the Feishu/Lark channel plugin as community maintained by this handle, making them a strong routing candidate for Feishu behavior review. (role: declared plugin owner; confidence: high; commits: 5f85e6b69213; files: extensions/feishu/package.json, extensions/feishu/src/streaming-card.ts)
  • vincentkoc: Recent Feishu streaming-card and dispatcher history includes closeout and streaming-card maintenance in the same files. (role: recent area contributor; confidence: high; commits: 455eba7f94f0, 84a22a64bef0, ddd1c77b49ef; files: extensions/feishu/src/streaming-card.ts, extensions/feishu/src/reply-dispatcher.ts)
  • xzq-xu: The CardKit streaming-card implementation appears to originate from the Feishu streaming card support commit that introduced this runtime surface. (role: feature-history contributor; confidence: medium; commits: 65be9ccf63f3; files: extensions/feishu/src/streaming-card.ts)
  • steipete: Recent Feishu helper and dispatcher commits touched adjacent plugin internals and broad Feishu channel behavior. (role: adjacent area contributor; confidence: medium; commits: b388209eaf72, f9c0dc2d2b9e; files: extensions/feishu/src/streaming-card.ts, extensions/feishu/src/reply-dispatcher.ts)

Remaining risk / open question:

  • No live Feishu/CardKit after-fix proof is available, so the client append behavior and replacement workaround remain unverified in a real tenant.
  • The PR's suffix-only approach still depends on observed CardKit append behavior rather than a locally verifiable documented replacement contract.
  • The remaining non-prefix streaming path can still duplicate visible content when reasoning/status content rewrites the beginning of the card before final close.

Codex review notes: model gpt-5.5, reasoning high; reviewed against f78434985af9.

@frankekn frankekn self-assigned this May 16, 2026
@frankekn

Copy link
Copy Markdown
Contributor

Pushed a follow-up fix for the failed-delta case.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@frankekn

Copy link
Copy Markdown
Contributor

Pushed a second follow-up for non-prefix final closeout rewrites.

@codex review

@frankekn

Copy link
Copy Markdown
Contributor

Pushed follow-up c33f2c0 for the non-prefix CardKit closeout rewrite case.

Local verification:

  • node scripts/run-vitest.mjs extensions/feishu/src/streaming-card.test.ts
  • node scripts/run-vitest.mjs extensions/feishu/src/reply-dispatcher.test.ts
  • pnpm check:changed
  • pnpm check:test-types

No live Feishu/CardKit credentialed proof is available from my environment, so that part still needs contributor/maintainer evidence.

@clawsweeper re-review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c33f2c0e8d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

auditContext: "feishu.streaming-card.update",
});
await release();
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat non-OK content updates as failed sends

updateCardContent returns true for any resolved fetch, even when Feishu responds with HTTP errors (for example 429/5xx), because it never checks response.ok before marking success. With the new sentText bookkeeping, that causes the session to advance its sent baseline and future delta payloads to omit text that was never actually accepted by CardKit, so users can permanently lose chunks after transient API failures.

Useful? React with 👍 / 👎.

auditContext: "feishu.streaming-card.replace",
});
await release();
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mark replace failures when final rewrite gets non-OK response

replaceCardContent has the same success-path issue: it returns true without validating the HTTP status, so a non-OK final rewrite response is treated as delivered. In non-prefix closeout rewrites this can leave the card showing stale streamed content while internal state assumes the replacement succeeded, preventing a correct retry path.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented May 16, 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:

@frankekn

Copy link
Copy Markdown
Contributor

@codex review

@frankekn

Copy link
Copy Markdown
Contributor

Addressed the Codex P1 findings in d99beb6.

Both CardKit append and replacement updates now treat non-OK HTTP responses as failed sends before advancing the delivered baseline, with regression coverage for 429 append retry and 500 final replacement failure.

Verification:

  • node scripts/run-vitest.mjs extensions/feishu/src/streaming-card.test.ts
  • pnpm check:changed
  • git diff --check

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@frankekn frankekn merged commit f436b43 into openclaw:main May 16, 2026
107 checks passed
@frankekn

Copy link
Copy Markdown
Contributor

Merged. Thanks @hclsys.

Head: abd6ad2
Landed: f436b43

woodygreen pushed a commit to woodygreen/openclaw that referenced this pull request May 18, 2026
Fixes openclaw#82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
galiniliev pushed a commit to galiniliev/openclaw that referenced this pull request May 20, 2026
Fixes openclaw#82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Fixes openclaw#82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
galiniliev pushed a commit to galiniliev/openclaw that referenced this pull request May 25, 2026
Fixes openclaw#82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
qiaokuan1992 pushed a commit to qiaokuan1992/openclaw that referenced this pull request Jun 2, 2026
Fixes openclaw#82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
Fixes openclaw#82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
SYU8384 pushed a commit to SYU8384/openclaw that referenced this pull request Jun 3, 2026
Fixes openclaw#82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Fixes openclaw#82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu proof: supplied External PR includes structured after-fix real behavior proof. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Feishu CardKit streaming card appends full accumulated text on each update instead of replacing

2 participants