fix(feishu): prevent duplicate text in streaming card updates#34746
fix(feishu): prevent duplicate text in streaming card updates#34746Daily-AC wants to merge 4 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR adds a
Confidence Score: 3/5
Last reviewed commit: e69de74 |
| if (nextText.includes(streamText)) { | ||
| streamText = nextText; | ||
| return; | ||
| } |
There was a problem hiding this comment.
includes check can produce false-positive replacements for true deltas
nextText.includes(streamText) treats nextText as a full cumulative payload whenever streamText appears as a substring anywhere inside it — including as part of an unrelated word. This will misfire for true delta chunks if the short accumulated streamText appears as a substring inside the incoming delta.
For example:
streamText = "is"(early in the stream, only two characters accumulated)nextText = "This result is important"(a genuine delta to be appended)"This result is important".includes("is")→true("is"appears in"Th**is**"and standalone)- Result:
streamTextis silently replaced with the delta text, permanently dropping everything before"is"
The same false-positive risk applies to any common short word or phrase that may appear as a substring in subsequent delta chunks. The risk is highest early in a streaming session when streamText is still short.
A more conservative guard — e.g. only triggering when nextText is meaningfully longer than streamText, or requiring the match to be in the second half of nextText — would reduce false positives. For instance:
// Require nextText to end with streamText, or that streamText occupies
// at least half of nextText, to reduce false-positive substring matches.
if (
nextText.includes(streamText) &&
nextText.length >= streamText.length * 2
) {
streamText = nextText;
return;
}Or alternatively, since the bug is triggered by leading whitespace/directive stripping, trimming both sides before comparing is a more targeted fix:
if (nextText.trimStart().startsWith(streamText.trimStart())) {
streamText = nextText;
return;
}Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/reply-dispatcher.ts
Line: 163-166
Comment:
**`includes` check can produce false-positive replacements for true deltas**
`nextText.includes(streamText)` treats `nextText` as a full cumulative payload whenever `streamText` appears as a substring anywhere inside it — including as part of an unrelated word. This will misfire for true delta chunks if the short accumulated `streamText` appears as a substring inside the incoming delta.
For example:
- `streamText = "is"` (early in the stream, only two characters accumulated)
- `nextText = "This result is important"` (a genuine delta to be appended)
- `"This result is important".includes("is")` → `true` (`"is"` appears in `"Th**is**"` and standalone)
- Result: `streamText` is silently **replaced** with the delta text, permanently dropping everything before `"is"`
The same false-positive risk applies to any common short word or phrase that may appear as a substring in subsequent delta chunks. The risk is highest early in a streaming session when `streamText` is still short.
A more conservative guard — e.g. only triggering when `nextText` is meaningfully longer than `streamText`, or requiring the match to be in the second half of `nextText` — would reduce false positives. For instance:
```ts
// Require nextText to end with streamText, or that streamText occupies
// at least half of nextText, to reduce false-positive substring matches.
if (
nextText.includes(streamText) &&
nextText.length >= streamText.length * 2
) {
streamText = nextText;
return;
}
```
Or alternatively, since the bug is triggered by leading whitespace/directive stripping, trimming both sides before comparing is a more targeted fix:
```ts
if (nextText.trimStart().startsWith(streamText.trimStart())) {
streamText = nextText;
return;
}
```
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (1)
The PR description notes that all 16 existing tests pass, but none of them exercise the newly added Adding targeted unit tests for it("mergeStreamingText handles cumulative payload with leading whitespace prefix mismatch", async () => {
// onPartialReply("Hello") → streamText = "Hello"
// onPartialReply(" Hello world") → prefix mismatch but includes "Hello"
// expected: streamText = " Hello world" (replace, not append)
...
});
it("mergeStreamingText does not treat a short delta as cumulative", async () => {
// streamText = "is" (first chunk)
// nextText = "This is the answer" (true delta starting differently)
// expected: streamText = "isThis is the answer" (append)
...
});Prompt To Fix With AIThis is a comment left during a code review.
Path: extensions/feishu/src/reply-dispatcher.ts
Line: 149-172
Comment:
**No test coverage for the new branch**
The PR description notes that all 16 existing tests pass, but none of them exercise the newly added `nextText.includes(streamText)` path. The bug scenario (cumulative payload that fails `startsWith` due to leading whitespace/directive stripping) and the potential false-positive scenario (delta whose text happens to contain the current `streamText` as a substring) are both untested.
Adding targeted unit tests for `mergeStreamingText` would both validate the fix and guard against future regressions. For example:
```ts
it("mergeStreamingText handles cumulative payload with leading whitespace prefix mismatch", async () => {
// onPartialReply("Hello") → streamText = "Hello"
// onPartialReply(" Hello world") → prefix mismatch but includes "Hello"
// expected: streamText = " Hello world" (replace, not append)
...
});
it("mergeStreamingText does not treat a short delta as cumulative", async () => {
// streamText = "is" (first chunk)
// nextText = "This is the answer" (true delta starting differently)
// expected: streamText = "isThis is the answer" (append)
...
});
```
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: e69de74499
ℹ️ 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".
| if (nextText.includes(streamText)) { | ||
| streamText = nextText; |
There was a problem hiding this comment.
Avoid replacing stream text on arbitrary substring matches
Using nextText.includes(streamText) to classify cumulative partials can drop already-streamed content when the upstream sends true deltas that happen to repeat prior output. In that case a delta chunk like "abc def" after "abc" matches this condition, so streamText is replaced instead of appended and the earlier prefix is lost in the card update/final close path. This regression appears whenever partial payloads are delta-style (which this function still claims to support via the append fallback), so the replacement check should be constrained to a safer cumulative-only signal.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 31e1f3d121
ℹ️ 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".
| if (streamText.trimStart().startsWith(nextText.trimStart())) { | ||
| return; |
There was a problem hiding this comment.
Tighten reverse-prefix dedupe before dropping partial chunks
The new streamText.trimStart().startsWith(nextText.trimStart()) fast-path can suppress valid delta chunks whenever a new token happens to match the beginning of the response (or is whitespace-only after trimming). For example, after streamText = "Hello there. ", an incoming delta "Hello" is dropped because "Hello there. ".trimStart().startsWith("Hello") is true, so the repeated word is never appended; the same happens for "\n" because trimming yields an empty string. This regresses content accuracy in streaming updates and can leak into the final card text when the flow closes from partials without a final payload.
Useful? React with 👍 / 👎.
mergeStreamingText appended the full cumulative text when nextText.startsWith(streamText) failed due to leading whitespace or directive-stripping differences. This caused the card to render the same content 3+ times, growing multiplicatively with each streaming update cycle. Add a nextText.includes(streamText) check to detect cumulative payloads even when prefixes diverge, and replace rather than append. Fixes openclaw#34108 Fixes openclaw#33883
Replace the broad String.includes heuristic with trimStart().startsWith() to avoid false positives when streamText is short. This precisely handles the leading-whitespace prefix mismatch without the risk of accidentally replacing accumulated text with a delta that happens to contain a short streamText as a substring.
a5c9c18 to
5112a68
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5112a68886
ℹ️ 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".
| if (streamText.trimStart().startsWith(nextText.trimStart())) { | ||
| return; |
There was a problem hiding this comment.
Remove reverse-prefix suppression for streamed deltas
The new reverse-prefix guard drops valid delta chunks whenever a chunk starts with text that already appeared at the beginning of the response. In mergeStreamingText, streamText.trimStart().startsWith(nextText.trimStart()) returns true for inputs like existing "Hello there. " plus delta "Hello" (or whitespace-only chunks where trimStart() becomes empty), so the function returns early and never appends text; if the stream closes from partials, the final card content is missing those tokens.
Useful? React with 👍 / 👎.
|
This no longer patches a live gap on current I re-checked the current Feishu streaming implementation, and the overlap / cumulative-text handling is already present in
So the underlying bug family is real and separate from #43683, but this specific PR is now stale against the current codebase and still carries the earlier review concern about broad Closing as already fixed on |
Summary
Fixes #34108 and #33883 — streaming card updates rendering the same content 3+ times.
Root Cause
mergeStreamingTextinreply-dispatcher.tsusesnextText.startsWith(streamText)to detect cumulative payloads. When this check fails (e.g., leading whitespace or directive stripping changes the prefix), the function falls through tostreamText += nextText— appending the entire cumulative text again instead of replacing.Fix
Add a
nextText.includes(streamText)check before the append fallback. If the incoming text contains the current stream text as a substring, it is the full cumulative output and should replace rather than append.Testing
All 16 existing reply-dispatcher tests pass.