Skip to content

feat(outbound): prefer sendPayload for all payloads when adapter supports it#29997

Open
nohat wants to merge 3 commits intoopenclaw:mainfrom
nohat:lifecycle/deliver-sendpayload-universal
Open

feat(outbound): prefer sendPayload for all payloads when adapter supports it#29997
nohat wants to merge 3 commits intoopenclaw:mainfrom
nohat:lifecycle/deliver-sendpayload-universal

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: Outbound delivery splits every payload into separate sendText/sendMedia calls, requiring each adapter to handle routing logic
  • Why it matters: The outbox-based delivery pipeline needs a single entry point per adapter for payload delivery
  • What changed: deliverOutboundPayloads now checks for sendPayload on the adapter and uses it when available, falling back to sendText/sendMedia
  • What did NOT change: Existing sendText/sendMedia methods remain unchanged; adapters without sendPayload work exactly as before

Change Type (select all)

  • Feature

Scope (select all touched areas)

  • Gateway / orchestration

Linked Issue/PR

User-visible / Behavior Changes

None — internal delivery path change 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+
  • Model/provider: Any
  • Integration/channel: All channels with sendPayload adapters

Steps

  1. Send a message through any channel with a sendPayload adapter
  2. Verify delivery succeeds via the new path
  3. Send via a channel without sendPayload — verify fallback works

Expected

  • Messages delivered successfully through both paths

Actual

  • Same as expected

Evidence

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

Human Verification (required)

  • Verified scenarios: Message delivery through channels with and without sendPayload
  • Edge cases checked: Media-only, text-only, mixed payloads
  • What you did not verify: Every channel adapter individually (covered by adapter batch PRs)

Compatibility / Migration

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

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert commit; delivery falls back to sendText/sendMedia
  • Files/config to restore: src/infra/outbound/deliver.ts
  • Known bad symptoms: Messages not delivered, adapter errors in logs

Risks and Mitigations

  • Risk: Adapter sendPayload implementation bugs could cause delivery failures
    • Mitigation: Fallback to sendText/sendMedia when sendPayload is not present; adapter batch PRs tested independently

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 28, 2026

Greptile Summary

This PR simplifies the outbound delivery logic by removing the channelData check before calling sendPayload. Now sendPayload is invoked for all payloads when the adapter supports it, regardless of whether channelData is present.

Key changes:

  • Previously, sendPayload was only called when effectivePayload.channelData existed (line 548 in old code)
  • Now sendPayload is preferred whenever the adapter implements it (line 553 in new code)
  • Falls back to sendText/sendMedia only when adapter lacks sendPayload
  • Added test coverage for the no-channelData case

Safety verification:

  • ReplyPayload.channelData is already typed as optional (channelData?: Record<string, unknown>)
  • Both existing adapter implementations (Telegram and LINE) use optional chaining (payload.channelData?.telegram, payload.channelData?.line) to safely handle undefined channelData
  • Fallback logic (lines 563-599) remains unchanged and continues to work correctly

This change enables a consistent single-call delivery path for all payloads, which is important for the write-ahead outbox recovery system.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The change is well-designed and properly implemented. The type system already defines channelData as optional, and all existing adapter implementations correctly handle undefined channelData using optional chaining. The new test provides good coverage for the changed behavior, and the fallback logic remains intact. This is a clean refactoring that simplifies the codebase while maintaining backward compatibility.
  • No files require special attention

Last reviewed commit: e325283

Copy link

@nikolasdehor nikolasdehor left a comment

Choose a reason for hiding this comment

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

The simplification makes sense — having sendPayload as the universal entry point is cleaner. Two considerations: (1) Third-party adapters implementing sendPayload might assume channelData is always present. Is there documentation or a changelog entry warning adapter authors about this behavioral change? (2) Since this is part of a broader PR stack, it would help to know the merge order and dependencies.

Copy link

@nikolasdehor nikolasdehor left a comment

Choose a reason for hiding this comment

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

The implementation looks correct but I have a design question: this overlaps significantly with #30010 which takes a different approach to the same problem. Have you coordinated with the author of that PR? It would be good to align on one approach before both get merged.

Also, consider adding a test for the edge case where the agent entry is partially populated (has some fields but not others).

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

This PR updates the outbound delivery orchestrator to prefer a unified adapter entry point (sendPayload) whenever the active channel adapter supports it, with a fallback to the existing sendText/sendMedia path.

Changes:

  • Update deliverOutboundPayloadsCore to call handler.sendPayload when available (instead of only when channelData is present).
  • Add a unit test asserting sendPayload is used even when channelData is not provided.

Reviewed changes

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

File Description
src/infra/outbound/deliver.ts Routes payload delivery through sendPayload whenever supported by the adapter.
src/infra/outbound/deliver.test.ts Adds coverage to ensure sendPayload is used for plain-text payloads (no channelData).

Comment on lines +551 to +553
// Prefer sendPayload when the adapter supports it (handles all payload fields
// including channelData, media, text in a single call).
if (handler.sendPayload) {
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.

Calling sendPayload for all payloads bypasses the existing text chunking / limit enforcement logic (e.g., Telegram clamping to 4096 and markdown chunking via sendTextChunks). Since adapters like telegramOutbound.sendPayload currently send the full payload.text as-is, this can regress long-message delivery (and will also invalidate the existing Telegram chunking tests in this file). Consider either (a) only using sendPayload when no chunking is needed, or (b) applying the same chunking logic while still invoking sendPayload (e.g., split text into chunks and call sendPayload per chunk, or require/ensure adapters implement chunking internally).

Suggested change
// Prefer sendPayload when the adapter supports it (handles all payload fields
// including channelData, media, text in a single call).
if (handler.sendPayload) {
// Prefer sendPayload for media payloads when the adapter supports it (handles all
// payload fields including channelData, media, text in a single call) but let
// text-only messages fall through to the existing chunking logic.
if (handler.sendPayload && payloadSummary.mediaUrls.length > 0) {

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.

Fixed in 835566c. When text exceeds textChunkLimit and the adapter has a chunker, leading chunks are now sent via sendText and only the final chunk goes through sendPayload (preserving channelData like inline keyboards on the last message). Short messages and adapters without a chunker are unaffected.

@nohat nohat force-pushed the lifecycle/deliver-sendpayload-universal branch from 93f8f79 to 6bf30db Compare March 2, 2026 05:09
@nohat nohat force-pushed the lifecycle/deliver-sendpayload-universal branch 2 times, most recently from 835566c to a4fb229 Compare March 2, 2026 17:39
nohat added 3 commits March 5, 2026 08:36
When text exceeds the adapter's textChunkLimit, send leading chunks via
sendText and pass only the final chunk through sendPayload. This preserves
channel-specific features (inline keyboards, reply markup) on the last
message while respecting provider text length limits.
@nohat nohat force-pushed the lifecycle/deliver-sendpayload-universal branch from 96850cf to cd58240 Compare March 5, 2026 16:37
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.

3 participants