Skip to content

fix(cron): merge multi-chunk text payloads for announce delivery#41365

Open
GodsBoy wants to merge 3 commits intoopenclaw:mainfrom
GodsBoy:fix/cron-announce-topic-delivery-13812
Open

fix(cron): merge multi-chunk text payloads for announce delivery#41365
GodsBoy wants to merge 3 commits intoopenclaw:mainfrom
GodsBoy:fix/cron-announce-topic-delivery-13812

Conversation

@GodsBoy
Copy link
Copy Markdown
Contributor

@GodsBoy GodsBoy commented Mar 9, 2026

Summary

  • Since v2026.2.9, 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 output
  • Root cause: pickLastNonEmptyTextFromPayloads selected only the final non-empty payload instead of concatenating all non-error text chunks
  • Adds mergeNonErrorTextPayloads helper that concatenates all non-error text payloads and uses the merged text for delivery when no structured content (media/channelData) is present
  • This ensures the full report output is delivered verbatim to the target topic, fixing both the truncation and the relay noise that appeared in DM sessions when only partial content was delivered

Fixes #13812

Test plan

  • Unit tests for mergeNonErrorTextPayloads (empty, single, multi-chunk, error-skipping)
  • Integration test: multi-chunk payloads delivered to forum topic contain full merged text
  • Integration test: multi-chunk payloads delivered to plain Telegram target contain full merged text
  • Existing forum topic routing tests updated to use multi-chunk payloads
  • Full cron test suite passes (491 tests across 61 files)
  • TypeScript typecheck clean

…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-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR fixes a regression (introduced in v2026.2.9) where delivery.mode='announce' cron jobs targeting Telegram forum topics (and Feishu/MS Teams) delivered only the last non-empty text chunk instead of the full multi-chunk agent output. The fix introduces mergeNonErrorTextPayloads in helpers.ts and wires it into the delivery-payload construction in run.ts so all non-error text chunks are concatenated before delivery when no structured content (media/channelData) is present.

  • mergeNonErrorTextPayloads accumulates all non-error, non-empty text payloads into a single string and is exported and mocked properly in the test harness.
  • The deliveryPayloads construction in run.ts now gates on deliveryPayloadHasStructuredContent before using the raw deliveryPayload object; text-only payloads are superseded by the merged result.
  • The skips-delivery integration test's expectedText was updated from "Final weather summary" to "Working on it...Final weather summary", correctly reflecting that intermediate chunks are now included.
  • Two new integration tests cover multi-chunk delivery to forum topics and plain Telegram targets.

Issues found:

  • mergeNonErrorTextPayloads uses a bare falsiness check (if (!text)) rather than trimming before checking, so whitespace-only chunks (e.g. " ") will pass through and appear in the final delivery string. pickLastNonEmptyTextFromPayloads trims before checking; the new function should do the same for consistency.
  • When deliveryPayload has text but no structured content it is now silently discarded and a fresh { text: mergedText } object is constructed. This is intentional but not documented in comments; if any runtime fields beyond the typed ones are ever present on deliveryPayload, they would be dropped silently.

Confidence Score: 3/5

  • The core fix is correct but a whitespace-only payload edge case can cause empty/whitespace deliveries that the old code suppressed.
  • The structural logic of the fix is sound and the test coverage is thorough for the happy path. The whitespace-handling inconsistency between mergeNonErrorTextPayloads (no trim before empty check) and pickLastNonEmptyTextFromPayloads (trims) is a genuine behavioural gap not covered by tests, and could cause unexpected whitespace deliveries. The silent discard of text-only deliveryPayload objects is intentional but lacks documentation and could hide future regressions if the payload type evolves.
  • Pay close attention to src/cron/isolated-agent/helpers.ts (whitespace handling) and src/cron/isolated-agent/run.ts (deliveryPayload discard).

Last reviewed commit: 7547212

Comment on lines +22 to +29
continue;
}
const text = typeof payload?.text === "string" ? payload.text : "";
if (!text) {
continue;
}
merged += text;
sawText = true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +772 to +779
let deliveryPayloads =
deliveryPayload !== undefined && deliveryPayloadHasStructuredContent
? [deliveryPayload]
: mergedText
? [{ text: mergedText }]
: synthesizedText
? [{ text: synthesizedText }]
: [];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

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

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: 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".

Comment on lines +773 to +776
deliveryPayload !== undefined && deliveryPayloadHasStructuredContent
? [deliveryPayload]
: mergedText
? [{ text: mergedText }]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c576af4 — spread operator preserves all original payload fields including replyToId, replyToCurrent, and replyToTag.

GodsBoy added 2 commits March 9, 2026 21:01
…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.
Copy link
Copy Markdown

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

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: 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 }]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@GodsBoy
Copy link
Copy Markdown
Contributor Author

GodsBoy commented Mar 10, 2026

Hey @obviyus — this PR is ready for review when you get a chance.

CI is clean: all checks pass. The only failure is the secrets scanner which is a known pre-existing flaky check on this repo (not related to this change).

Happy to address any feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: v2026.2.9 cron announce → Telegram topic sends summary instead of full output

1 participant