fix(cron): merge multi-chunk text payloads for announce delivery#41365
fix(cron): merge multi-chunk text payloads for announce delivery#41365GodsBoy wants to merge 3 commits intoopenclaw:mainfrom
Conversation
…nclaw#13812) Since v2026.2.9, announce mode on isolated cron jobs delivered only the last text chunk when the agent produced multiple text payloads, causing truncated output on Telegram forum topics, Feishu, and MS Teams. Root cause: pickLastNonEmptyTextFromPayloads selected only the final non-empty payload instead of concatenating all non-error text chunks. Add mergeNonErrorTextPayloads helper that concatenates all non-error text payloads and use the merged text for delivery payloads when no structured content (media/channelData) is present. This ensures the full multi-line report output is delivered verbatim to the target topic. Fixes openclaw#13812
Greptile SummaryThis PR fixes a regression (introduced in v2026.2.9) where
Issues found:
Confidence Score: 3/5
Last reviewed commit: 7547212 |
| continue; | ||
| } | ||
| const text = typeof payload?.text === "string" ? payload.text : ""; | ||
| if (!text) { | ||
| continue; | ||
| } | ||
| merged += text; | ||
| sawText = true; |
There was a problem hiding this comment.
Whitespace-only payloads bypass the empty check
mergeNonErrorTextPayloads uses a bare falsiness check (if (!text)) to skip empty payloads, so a chunk containing only whitespace (e.g. " ") is truthy and gets appended to merged. By contrast, pickLastNonEmptyTextFromPayloads (the function this replaces in the hot path) trims before checking:
const clean = (payloads[i]?.text ?? "").trim();
if (clean) { return clean; }As a result, whitespace-only chunks that the old code would silently ignore will now appear verbatim in the final merged string and be delivered to the channel. A simple guard matches the existing helper's behaviour:
| continue; | |
| } | |
| const text = typeof payload?.text === "string" ? payload.text : ""; | |
| if (!text) { | |
| continue; | |
| } | |
| merged += text; | |
| sawText = true; | |
| const text = typeof payload?.text === "string" ? payload.text.trim() : ""; | |
| if (!text) { | |
| continue; | |
| } | |
| merged += text; | |
| sawText = true; |
The unit tests in helpers.test.ts only cover "" (empty string) in the "returns undefined when all payloads have empty text" case; adding a " " (whitespace-only) assertion would catch this gap.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/helpers.ts
Line: 22-29
Comment:
**Whitespace-only payloads bypass the empty check**
`mergeNonErrorTextPayloads` uses a bare falsiness check (`if (!text)`) to skip empty payloads, so a chunk containing only whitespace (e.g. `" "`) is truthy and gets appended to `merged`. By contrast, `pickLastNonEmptyTextFromPayloads` (the function this replaces in the hot path) trims before checking:
```ts
const clean = (payloads[i]?.text ?? "").trim();
if (clean) { return clean; }
```
As a result, whitespace-only chunks that the old code would silently ignore will now appear verbatim in the final merged string and be delivered to the channel. A simple guard matches the existing helper's behaviour:
```suggestion
const text = typeof payload?.text === "string" ? payload.text.trim() : "";
if (!text) {
continue;
}
merged += text;
sawText = true;
```
The unit tests in `helpers.test.ts` only cover `""` (empty string) in the "returns undefined when all payloads have empty text" case; adding a `" "` (whitespace-only) assertion would catch this gap.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in c576af4 — split the check into raw (untrimmed, used for concatenation) and raw.trim() (used for emptiness check). Whitespace-only payloads are now skipped without altering the content of real payloads. Added test case for whitespace-only filtering.
| let deliveryPayloads = | ||
| deliveryPayload !== undefined && deliveryPayloadHasStructuredContent | ||
| ? [deliveryPayload] | ||
| : mergedText | ||
| ? [{ text: mergedText }] | ||
| : synthesizedText | ||
| ? [{ text: synthesizedText }] | ||
| : []; |
There was a problem hiding this comment.
Text-only deliveryPayload is silently dropped
Before this PR the condition was:
deliveryPayload !== undefined
? [deliveryPayload]
: ...Now it is:
deliveryPayload !== undefined && deliveryPayloadHasStructuredContent
? [deliveryPayload]
: mergedText
? [{ text: mergedText }]
: ...When deliveryPayload exists but carries only text (no mediaUrl, mediaUrls, or channelData), the new code discards deliveryPayload entirely and constructs a fresh { text: mergedText } object instead. This is the intended behaviour for the merge fix, and is safe as long as the DeliveryPayload type captures all fields that downstream delivery code cares about.
However, the code comments and the PR description don't acknowledge this side-effect explicitly. If deliveryPayload ever carries extra runtime fields beyond what the TypeScript type declares (e.g. metadata added by a plugin), those fields would be silently dropped. It would be safer to either spread deliveryPayload and override just the text field when structured content is absent, or add an explicit comment noting that the object is intentionally reconstructed:
// When there is no structured content, rebuild the payload with merged text
// so that all non-error text chunks are included verbatim (#13812).
: mergedText
? [{ ...deliveryPayload, text: mergedText }] // spread to preserve any extra fields
: ...Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/run.ts
Line: 772-779
Comment:
**Text-only `deliveryPayload` is silently dropped**
Before this PR the condition was:
```ts
deliveryPayload !== undefined
? [deliveryPayload]
: ...
```
Now it is:
```ts
deliveryPayload !== undefined && deliveryPayloadHasStructuredContent
? [deliveryPayload]
: mergedText
? [{ text: mergedText }]
: ...
```
When `deliveryPayload` exists but carries **only text** (no `mediaUrl`, `mediaUrls`, or `channelData`), the new code discards `deliveryPayload` entirely and constructs a fresh `{ text: mergedText }` object instead. This is the intended behaviour for the merge fix, and is safe as long as the `DeliveryPayload` type captures all fields that downstream delivery code cares about.
However, the code comments and the PR description don't acknowledge this side-effect explicitly. If `deliveryPayload` ever carries extra runtime fields beyond what the TypeScript type declares (e.g. metadata added by a plugin), those fields would be silently dropped. It would be safer to either spread `deliveryPayload` and override just the `text` field when structured content is absent, or add an explicit comment noting that the object is intentionally reconstructed:
```ts
// When there is no structured content, rebuild the payload with merged text
// so that all non-error text chunks are included verbatim (#13812).
: mergedText
? [{ ...deliveryPayload, text: mergedText }] // spread to preserve any extra fields
: ...
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in c576af4 — now uses { ...deliveryPayload, text: mergedText } to preserve any metadata fields (replyToId, replyToTag, etc.) from the original delivery payload when rebuilding with merged text.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7547212ea2
ℹ️ 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".
src/cron/isolated-agent/run.ts
Outdated
| deliveryPayload !== undefined && deliveryPayloadHasStructuredContent | ||
| ? [deliveryPayload] | ||
| : mergedText | ||
| ? [{ text: mergedText }] |
There was a problem hiding this comment.
Preserve text payload metadata when merging announce chunks
This branch drops the original deliveryPayload whenever it is text-only and rebuilds it as { text: mergedText }, which strips metadata fields like replyToId, replyToCurrent, and replyToTag from the payload. That changes delivery behavior for cron announce runs where the agent payload intentionally targets a reply/thread: the outbound layer reads payload.replyToId when building send overrides (src/infra/outbound/deliver.ts:713), so these messages will now be posted as standalone messages instead of threaded replies once chunk merging is enabled.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in c576af4 — spread operator preserves all original payload fields including replyToId, replyToCurrent, and replyToTag.
…ged delivery - Use trim() for emptiness check in mergeNonErrorTextPayloads without altering the raw text used for concatenation, matching the behaviour of pickLastNonEmptyTextFromPayloads. - Spread deliveryPayload when rebuilding with merged text to preserve metadata fields like replyToId and replyToTag. - Add test for whitespace-only payload filtering. Addresses review feedback on openclaw#41365.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c576af4987
ℹ️ 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".
| deliveryPayload !== undefined && deliveryPayloadHasStructuredContent | ||
| ? [deliveryPayload] | ||
| : mergedText | ||
| ? [{ ...deliveryPayload, text: mergedText }] |
There was a problem hiding this comment.
Preserve merge metadata from all text chunks
When mergedText is selected, the new payload is built from only deliveryPayload (the last deliverable chunk), so per-chunk metadata from earlier chunks is dropped. In buildEmbeddedRunPayloads each assistant chunk is parsed independently for reply directives (replyToId, replyToTag, replyToCurrent), so a multi-chunk response where only the first chunk carries threading directives will now deliver merged text without those directives and post to the wrong thread/channel context.
Useful? React with 👍 / 👎.
|
Hey @obviyus — this PR is ready for review when you get a chance. CI is clean: all checks pass. The only failure is the Happy to address any feedback. |
Summary
delivery.mode='announce'on isolated cron jobs targeting Telegram forum topics (and Feishu/MS Teams) delivered only a truncated summary instead of the full multi-line report outputpickLastNonEmptyTextFromPayloadsselected only the final non-empty payload instead of concatenating all non-error text chunksmergeNonErrorTextPayloadshelper that concatenates all non-error text payloads and uses the merged text for delivery when no structured content (media/channelData) is presentFixes #13812
Test plan
mergeNonErrorTextPayloads(empty, single, multi-chunk, error-skipping)