fix(feishu): treat block payloads as snapshots to avoid duplication with tool calls#39085
fix(feishu): treat block payloads as snapshots to avoid duplication with tool calls#39085babycoder-dev wants to merge 2 commits intoopenclaw:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e979e5562f
ℹ️ 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".
| @@ -280,7 +280,8 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP | |||
| if (info?.kind === "block") { | |||
| // Some runtimes emit block payloads without onPartial/final callbacks. | |||
| // Mirror block text into streamText so onIdle close still sends content. | |||
| queueStreamingUpdate(text, { mode: "delta" }); | |||
| // Use dedupeWithLastPartial to avoid duplicating content already sent via onPartialReply. | |||
| queueStreamingUpdate(text, { mode: "delta", dedupeWithLastPartial: true }); | |||
There was a problem hiding this comment.
Preserve repeated block deltas in streaming cards
Applying dedupeWithLastPartial: true to block updates drops any consecutive block chunks that have identical text, but this path is explicitly using mode: "delta". In block-streaming mode, identical adjacent deltas are valid output (for example, repeated short chunks), so the second chunk is now discarded and the final card content is truncated whenever runtimes emit duplicate block deltas without an intervening different partial.
Useful? React with 👍 / 👎.
| const isBundledAndConfig = | ||
| new Set([existing.candidate.origin, candidate.origin]).has("bundled") && | ||
| new Set([existing.candidate.origin, candidate.origin]).has("config"); |
There was a problem hiding this comment.
Keep duplicate-id warnings for distinct config overrides
This condition suppresses duplicate plugin-id warnings purely on origin (bundled + config) and does not require both candidates to resolve to the same directory. That means a config-loaded plugin with the same id as a bundled plugin but from a different path is now silently accepted, hiding real id collisions/shadowing in plugins.load.paths where operators previously got a warning.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR addresses duplicate content in Feishu streaming cards when Key change in
Issue identified in Confidence Score: 2/5
Last reviewed commit: e979e55 |
| if (isBundledAndConfig) { | ||
| // Skip the duplicate warning - config entry is intentionally for the same bundled channel plugin | ||
| // Add both entries so loader can handle precedence | ||
| seenIds.set(manifest.id, { candidate, recordIndex: records.length }); | ||
| records.push( | ||
| buildRecord({ | ||
| manifest, | ||
| candidate, | ||
| manifestPath: manifestRes.manifestPath, | ||
| schemaCacheKey, | ||
| configSchema, | ||
| }), | ||
| ); | ||
| continue; |
There was a problem hiding this comment.
The isBundledAndConfig branch pushes a new record to records[] and updates seenIds to point to the current candidate. If a third candidate with the same manifest.id arrives with alternating origin (e.g., another bundled after seenIds was updated to config), the condition evaluates to true again and pushes another record. This can repeat unboundedly with each alternating candidate.
This diverges from the samePlugin path (line 221), which replaces the lower-precedence record in-place, maintaining exactly one record per manifest.id. Given the PLUGIN_ORIGIN_RANK precedence (config=0 vs bundled=3), consider replacing in-place:
| if (isBundledAndConfig) { | |
| // Skip the duplicate warning - config entry is intentionally for the same bundled channel plugin | |
| // Add both entries so loader can handle precedence | |
| seenIds.set(manifest.id, { candidate, recordIndex: records.length }); | |
| records.push( | |
| buildRecord({ | |
| manifest, | |
| candidate, | |
| manifestPath: manifestRes.manifestPath, | |
| schemaCacheKey, | |
| configSchema, | |
| }), | |
| ); | |
| continue; | |
| if (isBundledAndConfig) { | |
| // Prefer config over bundled per PLUGIN_ORIGIN_RANK precedence. | |
| if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) { | |
| records[existing.recordIndex] = buildRecord({ | |
| manifest, | |
| candidate, | |
| manifestPath: manifestRes.manifestPath, | |
| schemaCacheKey, | |
| configSchema, | |
| }); | |
| seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex }); | |
| } | |
| continue; | |
| } |
This ensures only one record per plugin ID, consistent with the samePlugin pattern.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/manifest-registry.ts
Line: 238-251
Comment:
The `isBundledAndConfig` branch pushes a new record to `records[]` and updates `seenIds` to point to the current candidate. If a third candidate with the same `manifest.id` arrives with alternating origin (e.g., another `bundled` after `seenIds` was updated to `config`), the condition evaluates to true again and pushes another record. This can repeat unboundedly with each alternating candidate.
This diverges from the `samePlugin` path (line 221), which replaces the lower-precedence record in-place, maintaining exactly one record per `manifest.id`. Given the `PLUGIN_ORIGIN_RANK` precedence (config=0 vs bundled=3), consider replacing in-place:
```suggestion
if (isBundledAndConfig) {
// Prefer config over bundled per PLUGIN_ORIGIN_RANK precedence.
if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) {
records[existing.recordIndex] = buildRecord({
manifest,
candidate,
manifestPath: manifestRes.manifestPath,
schemaCacheKey,
configSchema,
});
seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex });
}
continue;
}
```
This ensures only one record per plugin ID, consistent with the `samePlugin` pattern.
How can I resolve this? If you propose a fix, please make it concise.e979e55 to
045e4b2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 045e4b2000
ℹ️ 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".
| queueStreamingUpdate(text, { mode: "delta" }); | ||
| // Skip if already in streamText to avoid duplicating content already sent | ||
| // via onPartialReply (which uses snapshot mode and updates lastPartial). | ||
| if (!streamText.includes(text)) { |
There was a problem hiding this comment.
Stop deduping block deltas by substring match
This guard drops any block chunk whose text already appears anywhere in streamText, but this branch feeds queueStreamingUpdate(..., { mode: "delta" }), where repeated chunks are valid output. In streams that emit duplicate/recurring delta tokens (for example repeated short words or characters), later chunks are skipped and the fallback block-only path can close with truncated card content.
Useful? React with 👍 / 👎.
| }); | ||
| seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex }); | ||
| } | ||
| continue; |
There was a problem hiding this comment.
Retain bundled duplicates so loader can mark overrides
Silently continuing on bundled+config ID collisions removes the bundled entry from manifestRegistry.plugins; downstream, loadOpenClawPlugins only processes candidates whose rootDir exists in that registry map (src/plugins/loader.ts:592-604), so the bundled plugin is skipped entirely instead of being surfaced as an overridden/disabled duplicate. This regresses duplicate-precedence visibility for users inspecting plugin status and diagnostics.
Useful? React with 👍 / 👎.
045e4b2 to
b678a01
Compare
…ith tool calls When blockStreaming is enabled, tool call responses were being duplicated in streaming cards (e.g. "看起来看起来 XX 的 Actions..."). This happened because block payloads were being added via delta mode without checking if the text was already present in streamText (sent via onPartialReply in snapshot mode). Fixes openclaw#38938
b678a01 to
898c743
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b6d93365dc
ℹ️ 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".
| // Use dedupeWithLastPartial to avoid duplicating content already sent via | ||
| // onPartialReply (which sets lastPartial). This does NOT dedupe between | ||
| // consecutive blocks - only blocks matching what onPartialReply last sent. | ||
| queueStreamingUpdate(text, { mode: "delta", dedupeWithLastPartial: true }); |
There was a problem hiding this comment.
Preserve repeated block deltas in streaming updates
Passing dedupeWithLastPartial: true on the mode: "delta" block path causes legitimate repeated chunks to be dropped, because queueStreamingUpdate updates lastPartial on every deduped call and then suppresses the next identical chunk. In streams that emit identical adjacent deltas (for example repeated short tokens) without an intervening onPartialReply snapshot, the second chunk is discarded and the final card content is truncated.
Useful? React with 👍 / 👎.
|
This pull request has been automatically marked as stale due to inactivity. |
|
Good catch on the block/partial duplication. I’m closing this as covered by #71523; the canonical PR now skips exact block payloads that repeat the latest partial snapshot and keeps the final card lifecycle on one stream. If your repro involved non-exact block boundary resets, that is a narrower follow-up and I can reopen this for that boundary. |
Summary
dedupeWithLastPartial: trueto block payload handling to prevent duplicationTest plan