* feat(whatsapp): add debounceMs for batching rapid messages
Add a `debounceMs` configuration option to WhatsApp channel settings
that batches rapid consecutive messages from the same sender into a
single response. This prevents triggering separate agent runs for
each message when a user sends multiple short messages in quick
succession (e.g., "Hey!", "how are you?", "I was wondering...").
Changes:
- Add `debounceMs` config to WhatsAppConfig and WhatsAppAccountConfig
- Implement message buffering in `monitorWebInbox` with:
- Map-based buffer keyed by sender (DM) or chat ID (groups)
- Debounce timer that resets on each new message
- Message combination with newline separator
- Single message optimization (no modification if only one message)
- Wire `debounceMs` through account resolution and monitor tuning
- Add UI hints and schema documentation
Usage example:
{
"channels": {
"whatsapp": {
"debounceMs": 5000 // 5 second window
}
}
}
Default behavior: `debounceMs: 0` (disabled by default)
Verified: All existing tests pass (3204 tests), TypeScript compilation
succeeds with no errors.
Implemented with assistance from AI coding tools.
Closes#967
* chore: wip inbound debounce
* fix: debounce inbound messages across channels (#971) (thanks @juanpablodlc)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Adds support for template variables in `messages.responsePrefix` that
resolve dynamically at runtime with the actual model used (including
after fallback).
Supported variables (case-insensitive):
- {model} - short model name (e.g., "claude-opus-4-5", "gpt-4o")
- {modelFull} - full model identifier (e.g., "anthropic/claude-opus-4-5")
- {provider} - provider name (e.g., "anthropic", "openai")
- {thinkingLevel} or {think} - thinking level ("high", "low", "off")
- {identity.name} or {identityName} - agent identity name
Example: "[{model} | think:{thinkingLevel}]" → "[claude-opus-4-5 | think:high]"
Variables show the actual model used after fallback, not the intended
model. Unresolved variables remain as literal text.
Implementation:
- New module: src/auto-reply/reply/response-prefix-template.ts
- Template interpolation in normalize-reply.ts via context provider
- onModelSelected callback in agent-runner-execution.ts
- Updated all 6 provider message handlers (web, signal, discord,
telegram, slack, imessage)
- 27 unit tests covering all variables and edge cases
- Documentation in docs/gateway/configuration.md and JSDoc
Fixes#923
* fix(signal): handle reactions inside dataMessage.reaction
Signal reactions can arrive in two formats:
1. envelope.reactionMessage (already handled)
2. envelope.dataMessage.reaction (now handled)
The signal-cli SSE events use the second format, which was being
misinterpreted as a message with attachments, leading to 'broken
media / attachments' errors.
Changes:
- Add reaction property to SignalDataMessage type
- Check both envelope.reactionMessage and dataMessage.reaction
- Improve body content detection to properly identify reaction-only messages
- Add test for dataMessage.reaction format
* fix(signal): reaction notifications work when account is phone number
When reactionNotifications mode is 'own', notifications would never fire
because resolveSignalReactionTarget() returned a UUID but
shouldEmitSignalReactionNotification() compared it against the account
phone number, which never matched.
The fix:
- Add optional 'phone' field to SignalReactionTarget type
- Extract phone number first in resolveSignalReactionTarget(), include
it even when UUID is present
- In shouldEmitSignalReactionNotification() 'own' mode, check phone
match first before falling back to UUID comparison
This ensures reactions to your own messages are properly detected when
the Signal account is configured as a phone number and the reaction
event contains both targetAuthor (phone) and targetAuthorUuid.
* fix(signal): include phone in reaction target for own-mode matching
When targetAuthorUuid is present, also store targetAuthor phone number
in the reaction target. This allows own-mode reaction notifications to
match when comparing account phone against UUID-based targets.
Adds `agent.humanDelay` config option to create natural rhythm between
streamed message bubbles. When enabled, introduces a random delay
(default 800-2500ms) between block replies, making multi-message
responses feel more like natural human texting.
Config example:
```json
{
"agent": {
"blockStreamingDefault": "on",
"humanDelay": {
"enabled": true,
"minMs": 800,
"maxMs": 2500
}
}
}
```
- First message sends immediately
- Subsequent messages wait a random delay before sending
- Works with iMessage, Signal, and Discord providers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SignalReactionMessage type with emoji, targetAuthor, timestamp
- Handle reaction messages in monitor (log and skip for now)
- Prevents reactions from showing as unknown media
Users without a visible phone number (like some Signal users)
were being silently dropped. Now falls back to UUID for sender ID.
- Add sourceUuid to SignalEnvelope type
- Use sourceUuid when sourceNumber is not available
- Only check against bot account when sourceNumber exists (avoid UUID comparison issues)
When OriginatingChannel matches Surface (same provider), use normal
dispatcher. Only route via routeReply() when they differ, ensuring
cross-provider messages (e.g., Telegram queued while Slack active)
get routed back to their origin.
Implement reply routing based on OriginatingChannel/OriginatingTo fields.
This ensures replies go back to the provider where the message originated
instead of using the session's lastChannel.
Changes:
- Add OriginatingChannel/OriginatingTo fields to MsgContext (templating.ts)
- Add originatingChannel/originatingTo fields to FollowupRun (queue.ts)
- Create route-reply.ts with provider-agnostic router
- Update all providers (Telegram, Slack, Discord, Signal, iMessage)
to pass originating channel info
- Update reply.ts to pass originating channel to followupRun
- Update followup-runner.ts to use route-reply for originating channels
This addresses the issue where messages from one provider (e.g., Slack)
would receive replies on a different provider (e.g., Telegram) because
the queue used the last active dispatcher instead of the originating one.