Skip to content

[pull] main from rmyndharis:main - #114

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

[pull] main from rmyndharis:main#114
pull[bot] merged 15 commits into
m7fz7:mainfrom
rmyndharis:main

Conversation

@pull

@pull pull Bot commented Aug 19, 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 : )

m7fz7 and others added 15 commits August 19, 2026 13:47
Baileys acknowledges individual messages, not chats, so POST
/sessions/{id}/chats/read could only mark the newest message the engine
still held in memory. A burst of inbound messages left everything but the
last one unread for good, and a session that restarted since the message
arrived had nothing to acknowledge at all, answering false under a 200.

The request body now takes an optional messageIds array. A caller that
persists inbound message IDs names exactly what to acknowledge and is
subject to neither case; omitting it keeps the previous last-message
behaviour. The list is capped at 100 so one request cannot hand the engine
an unbounded key list to round-trip.

whatsapp-web.js ignores the field: its sendSeen is chat-level and already
marks every message in the chat.

Group chats are only partly served: the keys built here carry remoteJid and
id but no participant, which Baileys aggregates receipts by, so a supplied
id in a group is acknowledged without the sender it belongs to. Direct
chats are unaffected, as is the last-message fallback in both, which
carries the stored key whole.
Address review on the messageIds PR.

A synthesised receipt key carried no participant, so a group receipt named
no sender and WhatsApp could not attribute it while the API answered
success: true. Its hardcoded fromMe was also wrong for an outbound id, and
its jid was whichever dialect the caller happened to send. Each supplied id
is now resolved through the message store and the stored key is used whole.
Ids the store has never seen - history backfill is emitted but not persisted
- keep the synthesised key, which is what the 1:1 case ran on before.

The store gains a batched getMessages: the receipt path resolves up to a
hundred ids per request, and a findOne apiece was a hundred sequential
round trips.

POST /chats/unread gets its own MarkChatUnreadDto instead of sharing
MarkChatReadDto. Sharing published messageIds on a route that discards it.
The JS SDK splits the same way: MarkChatReadRequest for markRead, with
MarkChatRequest left to markUnread and subscribePresence, which is served
by a chatId-only DTO and would have taken a 400 on the extra field under
forbidNonWhitelisted. check-contract-shapes maps both pairs, taking the
JavaScript floor from 78 to 79.

An empty messageIds array is refused rather than read as "acknowledge the
newest message", so a caller that computed an empty unread set no longer
acknowledges a message it never named. The cap is hoisted to
MARK_READ_MESSAGE_IDS_MAX and published as maxItems, since @ArrayMaxSize is
runtime-only and the schema advertised an unbounded array. Entries are
matched against a non-whitespace token, because @isnotempty accepted '  '.

The send-seen spec stubs the real @c.us to @s.whatsapp.net fold instead of
an identity function, which had let the whole suite pass with the
toEngineJid call deleted. The DTO spec covers the four new validators, and
SessionMarkChatRead forwards the list to MCP callers.
Rebasing on main brought the shape gate's request-body coverage to the
Python, Go and Java clients, each of which maps MarkChatRequest to
MarkChatReadDto. That pair now fails the same way the JavaScript one did:
the contract carries messageIds and the hand-written type does not.

Each client gets a MarkChatReadRequest carrying the optional list, with
MarkChatRequest left to markUnread and subscribePresence, which take the
chat id alone. The gate maps both pairs for all four clients and each
coverage floor rises by one.
supertest starts a listener per request with `listen(0)` and no host, which
binds the wildcard address, and then dials 127.0.0.1 whatever it bound. macOS
hands a wildcard listener an ephemeral port that another process already holds
on 127.0.0.1 specifically, permits the overlapping bind, and routes the
loopback connection to the more specific holder. That process answers, so an
assertion reads a status the app has no route for. Measured here: 40 of 40
such ports went to a wildcard listener, and a captured run took a 501 from a
desktop helper holding 127.0.0.1:49657 on a machine with 29 loopback-specific
listeners. It surfaced as a failure on a different test each run, which is why
it read as flakiness and why redirecting state never moved it.

Listening on 127.0.0.1 while the app initialises makes supertest reuse that
server instead of opening one, and takes the lane from 230 listeners a run to
27. The host cannot simply be added to supertest's own call: a host argument
routes the bind through dns.lookup, so address() is still null when supertest
reads it synchronously.

listen() initialises first and then binds the same server, so it hands the
port taken during init back before binding what it was asked for. A bind
failure is settled from the 'error' event, which otherwise leaves init pending
and throws detached from the boot that caused it.

Linux refuses the overlapping bind and its allocator skips held ports, so CI
was not exposed; the change is inert there.
test(e2e): put each suite's server on a loopback port
External contributions carry "Thanks @handle." on their CHANGELOG entry;
this one was missing it.
feat(session): let callers name which messages a read receipt covers
The REST body rejects a whitespace-only entry in `messageIds`, but
`SessionMarkChatRead` declared its own list and copied only the count
bound. `invokeTool` parses the tool's schema and calls the service
directly, so the DTO never applies on that path and `['  ']` reached the
engine as a receipt key.

The pattern and its message move into exported constants next to the
existing cap, and the tool holds them rather than restating the rule.
Restating it is how the two surfaces drifted in the first place.
Entries under `[Unreleased]` had grown past anything the released
sections carry: median 44 words against 34, and a longest of 139 against
a historical maximum of 80. They read as explanations rather than
release notes.

Rewritten to the same distribution (median 39, longest 57), keeping what
a reader acts on and dropping the reasoning. Contributor credit and the
section headings are unchanged.
fix(mcp): apply the message-id format rule on the agent tool
`Install sqlite3` held the scripts-smoke job open for over an hour on two
consecutive main runs while every other job had already gone green, so
neither run ever reported and main went without a signal. The step had no
timeout, so a stalled apt-get would have burned the six-hour job default
before failing.

Both apt steps now carry a five-minute timeout and skip the install when
the runner image already ships the tool, which is the common case. The
shellcheck step runs its own apt-get update rather than inheriting one
from the step above, which no longer always runs. A failing install still
fails the step: the short-circuit only covers the tool already being
present.

release.yml carries the same job and had the same two unbounded steps, so
it is fixed alongside; a release cut could have stalled the same way.
`SubscribePresenceDto` had no contract-shape coverage at all. Four clients
declared one `MarkChatRequest` for both `subscribePresence` and
`markUnread`, and the gate mapped that type to `MarkChatUnreadDto`, so the
pair being compared was not the pair that exists. The two DTOs are
identical today, which is the only reason nothing failed; a field added to
either would have gone unnoticed on every typed client.

Each of the four typed clients now declares `SubscribePresenceRequest`,
the gate maps it to `SubscribePresenceDto`, and the per-client coverage
floors rise by one so the pair cannot be dropped again in silence.
Comparisons go from 320 to 324.

BREAKING for the Go and Java clients: `SubscribePresence` takes the new
type. The wire body is unchanged.
…body

fix(sdk): give subscribe-presence its own request type
ci: bound the apt steps so a slow mirror cannot hang a run
@pull pull Bot locked and limited conversation to collaborators Aug 19, 2026
@pull pull Bot added the ⤵️ pull label Aug 19, 2026
@pull
pull Bot merged commit daa7b93 into m7fz7:main Aug 19, 2026
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.

2 participants