Take the safe, tested subset from #32367:\n- per-channel startup connect grace in health monitor\n- tool-context channel-provider fallback for message actions\n\nCo-authored-by: Munem Hashmi <munem.hashmi@gmail.com>
- Pass gfm:true + breaks:true explicitly to marked.parse() so table
support is guaranteed even if global setOptions() is bypassed or
reset by a future refactor (defense-in-depth)
- Add display:block + overflow-x:auto to .chat-text table so wide
multi-column tables scroll horizontally instead of being clipped
by the parent overflow-x:hidden chat container
- Add regression tests for GFM table rendering in markdown.test.ts
* fix(feishu): skip typing indicator keepalive re-adds to prevent notification spam
The typing keepalive loop calls addTypingIndicator() every 3 seconds,
which creates a new messageReaction.create API call each time. Feishu
treats each re-add as a new reaction event and fires a push notification,
causing users to receive repeated notifications while waiting for a
response.
Unlike Telegram/Discord where typing status expires after a few seconds,
Feishu reactions persist until explicitly removed. Skip the keepalive
re-add when a reaction already exists (reactionId is set) since there
is no need to refresh it.
Closes#28660
* Changelog: note Feishu typing keepalive suppression
---------
Co-authored-by: yuxh1996 <yuxh1996@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
When abortSignal is already aborted at lifecycle start, onAbort() fires
synchronously and pushes connected: false. Without a lifecycleStopping
guard, the subsequent gateway.isConnected check could push a spurious
connected: true, contradicting the shutdown.
Adds !lifecycleStopping to the isConnected guard and a test verifying
no connected: true is emitted when the signal is pre-aborted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the Discord gateway completes its READY handshake before
`runDiscordGatewayLifecycle` registers its debug event listener, the
initial "WebSocket connection opened" event is missed. This leaves
`connected` as undefined in the channel runtime, causing the health
monitor to treat the channel as "stuck" and restart it every check
cycle.
Check `gateway.isConnected` immediately after registering the debug
listener and push the initial connected status if the gateway is
already connected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cron delivery path short-circuits with an error when `toCandidate` is
falsy (line 151), before reaching `resolveOutboundTarget()` which provides
the `plugin.config.resolveDefaultTo()` fallback. The direct send path in
`targets.ts` already uses this fallback correctly.
Remove the early `!toCandidate` exit so that `resolveOutboundTarget()`
can attempt the plugin-provided default. Guard the WhatsApp allowFrom
override against falsy `toCandidate` to maintain existing behavior when
a target IS resolved.
Fixes#32355
Signed-off-by: HCL <chenglunhu@gmail.com>
Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.
Introduced in #10686, every new channel message got its own session key (agent:...🧵<messageTs>), breaking conversation continuity.
Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.
Fixes#32285
The sticker code path called ctx.getFile() directly without retry,
unlike the non-sticker media path which uses resolveTelegramFileWithRetry
(3 attempts with jitter). This made sticker downloads vulnerable to
transient Telegram API failures, particularly in group topics where
file availability can be delayed.
Refs #32326
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The `forceFlushTranscriptBytes` path (introduced in d729ab21) bypasses the
`memoryFlushCompactionCount` guard that prevents repeated flushes within the
same compaction cycle. Once the session transcript exceeds 2 MB, memory flush
fires on every single message — even when token count is well under the
compaction threshold.
Extract `hasAlreadyFlushedForCurrentCompaction()` from the inline guard in
`shouldRunMemoryFlush` and apply it to both the token-based and the
transcript-size trigger paths.
Fixes#32317
Signed-off-by: HCL <chenglunhu@gmail.com>
Fixes#32293: Discord voice message plays at ~0.5x speed with 24kHz TTS source
When TTS providers (like mlx-audio Qwen3-TTS) output audioHz,
Discord voice at 24k messages play at half speed because Discord expects 48kHz.
This fix adds explicit sample rate conversion to 48kHz in the ensureOggOpus
function, ensuring voice messages always play at correct speed regardless
of the input audio's sample rate.
Co-authored-by: Kevin Shenghui <shenghuikevin@gmail.com>
- Remove vi.hoisted() wrapper from exported mock in shared module
(Vitest cannot export hoisted variables)
- Inline vi.hoisted + vi.mock in startup test so Vitest's per-file
hoisting registers mocks before production imports
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When gateway.restart is triggered with a reason but no separate note,
the payload sets both message and stats.reason to the same text.
formatRestartSentinelMessage() then emits both the message line and a
redundant 'Reason: <same text>' line, doubling the restart reason in
the notification delivered to the agent session.
Skip the 'Reason:' line when stats.reason matches the already-emitted
message text. Add regression tests for both duplicate and distinct
reason scenarios.