* fix: truncate skill command descriptions to 100 chars for Discord
Discord slash commands have a 100 character limit for descriptions.
Skill descriptions were not being truncated, causing command registration
to fail with an empty error from the Discord API.
* style: format
* style: format
Previously, when block streaming was disabled (the default), text generated
between tool calls would only appear after all tools completed. This was
because onBlockReply wasn't passed to the subscription when block streaming
was off, so flushBlockReplyBuffer() before tool execution did nothing.
Now onBlockReply is always passed, and when block streaming is disabled,
block replies are sent directly during tool flush. Directly sent payloads
are tracked to avoid duplicates in final payloads.
Also fixes a race condition where tool summaries could be emitted before
the typing indicator started by awaiting onAgentEvent in tool handlers.
* 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>
* fix(model-picker): list each provider/model combo separately
Previously, /model grouped models by name and showed all providers
that offer the same model (e.g. 'claude-sonnet-4-5 — anthropic, google-antigravity').
This was confusing because:
1. Users couldn't tell which provider would be used when selecting by number
2. The display implied choice between providers but selection was automatic
Now each provider/model combination is listed separately so users
can explicitly select the exact provider they want.
- Remove model grouping in buildModelPickerItems
- Display format changed from 'model — providers' to 'provider/model'
- pickProviderForModel now returns the single provider directly
- Updated tests to reflect new behavior
* fix: simplify model picker entries (#970) (thanks @mcinteerj)
---------
Co-authored-by: Keith the Silly Goose <keith@42bolton.macnet.nz>
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
- Added src/utils/provider-utils.ts to track reasoning provider logic
- Updated isReasoningTagProvider to loosely match 'google-antigravity' (fixes sub-models)
- Enabled enforceFinalTag in reply.ts when provider matches
- Verified <final> tag stripping logic in pi-embedded-subscribe.ts
- Updated pi-embedded-runner to use consistent provider check for prompt hints
The nullish coalescing operator (??) only skips null/undefined, not
empty strings. For direct WhatsApp messages, ctx.SenderId was an empty
string, causing senderRaw to be "" instead of falling through to the
valid ctx.SenderE164 value.
This caused commands like /status to be rejected with "unauthorized
sender" for self-chat WhatsApp messages.
Tested: Verified /status command now works correctly for self-chat
WhatsApp messages after the fix.
When block streaming is enabled with verbose=off, tool blocks are hidden
but their boundary information was lost. Text segments before and after
tool execution would get coalesced into a single message because the
coalescer had no signal that a tool had executed between them.
This adds an onBlockReplyFlush callback that fires on tool_execution_start,
allowing the block reply pipeline to flush pending text before the tool
runs. This preserves natural message boundaries even when tools are hidden.
Fixes the issue where:
text → [hidden tool] → text → rendered as one merged message
Now correctly renders as:
text → [hidden tool] → text → two separate messages
Co-diagnosed-by: Krill (Discord assistant)