feat(chatwoot-adapter): relay an agent's reply-to as a WhatsApp quote - #64
Conversation
The adapter posts every message with its WA id as source_id, and Chatwoot hands that id back as content_attributes.in_reply_to_external_id when an agent replies to a specific message - but the outbound relay dropped it, so the reply arrived unquoted and the recipient couldn't tell which message it answered. Pass it through as the send envelope's replyTo, for text and media replies alike; a reply whose quoted message has no external id still goes out unquoted rather than being dropped.
There was a problem hiding this comment.
Pull request overview
This PR updates the Chatwoot adapter’s outbound relay so that when a Chatwoot agent uses “Reply to”, the outbound WhatsApp message can be sent with quote context by passing Chatwoot’s content_attributes.in_reply_to_external_id through to the OpenWA send envelope as replyTo.
Changes:
- Extend
ChatwootWebhookMessagetyping to include optionalcontent_attributeswith reply-to metadata. - Populate
ConversationSendEnvelope.replyTofor outbound sends whenin_reply_to_external_idis present. - Add tests covering quoted text replies and the missing-external-id fallback; update documentation to describe the new behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| chatwoot-adapter/outbound.ts | Adds extraction of in_reply_to_external_id and forwards it as replyTo on outbound sends. |
| chatwoot-adapter/outbound.test.ts | Adds test cases for reply-to quoting and fallback behavior. |
| chatwoot-adapter/filters.ts | Expands webhook message type to include content_attributes used by outbound relay. |
| chatwoot-adapter/CHANGELOG.md | Documents the new reply-to/quote behavior in Unreleased notes. |
Suppressed comments (1)
chatwoot-adapter/outbound.ts:99
ConversationSendEnvelopedocuments that settingreplyToon media envelopes (image/file/audio/video/voice withmediaUrl) throws because the engine media path cannot quote. This code currently addsreplyToto media sends, which can break outbound attachment replies at runtime.
type: media.type,
mediaUrl: media.url,
text: text || undefined,
...(replyTo ? { replyTo } : {}),
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // An agent "Reply to" carries the quoted message's source_id — a WA message id for everything this | ||
| // adapter posts — and the engine renders it as a real WhatsApp quote. Absent or foreign (a Chatwoot | ||
| // message without source_id quotes as external id undefined), the reply just goes unquoted. |
| test('a quoted reply with an attachment carries replyTo on the media envelope', async () => { | ||
| const { deps: d, sent } = deps(); | ||
| await handleOutbound( | ||
| d, | ||
| req({ | ||
| event: 'message_created', message_type: 'outgoing', private: false, id: 10, content: 'see this', | ||
| inbox: { id: 7 }, conversation: { id: 55 }, | ||
| attachments: [{ id: 1, file_type: 'image', data_url: 'https://chat.acme.com/blob/x.jpg' }], | ||
| content_attributes: { in_reply_to_external_id: 'WA_QUOTED_2' }, | ||
| }), | ||
| ); | ||
| assert.deepEqual(sent, [ | ||
| { sessionId: 'sess', chatId: 'c@wa', type: 'image', mediaUrl: 'https://chat.acme.com/blob/x.jpg', text: 'see this', replyTo: 'WA_QUOTED_2' }, | ||
| ]); | ||
| }); |
| answered. The relay now passes it through as the send envelope's `replyTo`, for text and attachment | ||
| replies alike. A reply whose quoted message has no external id (for example a note imported by an | ||
| external tool without a `source_id`) still goes out — just unquoted, as before, rather than being | ||
| dropped. |
|
Thanks for this — the feature is worth having, and the The media envelope can't carry a quote. A quote target can be gone. The test double now models the host. The fake Suite is 539 passing, |
… a refused quote The outbound relay set `replyTo` on the media envelope as well as the text one. The host rejects that combination outright — the engine's media path cannot quote — so every agent "Reply to" carrying an attachment threw, retried identically until the ingress budget was gone, and dead-lettered while Chatwoot showed the reply as sent. The echo marker is written only after a successful send, so nothing deduped the retries either. A media reply now goes out unquoted, with its caption intact. A text reply is still quoted, but best-effort: a quoted message that has fallen outside the engine's retained window makes the engine refuse the quote, so the reply is re-sent unquoted rather than lost. Both paths favour delivering the reply over decorating it. The test double now mirrors the host contract and rejects a media envelope carrying a quote, so this class of defect cannot pass the suite again; the attachment case asserts the absence of `replyTo`, and a new case covers the unresolvable quote target.
dd30735 to
ea5bd81
Compare
What
When an agent uses "Reply to" on a specific message in Chatwoot, the reply now arrives on WhatsApp as a real quoted reply instead of a plain message.
Why
The adapter already posts every relayed message with its WhatsApp id as
source_id, and Chatwoot hands that id back on agent replies ascontent_attributes.in_reply_to_external_id— buthandleOutboundignored the field, so the quote context was silently lost. On a busy chat the person on WhatsApp can't tell which message the agent is answering.How
ChatwootWebhookMessagegains the (optional)content_attributesshape.relay()passesin_reply_to_external_idthrough as theConversationSendEnvelope.replyTothe SDK already supports — for text and attachment replies alike. No new permissions, no envelope changes.source_id) still relays — just unquoted, as before — rather than being dropped.Tests
Three new cases in
outbound.test.ts(text quote, attachment quote, missing-external-id fallback). Full suite: 542 pass,tsc --noEmitclean, packaging builds.Observed on a live OpenWA 0.12.1 host + Chatwoot v4.16.2: agent replies-to arrived on WhatsApp with empty/missing quote context; with this change wired in, the quoted id rides the envelope.