Critical:
- describe-video: add mkdir for MEDIA_CACHE_DIR before ffmpeg write
- telegram: check bot ID (not is_bot) for reply-to detection in groups
Important:
- telegram: check @mention in caption for media messages in groups
- hub: add .catch() to channelManager.startAll()
- describe-image: add 20MB file size check to prevent OOM
- async-agent: remove dead writeWithImages, refactor with enqueue()
- manager: lazy agent subscription via ensureSubscribed() to handle
late agent availability and agent replacement
Suggestions:
- telegram-format: escape quotes in link URLs to prevent HTML breakout
- transcribe: catch API errors and return null (match local fallback)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add isInsideTable() to BlockChunker: prevents breaking Markdown tables
in the middle (table rows lose header context when split across messages)
- Set Telegram chunkerConfig maxChars to 4000 (was default 2000; Telegram
API limit is 4096, leaving room for HTML formatting overhead)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add describe-image.ts: OpenAI Vision API (gpt-4o-mini) image description
- Add describe-video.ts: ffmpeg frame extraction + Vision API description
- Rewrite transcribe.ts: local whisper/whisper-cli → OpenAI API → null
- Update manager.ts routeMedia(): all media converted to text before agent
- Image: describeImage() → text (was: raw ImageContent via writeWithImages)
- Video: describeVideo() → text (was: file path info only)
- Audio: unchanged (but underlying transcribeAudio now tries local first)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move audio transcription from agent-driven (exec + local whisper) to
Manager-layer processing via OpenAI Whisper API. Voice messages are
now transcribed automatically before the agent sees them, so the
agent only receives text. Local whisper skill remains as fallback
when API key is not configured. Also changed default model from
turbo to base for faster first-time experience.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bundled skill that enables the agent to transcribe audio files using
OpenAI Whisper CLI. Uses anyBins requirement so the skill is only
visible when whisper is installed. Includes brew and uv install specs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add writeWithImages() to AsyncAgent for passing images directly to
the LLM via ImageContent. Extend Agent.run() to accept optional
images parameter. Update ChannelManager.routeIncoming() to download
media files and forward them: images as ImageContent to the LLM,
audio/video/document as file paths for agent-driven processing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add handlers for voice, audio, photo, video, and document messages.
Each handler emits a ChannelMessage with media attachment metadata.
Implement downloadMedia() to fetch files from Telegram API and save
to the local media cache directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ChannelMediaAttachment type with support for audio, image, video,
and document media types. Extend ChannelMessage with optional media
field and ChannelPlugin with optional downloadMedia method.
Add MEDIA_CACHE_DIR path for downloaded media files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forward voice messages and audio files to the agent as <media:audio>
placeholder text. In groups, only process voice/audio that replies to
the bot. Includes caption text if present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add sendTyping to ChannelOutboundAdapter (optional per platform)
- Implement typing lifecycle in ChannelManager (5s interval, cleanup on message_end/error/clear)
- Convert Markdown to Telegram HTML subset (bold, italic, code, links, blockquotes)
- Fallback to plain text on HTML parse errors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use setWindowOpenHandler to intercept window.open calls and route
them through shell.openExternal instead of navigating inside Electron.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
StreamPayload.event was missing the agent_error event type, causing
TypeScript errors in useGatewayChat and useLocalChat where the
comparison payload.event.type === "agent_error" had no type overlap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Desktop path already forwards agent_error to chat.setError() via
use-local-chat.ts, but the Web/Gateway path was missing this handling.
Add agent_error interception in the StreamAction branch so Web clients
render LLM errors the same way Desktop does.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the three independent message paths (Desktop IPC, Web
WebSocket, Channel Bot API) including send/receive flows, error
handling, lastRoute pattern, and event filtering comparison.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forward agent_error as passthrough event to renderer. Add
clearLastRoute() calls in hub:sendMessage and localChat:send
handlers so channel replies stop when desktop sends a message.
Handle agent_error in use-local-chat to show error UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add agent_error event type to MulticaEvent union so errors from
agent runs reach subscribe() consumers (Desktop IPC + Channel).
Make emitMulticaEvent public on Runner so AsyncAgent can emit errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace per-conversation agent creation with single Hub agent model.
Messages from channels are routed to the existing Hub agent via
agent.write(), and replies are sent back through the lastRoute context.
Desktop and Gateway paths call clearLastRoute() so channel replies
stop when the user switches input surface.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Handle 409 conflict (another bot instance running) with a clear error
message instead of an unhandled promise rejection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move channel configuration into the existing credentials.json5 under a
`channels` section, matching OpenClaw's single-config-file approach.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up channel system in Hub constructor and shutdown.
Add grammy dependency for Telegram bot support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ChannelManager orchestrates channel lifecycles and routes messages to per-conversation Agents.
Telegram plugin uses grammy for long polling with group @mention detection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces the extensible channel plugin architecture for messaging platform integrations.
- ChannelPlugin interface with config, gateway, and outbound adapters
- Plugin registry with register/get/list operations
- Config loader for ~/.super-multica/channels.json5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The closing fence regex was not checking for an empty info string,
allowing e.g. ```python to incorrectly close an open fence. Also
adds missing test for tool_execution_update passthrough.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Message aggregation should not be managed by the Hub. The aggregator
is a standalone utility for client adapters to use internally — the
Hub stays clean and only does event forwarding. Third-party messaging
integrations (Discord, Telegram) will use MessageAggregator in their
own adapter layer.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
After the chat refactoring moved state management to @multica/hooks,
the Zustand stores (useConnectionStore, useMessagesStore, useAutoConnect)
are no longer imported by any application code. This removes them along
with their unused dependencies (zustand, uuid, react).
- Delete connection-store.ts, messages.ts, use-auto-connect.ts
- Extract Message/ToolStatus types into types.ts (preserves UI imports)
- Remove saveConnection/loadConnection/clearConnection from connection.ts
- Drop zustand, uuid, react deps from package.json
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Return latest messages by default instead of oldest. Support paginated
loading of older messages when scrolling up via IntersectionObserver,
with scrollHeight compensation to preserve scroll position.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add enableAggregation/disableAggregation methods to Hub for per-agent
aggregation control. When enabled, streaming text deltas are buffered
and emitted as block-reply events instead of raw stream events. The
existing streaming mode remains the default for own clients.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
BlockChunker splits streaming text at natural boundaries (paragraph,
newline, sentence, word) with configurable min/max chars and markdown
fence-safe splitting. MessageAggregator consumes AgentEvents, buffers
text deltas, and emits complete BlockReply objects via callbacks.
Enables future third-party messaging integrations (Discord, Telegram)
that cannot consume raw streaming deltas.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use chatRef to hold stable reference to useChat return value, avoiding
stale closure issues and satisfying react-hooks/exhaustive-deps rule.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign each search request with HMAC-SHA256 using Hub ID, a per-request
nonce (UUIDv7), and unix timestamp. The signed reqId is sent in the
request body to prevent unauthorized API usage.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add LocalChat component using useLocalChat hook that communicates with
the Hub via IPC (no Gateway required). Fix streamId extraction to use
event.message.id matching Hub behavior. Fix history to return raw
AgentMessageItem[] instead of flattened strings. Add exec approval
forwarding over IPC. Use conditional rendering for LocalChat to prevent
event leaking from remote sessions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>