Add requestHeartbeatNow to PluginRuntime.system so extensions can
trigger an immediate heartbeat wake without importing internal modules.
This enables extensions to inject a system event and wake the agent
in one step — useful for inbound message handlers that use the
heartbeat model (e.g. agent-to-agent DMs via Nostr).
Changes:
- src/plugins/runtime/types.ts: add RequestHeartbeatNow type alias
and requestHeartbeatNow to PluginRuntime.system
- src/plugins/runtime/index.ts: import and wire requestHeartbeatNow
into createPluginRuntime()
* feat(hooks): add trigger and channelId to plugin hook agent context
Adds `trigger` and `channelId` fields to `PluginHookAgentContext` so
plugins can determine what initiated the agent run and which channel
it originated from, without session-key parsing or Redis bridging.
trigger values: "user", "heartbeat", "cron", "memory"
channelId values: "telegram", "discord", "whatsapp", etc.
Both fields are threaded through run.ts and attempt.ts hookCtx so all
hook phases receive them (before_model_resolve, before_prompt_build,
before_agent_start, llm_input, llm_output, agent_end).
channelId falls back from messageChannel to messageProvider when the
former is not set. followup-runner passes originatingChannel so queued
followup runs also carry channel context.
* docs(changelog): note hook context parity fix for #28623
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
## Overview
This PR enables external channel plugins (loaded via Plugin SDK) to access
advanced runtime features like AI response dispatching, which were previously
only available to built-in channels.
## Changes
### src/gateway/server-channels.ts
- Import PluginRuntime type
- Add optional channelRuntime parameter to ChannelManagerOptions
- Pass channelRuntime to channel startAccount calls via conditional spread
- Ensures backward compatibility (field is optional)
### src/gateway/server.impl.ts
- Import createPluginRuntime from plugins/runtime
- Create and pass channelRuntime to channel manager
### src/channels/plugins/types.adapters.ts
- Import PluginRuntime type
- Add comprehensive documentation for channelRuntime field
- Document available features, use cases, and examples
- Improve type safety (use imported PluginRuntime type vs inline import)
## Benefits
External channel plugins can now:
- Generate AI-powered responses using dispatchReplyWithBufferedBlockDispatcher
- Access routing, text processing, and session management utilities
- Use command authorization and group policy resolution
- Maintain feature parity with built-in channels
## Backward Compatibility
- channelRuntime field is optional in ChannelGatewayContext
- Conditional spread ensures it's only passed when explicitly provided
- Existing channels without channelRuntime support continue to work unchanged
- No breaking changes to channel plugin API
## Testing
- Email channel plugin successfully uses channelRuntime for AI responses
- All existing built-in channels (slack, discord, telegram, etc.) work unchanged
- Gateway loads and runs without errors when channelRuntime is provided
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>
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>