Skip to content

[pull] main from rmyndharis:main - #115

Merged
pull[bot] merged 6 commits into
m7fz7:mainfrom
rmyndharis:main
Aug 20, 2026
Merged

[pull] main from rmyndharis:main#115
pull[bot] merged 6 commits into
m7fz7:mainfrom
rmyndharis:main

Conversation

@pull

@pull pull Bot commented Aug 20, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

Two defects in the outbound delivery record, both reachable on the default
deployment.

The reconciler decided a replay had worked by whether `redeliver()` threw,
and it cannot: every failing path inside it dead-letters, hooks and logs in
place, so the call resolves either way. A replay to a receiver that was
still down was therefore retired as 'dispatched' with its payload nulled on
the very first sweep, and `findStale` never sees it again. The documented
WEBHOOK_RECONCILE_MAX_ATTEMPTS budget was unreachable, the catch block was
dead code whose comment claimed the opposite, and stats counted the event
as replayed. Delivery now returns an outcome, the way the inbound twin
already does, and a failed replay stays pending until the budget is spent.

The import never cleared `webhook_outbox_events`. It carries no foreign key
to sessions, so the sessions DELETE did not reach it, and
UNIQUE(webhookId, idempotencyKey) then collided on every overlapping row
until the all-or-nothing gate rolled the entire import back. Restoring a
backup onto the instance that produced it is the rollback flow, so this
broke recovery rather than a corner of it.

The reconciler spec mocked a rejected promise, a shape the real collaborator
cannot produce, so it green-lit a budget it never exercised; it now mocks
the outcome. The round-trip spec cleared each table before importing, which
is why the collision never surfaced; the new case deliberately does not.
Three defects in the caller-named read receipt, all reachable from the
public route.

`@IsOptional` skips every validator when the value is null as well as
undefined, so `"messageIds": null` passed validation, reached the Baileys
adapter and was dereferenced there: a 500 on a malformed body that should
have been a 400. The DTO now validates a present-but-null value, and the
adapter treats null as absent so an internal caller cannot reproduce it
either. The published schema gains `minItems`, which it needed anyway: it
advertised an empty array the server refuses.

A stored key carries its own remoteJid, and it was used unchecked. An id
belonging to a different chat in the same session therefore sent the
receipt to that chat while the route answered success for the one named in
the path, leaving the caller's own chat unread. Stored keys are now used
only when they resolve to the addressed chat, folded through toEngineJid so
the @c.us and @s.whatsapp.net spellings still match; anything else falls
back to the synthesised key for the addressed chat, which is what every id
ran on before the store was consulted.

The Go client tagged `messageIds` `omitempty` on a plain slice, so an empty
list was dropped from the body and the server read it as "acknowledge the
newest message", the exact collapse the empty-array refusal exists to
prevent. The field is a pointer, so absent and empty are distinct on the
wire. The type has never shipped, so nothing external moves.

The breaking-change note also understated its own scope: markRead is
source-breaking on Go and Java alongside subscribePresence, and typed
Python callers fail mypy on both. Verified against the published 0.4.0
signatures rather than assumed; JavaScript is structural and unaffected.
Both scripts resolve every other data path through openwa_resolve and then
hardcoded the plugin state as $OPENWA_DATA_DIR/plugins. With the knob set,
the archive carried neither the registry nor any plugin's persisted
ctx.storage, and a restore put nothing back. Silent in both directions: an
absent source directory simply produces no plugin-state entry, so the
backup still exits 0 and only a restore reveals what is missing.

The local variable shares its name with the knob, which is what made the
line read as if it already honoured it. The resolved root now has its own
name, because the knob points at the ROOT and the state sits at
<root>/plugins.

The knob's note told operators to move registry.json. That leaves every
plugin enabled with its state gone, since each plugin's ctx.storage sits
beside the registry as <plugin id>/key-*.json; it now names the whole tree.

Smoke case (g) covers both legs. Reverting either script alone fails it
with that script's own message.
…d-restore

fix(webhook): report the delivery outcome and clear the outbox on import
…scope

fix(session): refuse a null id list and keep read receipts in their chat
fix(scripts): resolve PLUGIN_STATE_DIR in backup and restore
@pull pull Bot locked and limited conversation to collaborators Aug 20, 2026
@pull pull Bot added the ⤵️ pull label Aug 20, 2026
@pull
pull Bot merged commit 84d2132 into m7fz7:main Aug 20, 2026
14 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant