fix(api): build the bridge for a contact's first outgoing message - #403
Merged
Merged
Conversation
The outbound "new chat" flow inserts a `contacts` row and nothing else, and the channel-spine bridge is otherwise only ever written as a shadow of a legacy message. A contact that has never exchanged a message therefore has no conversation row, so under neutral write authority the send had nothing to write against and answered 404 - the first message to a new contact was impossible while every later one succeeded. Resolve the conversation through the bridge instead, building it when the contact does not have one yet. Unlike `shadowLinkedDeviceLegacyMutation` this is not best-effort: the send depends on the conversation, so an unresolvable bridge fails the request rather than being journalled. Every identity in the bridge is deterministic and every write upserts, so two first sends racing each other converge on the same conversation. The regression test drives the real route against a contact with no conversation: 404 before the change, 200 after, with the conversation and message written against the right channel account. Scheduled sends and the MCP send read the same bridge but tolerate a null conversation; neither was verified against this state and both are left for a follow-up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The first outgoing message to a brand-new contact fails with
404fromPOST /api/messages. Every later message to the same contact works, which is what made it read as a contact problem rather than a bridge one.The outbound "new chat" flow (
findOrCreateContactByPhone) inserts acontactsrow and nothing else. The channel-spine bridge — including theconversationsrow — is otherwise only ever written as a shadow of a legacy message write. So a contact that has never exchanged a message has no conversation, and under neutral write authoritysendChannelContactMessagelooked one up, found nothing, and returnednotFound(c, "Contact or JID").The fix
ensureChannelConversationId()resolves the conversation through the bridge, building it withshadowLinkedDeviceWorkflowwhen the contact has none.Deliberately not
shadowLinkedDeviceLegacyMutation: that one is best-effort and swallows failures into the reconciliation journal, which is right for a shadow of an already-authoritative legacy write and wrong here, where the send depends on the conversation existing. An unresolvable bridge fails the request instead.Every identity in the bridge is a deterministic UUID and every write upserts, so two first sends racing each other converge on the same conversation. Unchanged behaviour elsewhere: a non-WhatsApp contact (no jid / no connection) still resolves to nothing and still 404s, and an archived conversation still falls through to the existing
notFound(c, "Conversation").Verification
New
apps/api/src/routes/messages/first-neutral-send.integration.test.tsdrives the real route under neutral authority against a connected account and a contact with no conversation row:POST /api/messages→ 404, test failsAlso green:
send.integration.test.ts,neutral-whatsapp-send.integration.test.ts,first-chat-acknowledgment.integration.test.ts,scheduled.integration.test.ts, the full API unit suite (947 pass / 0 fail),tsc --noEmit, andgit diff --check.Follow-ups (not in this diff)
routes/messages/scheduled.ts) and the MCP send (routes/mcp/tools/write.ts) read the same bridge but tolerate a null conversation. Neither was verified against the bridgeless state.409, not a404. Unchanged here.