Adds sanitization to extractAssistantText in sessions-helpers.ts to
prevent tool call text from leaking to users. Previously, messages
retrieved from chat history via sessions-helpers.ts could expose:
- Minimax XML tool calls (<invoke>...</invoke>)
- Downgraded tool call markers ([Tool Call: name (ID: ...)])
- Thinking tags (<think>...</think>)
This fix:
- Exports the stripping functions from pi-embedded-utils.ts
- Adds a new sanitizeTextContent helper in sessions-helpers.ts
- Updates extractAssistantText to sanitize before returning
- Updates extractMessageText in commands-subagents.ts to sanitize
Fixes#1269
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ui): allow relative URLs in avatar validation
The isAvatarUrl check only accepted http://, https://, or data: URLs,
but the /avatar/{agentId} endpoint returns relative paths like /avatar/main.
This caused local file avatars to display as text instead of images.
Fixes avatar display for locally configured avatar files.
* fix(gateway): resolve local avatars to URL in HTML injection and RPC
The frontend fix alone wasn't enough because:
1. serveIndexHtml() was injecting the raw avatar filename into HTML
2. agent.identity.get RPC was returning raw filename, overwriting the
HTML-injected value
Now both paths resolve local file avatars (*.png, *.jpg, etc.) to the
/avatar/{agentId} endpoint URL.
* feat(compaction): add adaptive chunk sizing and progressive fallback
- Add computeAdaptiveChunkRatio() to reduce chunk size for large messages
- Add isOversizedForSummary() to detect messages too large to summarize
- Add summarizeWithFallback() with progressive fallback:
- Tries full summarization first
- Falls back to partial summarization excluding oversized messages
- Notes oversized messages in the summary output
- Add SAFETY_MARGIN (1.2x) buffer for token estimation inaccuracy
- Reduce MIN_CHUNK_RATIO to 0.15 for very large messages
This prevents compaction failures when conversations contain
unusually large tool outputs or responses that exceed the
summarization model's context window.
* feat(ui): add compaction indicator and improve event error handling
Compaction indicator:
- Add CompactionStatus type and handleCompactionEvent() in app-tool-stream.ts
- Show '🧹 Compacting context...' toast while active (with pulse animation)
- Show '🧹 Context compacted' briefly after completion
- Auto-clear toast after 5 seconds
- Add CSS styles for .callout.info, .callout.success, .compaction-indicator
Error handling improvements:
- Wrap onEvent callback in try/catch in gateway.ts to prevent errors
from breaking the WebSocket message handler
- Wrap handleGatewayEvent in try/catch with console.error logging
to isolate errors and make them visible in devtools
These changes address UI freezes during heavy agent activity by:
1. Showing users when compaction is happening
2. Preventing uncaught errors from silently breaking the event loop
* fix(control-ui): add agentId to DEFAULT_ASSISTANT_IDENTITY
TypeScript inferred the union type without agentId when falling back to
DEFAULT_ASSISTANT_IDENTITY, causing build errors at control-ui.ts:222-223.
Adds support for separate replyToMode settings for DMs vs channels:
- Add channels.slack.dm.replyToMode for DM-specific threading
- Keep channels.slack.replyToMode as default for channels
- Add resolveSlackReplyToMode helper to centralize logic
- Pass chatType through threading resolution chain
Usage:
```json5
{
channels: {
slack: {
replyToMode: "off", // channels
dm: {
replyToMode: "all" // DMs always thread
}
}
}
}
```
When dm.replyToMode is set, DMs use that mode; channels use the
top-level replyToMode. Backward compatible when not configured.
When replying to a Slack thread, files attached to the root message were
not being fetched. The existing `resolveSlackThreadStarter()` fetched the
root message text via `conversations.replies` but ignored the `files[]`
array in the response.
Changes:
- Add `files` to `SlackThreadStarter` type and extract from API response
- Download thread starter files when the reply message has no attachments
- Add verbose log for thread starter file hydration
Fixes issue where asking about a PDF in a thread reply would fail because
the model never received the file content from the root message.
- Add regex caching to avoid creating new RegExp objects on each render
- Optimize smartFilter to use single array with tier-based scoring
- Replace non-existent fuzzyFilter import with local fuzzyFilterLower
- Reduces from 4 array allocations and 4 sorts to 1 array and 1 sort
Fixes pre-existing bug where fuzzyFilter was imported from pi-tui but not exported.
Auth profiles in cooldown (due to rate limiting) were being attempted,
causing unnecessary retries and delays. This fix ensures:
1. Initial profile selection skips profiles in cooldown
2. Profile rotation (after failures) skips cooldown profiles
3. Clear error message when all profiles are unavailable
Tests added:
- Skips profiles in cooldown during initial selection
- Skips profiles in cooldown when rotating after failure
Fixes#1316
The progress spinner was being shown for each gateway RPC call during
log tailing, causing repeated spinner frames (◇ │) to appear every
polling interval.
Add a `progress` option to `callGatewayFromCli` and disable the spinner
during follow mode polling to keep output clean.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests verify:
- Success message shown when session state available
- Error message shown when sessionEntry missing
- Error message shown when sessionStore missing
- No model message when no /model directive
Covers edge cases for #1435 fix.
Previously, the /model command would display 'Model set to X' even when
the session state wasn't actually persisted (when sessionEntry, sessionStore,
or sessionKey were missing). This caused confusion as users saw success
messages but the model didn't actually change.
This fix:
- Tracks whether the model override was actually persisted
- Only shows success message when persist happened
- Shows a clear error message when persist fails
AI-assisted: Claude Opus 4.5 via Clawdbot
Testing: lightly tested (code review, no runtime test)
The session-memory hook saves session context to memory files when /new is run,
which is useful internal housekeeping. However, the confirmation message that
was displayed to users (showing the file path) leaked implementation details.
This change removes the user-visible message while keeping the console.log
for debugging purposes. The hook continues to save session context silently.
The frontend fix alone wasn't enough because:
1. serveIndexHtml() was injecting the raw avatar filename into HTML
2. agent.identity.get RPC was returning raw filename, overwriting the
HTML-injected value
Now both paths resolve local file avatars (*.png, *.jpg, etc.) to the
/avatar/{agentId} endpoint URL.
Move mattermost channel implementation from core to extensions/mattermost plugin. Extract config schema, group mentions, normalize utilities, and all mattermost-specific logic (accounts, client, monitor, probe, send) into the extension. Update imports to use plugin SDK and local modules. Add channel metadata directly in plugin definition instead of using getChatChannelMeta. Update package.json with channel and install configuration.
Slack's files.uploadV2 API no longer supports the filetype field and logs
deprecation warnings when it's included. Slack auto-detects the file type
from the file content, so this field is unnecessary.
This removes the warning:
[WARN] web-api:WebClient filetype is no longer a supported field in files.uploadV2.
The message tool accepts path and filePath parameters in its schema,
but these were never converted to mediaUrl, causing local files to
be ignored when sending messages.
Changes:
- src/agents/tools/message-tool.ts: Convert path/filePath to media with file:// URL
- src/infra/outbound/message-action-runner.ts: Allow hydrateSendAttachmentParams for "send" action
Fixes issue where local audio files (and other media) couldn't be sent
via the message tool with the path parameter.
Users can now use:
message({ path: "/tmp/file.ogg" })
message({ filePath: "/tmp/file.ogg" })
Adds the ability to customize the assistant's name and avatar in the Web UI.
Configuration options:
- config.ui.assistant.name: Custom name (replaces 'Assistant')
- config.ui.assistant.avatar: Emoji or letter for avatar (replaces 'A')
Also reads from workspace IDENTITY.md as fallback:
- Name: field sets the assistant name
- Emoji: field sets the avatar
Priority: config > IDENTITY.md > defaults
Closes#1383
When creating exec tools via chat/Discord, agentId was not passed,
causing allowlist lookup to use 'default' key instead of 'main'.
User's allowlist entries in agents.main were never matched.
Now derives agentId from sessionKey if not explicitly provided,
ensuring correct allowlist lookup for all exec paths.
- Add avatar field to IdentityConfig type
- Add avatar parsing in AgentIdentity from IDENTITY.md
- Add renderAvatar support for image avatars in webchat
- Add CSS styling for image avatars
Users can now configure a custom avatar for the assistant in the webchat
by setting 'identity.avatar' in the agent config or adding 'Avatar: path'
to IDENTITY.md. The avatar can be served from the assets folder.
Closes #TBD
TypeBox Type.Optional(Type.String()) accepts string|undefined but NOT null.
Discord exec was failing with 'resolvedPath must be string' because callers
passed null explicitly. Web UI worked because it skipped the approval request.
Fixes exec approval validation error in Discord-triggered sessions.
- Add ToolCallIdMode type ('standard' | 'strict') for provider compatibility
- Standard mode (default): allows [a-zA-Z0-9_-] for readable session logs
- Strict mode: only [a-zA-Z0-9] for Mistral via OpenRouter
- Update sanitizeSessionMessagesImages to accept toolCallIdMode option
- Export ToolCallIdMode from pi-embedded-helpers barrel
Addresses review feedback on PR #1372 about readability.
Some providers like Mistral via OpenRouter require strictly alphanumeric
tool call IDs. The error message indicates: "Tool call id was
whatsapp_login_1768799841527_1 but must be a-z, A-Z, 0-9, with a length
of 9."
Changes:
- Update sanitizeToolCallId to strip all non-alphanumeric characters
(previously allowed underscores and hyphens)
- Update makeUniqueToolId to use alphanumeric suffixes (x2, x3, etc.)
instead of underscores
- Update isValidCloudCodeAssistToolId to validate alphanumeric-only IDs
- Update tests to reflect stricter sanitization
Fixes#1359
Co-Authored-By: Claude <noreply@anthropic.com>
Add Mattermost as a supported messaging channel with bot API and WebSocket integration. Includes channel state tracking (tint, summary, details), multi-account support, and delivery target routing. Update documentation and tests to include Mattermost alongside existing channels.
* feat(sessions): add channelIdleMinutes config for per-channel session idle durations
Add new `channelIdleMinutes` config option to allow different session idle
timeouts per channel. For example, Discord sessions can now be configured
to last 7 days (10080 minutes) while other channels use shorter defaults.
Config example:
sessions:
channelIdleMinutes:
discord: 10080 # 7 days
The channel-specific idle is passed as idleMinutesOverride to the existing
resolveSessionResetPolicy, integrating cleanly with the new reset policy
architecture.
* fix
* feat: add per-channel session reset overrides (#1353) (thanks @cash-echo-bot)
---------
Co-authored-by: Cash Williams <cashwilliams@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Add support for '*' wildcard in Discord channel configuration,
matching the existing guild-level wildcard behavior.
This allows applying default channel settings (like autoThread)
to all channels without listing each one explicitly:
guilds:
'*':
channels:
'*': { autoThread: true }
Specific channel configs still take precedence over the wildcard.