Matrix gains emoji-list backed by MSC2545 image packs: room state
im.ponies.room_emotes (any state key) plus the personal
im.ponies.user_emotes account-data pack, sticker-only entries excluded,
identifiers are literal shortcodes (Matrix reactions send the annotation
key as-is; the mxc URL is exposed alongside). Discord emoji-list results
now ride the lifecycle-owned bounded entity cache, invalidated by
GuildEmojisUpdate; the non-privileged GuildExpressions intent is added so
that dispatch actually arrives. Google Chat custom-emoji listing is
documented as unavailable: customEmojis.list requires user auth while the
plugin is a chat.bot service-account channel.
* feat(channels): custom emoji discovery via emoji-list across Discord, Slack, Telegram
Make custom emojis discoverable by the agent. The message tool's emoji
param now documents custom-emoji syntax per channel (gate-aware, only
naming emoji-list when the action is actually advertised). Discord
emoji-list defaults guildId from the current conversation and returns
reaction-ready { name, identifier, animated? } entries; Slack returns
normalized shortcodes with aliasOf. Telegram gains emoji-list backed by
one canonical allowed-reactions owner (getChat available_reactions,
custom_emoji entries preserved), numeric custom-emoji reactions, and
replaces the dead 'reaction disallow list' error advice with a bounded
sample of the chat's allowed reactions.
* test(channels): expect telegram emoji-list provider-owned read gate in plugin shape contract
* test(telegram): prove emoji-list authority chain via mock-gateway e2e
Ephemeral gateway + mock Bot API + mock OpenAI provider: current-chat
emoji-list returns normalized standard and custom_emoji identifiers with
exactly one getChat call; a delegated cross-chat request is rejected with
the conversation-binding error and zero Bot API requests reference the
foreign chat.
* fix(discord): survive malformed gateway HELLO frames
The HELLO branch dereferenced payload.d through a bare cast and replaced
only null/undefined with the 45s default, so a { op: 10, d: null } frame
threw a TypeError out of the socket message listener (no try/catch), and
a zero, negative, sub-second, or out-of-range interval reached setTimeout
unchanged, scheduling the first heartbeat immediately.
Normalize the HELLO body with the canonical record guard and accept the
interval only inside the timer-safe range, matching the QQ gateway. The
cast is removed rather than guarded.
* fix(discord): honor any heartbeat interval the gateway can schedule
Review of the first revision pointed out that the 1000 ms lower bound was
an interoperability break: a compatible gateway negotiating, say, 500 ms
would have had its interval replaced by the 45s default and then been
disconnected as unresponsive. Discord documents no lower bound.
Drop the bound to a positive timer-safe integer, so shape validation and
the timer ceiling stay while every interval this client can actually
schedule is honored. Regression rows pin 500 ms and 1 ms.
* fix(discord): reject upload-file buffer attachments with send guidance
The flat message tool schema advertises buffer for every action, so
callers reasonably infer upload-file + buffer is valid on Discord. The
Discord handler only reads filePath/path/media for upload-file and
failed with an error that never mentioned the supported path.
Scope the buffer schema description to action="send" and make Discord
upload-file reject a buffer-only call with guidance to use send instead
of duplicating the send buffer pipeline under a second action.
* review: keep the shared buffer description channel-neutral
iMessage's upload-file deliberately consumes a buffer, so scoping the
shared schema text to send would teach models away from a supported
path. Keep the guidance owner-local: Discord's rejection carries it.
* fix(discord,msteams): upload-file silently drops a caption
Both channels accept `media` as an alias for the uploaded file, so a
send-shaped call reaches `upload-file` carrying its text in `caption`.
Discord read only `message`/`content` and MS Teams read only
`text`/`content`/`message`, so that text was dropped and the file landed
with no message at all.
Accept `caption` as the lowest-precedence alias on both channels, matching
the WhatsApp and Slack upload paths. An explicitly empty higher-precedence
value still wins, so existing callers keep their behavior. The MS Teams
reader is scoped to `upload-file` because the other actions carry their own
text field.
* refactor: narrow this PR to the Discord channel
Microsoft Teams keeps the same defect and the same fix shape, but a Teams
after-fix delivery or activity trace is not reachable without a live tenant,
so that half is deferred to its own change rather than blocking the Discord
repair that already has a production-boundary trace.
* fix(channels): preserve gateway context for inbound turns
* fix(plugin-sdk): type bound channel reply dispatcher
* fix(channels): carry bound reply dispatchers
* fix(channels): keep reply carrier internal
* fix(auto-reply): consolidate dispatcher type imports
* fix(channels): keep reply dispatch typing internal
* fix(channels): derive inbound reply dispatcher types from turn plan
Keep ChannelRuntimeSurface identical to main: adapters read the bound
dispatchReplyFromConfig through the existing PluginRuntime["channel"]
wiring type and derive its type from the public ChannelInboundTurnPlan
contract, so the compatibility surface no longer grows a Gateway-bound
reply member.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(channels): hoist bound channel runtime casts for assertion ratchet
The assertion-safety ratchet collects SAFETY comments with a plain
ts.Scanner pass, which desyncs at the first template literal with a
substitution — comments after that point in a file are invisible, so
deep casts in monitor files cannot be SAFETY-covered. Hoist one shared
cast per call-site scope (absorbing the pre-existing buildContext
casts) and retype Discord's internal channelRuntime chain as
PluginRuntime["channel"] so dispatch reads need no assertion.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(channels): verify gateway dispatcher ownership
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* test(codex): assert sequenced node process notifications
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* docs(changelog): preserve release-owned root changelog
Channel Gateway tools preserve the owning dispatcher for Telegram, Discord, iMessage, Signal, and WhatsApp so terminal tools remain available. Thanks @VACInc.
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* test(codex): avoid shadowing process notification bindings
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
---------
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Centralize untrusted audio transcript framing across channel plugins and keep Discord voice transcripts out of typed command provenance.
Co-authored-by: xialonglee <li.xialong@xydigit.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(discord): auto-join occupied voice rooms
Add opt-in voice.autoJoin[].whenOccupied residency so Discord voice bots join for the first human and leave when the room becomes empty while preserving existing always-on, manual, transcript, and follow-user behavior.\n\nCloses #125973
* test(discord): isolate process runtime mocks
Use stable hoisted runtime-env mocks so isolate=false Discord test ordering cannot turn sleepWithAbort back into an unmocked function.
* fix(discord): defer unknown voice occupancy
Treat memberless voice states as unresolved instead of human so bot-only rooms cannot trigger occupancy-managed auto-join. Add cache-to-listener and manager regressions.\n\nCloses #125973
* test: isolate shared module mocks
Replace ineffective non-isolated module spies with stable hoisted mocks and a child-process SQLite connection-reuse probe so gateway and Discord shards are order-independent.
* test(gateway): inline connection reuse probe
Keep the child-process SQLite ownership probe in its owning Vitest file so Knip sees the full test surface without weakening process isolation or the original order regression.