fix(feishu): improve streaming card update performance#48382
fix(feishu): improve streaming card update performance#48382Pengxiao-Wang wants to merge 1 commit intoopenclaw:mainfrom
Conversation
The streaming card update path had two sequential bottlenecks: 1. `update()` awaited the HTTP queue, blocking the caller until each PUT completed (~100-200 ms per round-trip). Back-to-back partial-reply callbacks stacked up and produced visible stutter. 2. Throttled (skipped) text was stored in `pendingText` but never re-scheduled — if no further `update()` arrived within the throttle window the content was silently dropped. 3. `mergeStreamingText` was called twice per update inside the streaming card, even though the reply dispatcher already sends a complete snapshot via `buildCombinedStreamText()`. The redundant merge caused tool-status lines to accumulate instead of being replaced. Changes: - Split `update()` into a non-blocking entry point and a private `flushUpdate()` that enqueues the HTTP PUT without awaiting it. - Add a `pendingTimer` that fires after the throttle window so throttled content is never lost. - Replace `mergeStreamingText` calls with direct snapshot assignment. - Reduce `updateThrottleMs` from 100 ms → 50 ms (20 updates/sec, matching the CardKit `print_frequency_ms` advisory). - Increase `print_step` from 1 → 3 so the Feishu client renders 3 characters per tick (~60 chars/sec) for smoother visual output. - Change `flushStreamingCardUpdate` in reply-dispatcher to fire-and-forget (`void` instead of `await`). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR improves Feishu streaming card update performance by removing blocking Key observations:
Confidence Score: 3/5
|
| void this.flushUpdate(text); | ||
| } | ||
|
|
||
| private async flushUpdate(text: string): Promise<void> { |
There was a problem hiding this comment.
flushUpdate declared async but never awaits
flushUpdate is marked async but its body only does synchronous work (state mutations + chaining onto this.queue). It never awaits anything directly. The async keyword makes the function return a Promise<void> that resolves immediately, which is then discarded via void at every call site. This could mislead readers into thinking there is asynchronous work happening inside the function body itself.
| private async flushUpdate(text: string): Promise<void> { | |
| private flushUpdate(text: string): void { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/streaming-card.ts
Line: 352
Comment:
**`flushUpdate` declared `async` but never awaits**
`flushUpdate` is marked `async` but its body only does synchronous work (state mutations + chaining onto `this.queue`). It never `await`s anything directly. The `async` keyword makes the function return a `Promise<void>` that resolves immediately, which is then discarded via `void` at every call site. This could mislead readers into thinking there is asynchronous work happening inside the function body itself.
```suggestion
private flushUpdate(text: string): void {
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c1197d846e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
| return; | ||
| } | ||
| void this.flushUpdate(text); |
There was a problem hiding this comment.
Wait for queued card updates before returning from update
FeishuStreamingSession.update() now returns immediately after void this.flushUpdate(text), so callers cannot observe whether the queued PUT has actually run; when partial snapshots arrive faster than Feishu accepts updates (the commit itself notes ~100–200ms RTT), this.queue can lag behind and close() then marks closed=true, causing queued callbacks to short-circuit and leaving state.currentText stale before final merge. In streams where snapshots replace prior content (for example tool-status line changes), that stale state can make the final mergeStreamingText(...) concatenate old and new snapshots instead of replacing, reintroducing duplicated/garbled final card text.
Useful? React with 👍 / 👎.
|
Thanks for the performance pass. The landed path in #71523 keeps the low-risk part: throttled card edits now schedule a pending flush, and meaningful or natural-boundary edits push immediately. I’m closing this as superseded because this branch also changes the hot path more broadly. Reopen if there is a specific Feishu card update performance issue still visible after the canonical fix. |
Summary
awaitin streaming card update path —update()no longer blocks on HTTP round-trip, andflushStreamingCardUpdatein reply-dispatcher uses fire-and-forget (voidinstead ofawait)pendingTimerso throttled text is never silently dropped — previouslypendingTextwas stored but had no re-send mechanismmergeStreamingTextcalls instreaming-card.tswith direct snapshot assignment — the reply dispatcher already sends complete combined text viabuildCombinedStreamText(), so re-merging caused tool status lines to accumulateupdateThrottleMsfrom 100 ms → 50 ms (matching CardKitprint_frequency_msadvisory)print_stepfrom 1 → 3 for smoother visual rendering (~60 chars/sec)Why
The Feishu streaming card path had three bottlenecks causing visible stutter:
update()awaited the HTTP queue (~100-200 ms per PUT), so back-to-back partial-reply callbacks stacked upupdate()arrivedmergeStreamingTextin the card layer concatenated old+new content instead of replacing, causing tool status lines like🔧 Using: read...🔧 Using: exec...to accumulateTest plan
pnpm test -- extensions/feishu/src/streaming-card.test.ts🤖 Generated with Claude Code