Merge main's run mutex + soft error return with branch's refreshAuthState(),
keeping getApiKey defensive throw as defense-in-depth.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Style is now solely managed by the agent editing soul.md directly,
removing the need for UI controls, IPC handlers, and typed constants.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the agent fails due to missing API key, the error banner now
shows a "Configure" button that opens the same ApiKeyDialog (or
OAuthDialog) used on the home page. After successful configuration
the error clears and the user can immediately start chatting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getApiKey errors thrown inside PiAgentCore's internal async context
result in UnhandledPromiseRejection instead of propagating to the
caller. Return a graceful error early so AsyncAgent can emit it
through the subscriber mechanism to the UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix double useChannels() instantiation: call once in ChannelsPage,
pass as props to TelegramCard
- Mask bot tokens in channels:getConfig before sending to renderer
- Add input validation (isValidId, token length) on all IPC handlers
- Fix stopAccount() to clean up typingTimer, lastRoute, aggregator,
and debouncer when stopping the account they belong to
- Add try/catch to stopChannel/startChannel in useChannels hook
- Consistent return type { ok, error? } on channels:stop handler
- Add tooltip hint on disabled Remove button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add IPC handlers, preload API, useChannels hook, and Channels page UI.
Users can save/remove Telegram bot tokens and start/stop bots directly
from the desktop app with immediate effect and persistence across restarts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add setChannelAccountConfig/removeChannelAccountConfig to CredentialManager
for persisting channel tokens. Make ChannelManager.startAccount public and
add stopAccount for individual account lifecycle control via IPC.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- InboundDebouncer: batches rapid-fire messages from the same conversation
into a single agent.write() call (500ms idle, 2s hard cap)
- ACK reactions: add 👀 emoji on message receipt, remove on completion
(addReaction/removeReaction on ChannelOutboundAdapter interface)
- Grammy sequentialize middleware: ensures same-chat updates are processed
in order, preventing race conditions on shared state
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
When the agent encounters an error (e.g. no API key configured),
the Chat UI now shows an error banner instead of silently hanging.
The user can still type and retry after fixing their configuration.
- Add AgentErrorEvent to SDK stream types
- Forward agent_error events through IPC to renderer
- Handle error events in useLocalChat hook
- Keep chat input enabled for AGENT_ERROR (retriable)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PiAgentCore was created with an empty object when no API key was
initially configured. This broke dynamic provider switching because
setProvider() updated currentApiKey but PiAgentCore had no getApiKey
callback to read it. Always provide the callback so it dynamically
reads the current key.
Also adds AgentErrorEvent to MulticaEvent and emits it from
AsyncAgent.write() catch handlers so errors flow through the
subscriber mechanism to IPC listeners.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each tool call added an abort listener to the shared agent signal
without cleanup, exceeding the default 10-listener limit after 11+
exec calls. Fix by using { once: true } and removing the listener
on child process close (exec) to prevent accumulation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of keeping announced runs in the registry for 60 minutes
(archive sweeper), delete them right after findings are delivered
to the parent. This prevents stale completed tasks from appearing
in sessions_list on subsequent parent turns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After child subagents complete, the coalesced announcement runs as an
internal turn which rolls back all messages from the parent's in-memory
context. This causes the parent LLM to lose findings in subsequent turns.
Add persistResponse option to writeInternal that re-injects the LLM's
summary as a non-internal assistant message after the internal run
completes. The internal prompt stays hidden while the summary persists
in both memory and session JSONL for future turns.
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>