Skip to content

feat(adapters): add sendPayload to batch-d (Zalo, Zalouser, core outbound plugins)#30144

Merged
steipete merged 5 commits intoopenclaw:mainfrom
nohat:lifecycle/adapter-sendpayload-batch-d-v2
Mar 2, 2026
Merged

feat(adapters): add sendPayload to batch-d (Zalo, Zalouser, core outbound plugins)#30144
steipete merged 5 commits intoopenclaw:mainfrom
nohat:lifecycle/adapter-sendpayload-batch-d-v2

Conversation

@nohat
Copy link
Contributor

@nohat nohat commented Feb 28, 2026

Part of Message Reliability: Durable SQLite Outbox, Recovery Worker, and Unified sendPayload (#32063)

Summary

  • Problem: Channel adapters lack a unified sendPayload method for the delivery pipeline
  • Why it matters: The outbox-based delivery pipeline (feat(outbound): prefer sendPayload for all payloads when adapter supports it #29997) needs sendPayload per adapter
  • What changed: Added sendPayload to batch-d adapters (Zalo, Zalouser) and core outbound plugins (direct-text-media, Discord, Slack, WhatsApp). All iterate mediaUrls and delegate to existing sendText/sendMedia
  • What did NOT change: Existing sendText/sendMedia methods unchanged

Change Type (select all)

  • Feature

Scope (select all touched areas)

  • Integrations

Linked Issue/PR

User-visible / Behavior Changes

None — additive internal method only.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Any
  • Runtime/container: Node 22+
  • Integration/channel: Zalo, Zalouser, Discord, Slack, WhatsApp

Steps

  1. pnpm check passes
  2. Send messages through affected channels — delivery unchanged

Expected

  • No behavior change; new method available for delivery pipeline

Actual

  • Same as expected

Evidence

  • Failing test/log before + passing after (CI green)

Human Verification (required)

  • Verified scenarios: TypeScript compilation, existing test suite
  • Edge cases checked: Media-only, text-only, mixed payloads via sendPayload
  • What you did not verify: Live delivery through all 5 channels

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

Risks and Mitigations

None — additive change.

@openclaw-barnacle openclaw-barnacle bot added channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser size: S labels Feb 28, 2026
@nohat nohat force-pushed the lifecycle/adapter-sendpayload-batch-d-v2 branch 2 times, most recently from 3a2ab74 to 03962fe Compare March 2, 2026 04:10
@nohat nohat marked this pull request as ready for review March 2, 2026 04:39
Copilot AI review requested due to automatic review settings March 2, 2026 04:39
Copy link

@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: 03962fe6c8

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
return lastResult;
}
return outbound.sendText!({ ...ctx });

Choose a reason for hiding this comment

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

P2 Badge Preserve chunking for text-only sendPayload deliveries

When channelData is present, deliverOutboundPayloads routes through sendPayload instead of sendTextChunks (src/infra/outbound/deliver.ts), and this branch sends text with a single sendText call. For adapters created by this helper (textChunkLimit: 4000), a text-only payload that includes metadata in channelData will no longer be chunked, so long messages can exceed channel limits (or bypass Signal’s special chunking/format path) even though the non-sendPayload flow handled them safely.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Chunking is handled at the delivery layer in deliver.ts (PR #29997, commit 835566c). When channelData is present, deliverOutboundPayloadsCore chunks text before calling sendPayload — leading chunks go via sendText, the final chunk (with channelData) goes via sendPayload. Adapters do not need to chunk internally.

The text-only fallback now explicitly passes text: ctx.payload.text ?? "" (b07f80e) for defensive clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correction: chunking is now handled in the adapter's sendPayload (commit 61b6475), using the factory's own chunker/textChunkLimit. Test coverage added.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds sendPayload support across several outbound adapters/plugins to support the outbox-based delivery pipeline that prefers a single unified adapter entrypoint.

Changes:

  • Added sendPayload to core outbound adapters: WhatsApp, Slack, Discord.
  • Added sendPayload to the shared direct text/media outbound factory (affecting iMessage/Signal).
  • Added sendPayload to batch-d extension adapters: Zalo and Zalouser.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/channels/plugins/outbound/whatsapp.ts Adds sendPayload that delegates to existing sendText / sendMedia for WhatsApp.
src/channels/plugins/outbound/slack.ts Adds sendPayload that delegates to existing sendText / sendMedia for Slack.
src/channels/plugins/outbound/discord.ts Adds sendPayload that delegates to existing sendText / sendMedia for Discord.
src/channels/plugins/outbound/direct-text-media.ts Adds sendPayload to the shared direct adapter factory by sequencing sendMedia / sendText.
extensions/zalouser/src/channel.ts Adds sendPayload to Zalouser outbound adapter delegating to existing methods.
extensions/zalo/src/channel.ts Adds sendPayload to Zalo outbound adapter delegating to existing methods.

Comment on lines +94 to +116
sendPayload: async (ctx) => {
const urls = ctx.payload.mediaUrls?.length
? ctx.payload.mediaUrls
: ctx.payload.mediaUrl
? [ctx.payload.mediaUrl]
: [];
if (urls.length > 0) {
let lastResult = await outbound.sendMedia!({
...ctx,
text: ctx.payload.text ?? "",
mediaUrl: urls[0],
});
for (let i = 1; i < urls.length; i++) {
lastResult = await outbound.sendMedia!({
...ctx,
text: "",
mediaUrl: urls[i],
});
}
return lastResult;
}
return outbound.sendText!({ ...ctx });
},
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

sendPayload is newly added here (and therefore to the iMessage/Signal adapters that are built from this factory), but there are no unit tests exercising the new delegation behavior (text-only, media-only, multi-media ordering/captioning). Consider adding tests similar to the existing telegramOutbound.sendPayload tests to ensure the first media gets the caption and subsequent media are sent with empty text, and that the returned result is the final send.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — sendPayload test coverage will be added in a follow-up. The delegation logic is straightforward (iterate URLs → sendMedia, fallback → sendText) and is now defensively explicit about the text field (b07f80e).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test coverage added in 61b6475.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

Adds a unified sendPayload method to six channel adapters (Zalo, Zalouser, Discord, Slack, WhatsApp, and direct-text-media) to support the delivery pipeline (#29997). The implementation follows a consistent pattern across all adapters:

  • Consolidates mediaUrls array and single mediaUrl into a unified array
  • Sends first media with text, subsequent media without text to avoid duplication
  • Falls back to text-only when no media is present
  • The HEAD commit (03962fe) includes a fix that restructures the media loop to initialize lastResult immediately rather than leaving it uninitialized, eliminating the need for non-null assertions

The changes are additive only—existing sendText and sendMedia methods remain unchanged. The implementation is consistent with patterns used in other adapters like Telegram and LINE.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is straightforward, additive-only, and follows existing patterns in the codebase. The HEAD commit includes a proactive fix for variable initialization. All adapters implement the same logic consistently, reducing the risk of adapter-specific bugs. The new method won't be actively used until the delivery pipeline PR (feat(outbound): prefer sendPayload for all payloads when adapter supports it #29997) is merged.
  • No files require special attention

Last reviewed commit: 03962fe

Copy link

@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: b07f80ee25

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
return lastResult;
}
return whatsappOutbound.sendText!({ ...ctx, text: ctx.payload.text ?? "" });

Choose a reason for hiding this comment

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

P2 Badge Keep chunking for text-only sendPayload deliveries

deliverOutboundPayloads routes any payload with non-empty channelData through adapter sendPayload, but this fallback sends text via a single sendText call (ctx.payload.text ?? "") instead of the adapter chunker. In whatsappOutbound (and the identical pattern in extensions/zalo/src/channel.ts and extensions/zalouser/src/channel.ts), a long text payload that includes metadata in channelData will bypass textChunkLimit and can exceed provider limits or fail, even though the non-sendPayload path would chunk it.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 61b6475. sendPayload now chunks text using the adapter's own chunker/textChunkLimit. Test coverage added.

}
return lastResult;
}
return outbound.sendText!({ ...ctx, text: ctx.payload.text ?? "" });

Choose a reason for hiding this comment

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

P2 Badge Skip empty Signal sends in sendPayload fallback

When payload has channelData but no text/media, this fallback always calls sendText with an empty string. For the Signal adapter (which is built from this helper), that reaches sendMessageSignal, which throws when both text and media are empty; channelData-only payloads are intentionally preserved upstream, so these deliveries now fail instead of no-oping as they did before sendPayload was added.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 61b6475. Empty payloads return a no-op result. Test coverage added.

Copy link

@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: 61b6475a60

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +119 to +120
const limit = outbound.textChunkLimit;
const chunks = limit && outbound.chunker ? outbound.chunker(text, limit) : [text];

Choose a reason for hiding this comment

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

P2 Badge Use resolved chunk limits when chunking payload text

When channelData is present, delivery switches to sendPayload, and this implementation chunks using outbound.textChunkLimit directly instead of the delivery-layer resolveTextChunkLimit/resolveChunkMode logic. That means channel/account overrides are ignored for these payloads: for example, a configured smaller Signal/iMessage limit (or newline chunk mode) is bypassed and the hardcoded adapter limit behavior is used instead, which can change chunking semantics and violate configured limits.

Useful? React with 👍 / 👎.

Comment on lines +122 to +123
for (const chunk of chunks) {
lastResult = await outbound.sendText!({ ...ctx, text: chunk });

Choose a reason for hiding this comment

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

P2 Badge Check cancellation between chunk sends

This loop sends all chunks inside sendPayload without any abort checks, so once execution enters this method it will continue sending remaining chunks even if delivery is cancelled mid-flight. Before this commit, channelData payloads on this adapter went through delivery-layer chunk/media loops that call throwIfAborted between sends, so cancellation could stop subsequent messages.

Useful? React with 👍 / 👎.

@steipete steipete force-pushed the lifecycle/adapter-sendpayload-batch-d-v2 branch from 61b6475 to 0f73e2d Compare March 2, 2026 15:35
@steipete steipete merged commit c63c179 into openclaw:main Mar 2, 2026
6 checks passed
@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: pnpm test -- extensions/zalo/src/channel.sendpayload.test.ts extensions/zalouser/src/channel.sendpayload.test.ts src/channels/plugins/outbound/direct-text-media.sendpayload.test.ts src/channels/plugins/outbound/discord.sendpayload.test.ts src/channels/plugins/outbound/slack.sendpayload.test.ts src/channels/plugins/outbound/whatsapp.sendpayload.test.ts
  • Land commit: 0f73e2d
  • Merge commit: c63c179

Thanks @nohat!

Copy link

@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: 0f73e2d3ee

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +40 to +42
const limit = whatsappOutbound.textChunkLimit;
const chunks =
limit && whatsappOutbound.chunker ? whatsappOutbound.chunker(text, limit) : [text];

Choose a reason for hiding this comment

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

P2 Badge Respect resolved chunk config in WhatsApp sendPayload

This sendPayload implementation chunks text with whatsappOutbound.textChunkLimit directly, which bypasses the delivery-layer resolveTextChunkLimit/resolveChunkMode logic used for normal sends. For payloads that include channelData, account/channel overrides like channels.whatsapp.accounts.<id>.textChunkLimit or chunkMode: "newline" are ignored, so chunking semantics can diverge from configured behavior and long messages may exceed limits that were explicitly configured to prevent that.

Useful? React with 👍 / 👎.

Comment on lines +31 to +33
for (let i = 1; i < urls.length; i++) {
lastResult = await whatsappOutbound.sendMedia!({
...ctx,

Choose a reason for hiding this comment

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

P2 Badge Stop sendPayload loops when delivery is aborted

The new media iteration loop does not check cancellation between sends. deliverOutboundPayloadsCore only checks throwIfAborted before calling sendPayload, so for channelData payloads an abort that happens after the first media send will not prevent subsequent media sends in this loop. This can produce unintended extra outbound messages even after callers cancel the delivery.

Useful? React with 👍 / 👎.

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

Labels

channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants