Lift file state from ChatInput to ChatPage so DropZone.onDrop can
append dropped files to the shared state. Previously drag-and-drop
showed the overlay but never passed files to the input.
Improves supportsThoughtSignature with providerType and model normalization.
Strengthens collapseToolCallsWithoutSig with TrimSpace and camelCase support
to handle 'Thinking=OFF' whitespace signatures and proxy key translations.
Simplified approach to fix duplicate tool_call_ids that cause HTTP 400 on OpenAI-compatible APIs (OpenRouter, vLLM, DeepSeek).
- uniquifyToolCallIDs: appends runID+iteration+index to all IDs at response time, guaranteeing cross-turn uniqueness via UUID
- sanitizeHistory: simple seen-set dedup for legacy sessions with pre-existing duplicate IDs
- Anthropic guard: skips ID rewriting when RawAssistantContent is present
- Unit tests for both paths + edge cases
Closes#283
Add "config" to channelInstanceAllowedFields so channel config changes
from the UI are actually persisted to the database instead of being
silently dropped by the allowlist filter.
- Add MiniMax-M2.7 to minimax_native provider model list
- Fix combobox dropdown UX: show all options on re-focus instead of filtering by current selection
- Add allowCustom prop to combobox for typing custom model names not in the list
- Add i18n key useCustomModel for EN/VI/ZH
- Audit all raw input/select/textarea elements across web UI to use text-base (16px) on mobile, preventing iOS Safari auto-zoom on focus
- Listen for session.updated event to update sidebar labels in-place
- Replace bg-muted/30 with bg-muted on chat cards so dotted background
pattern does not bleed through
After the first agent run completes, generate a short title from the
user's message using a 10s/50-token LLM call. Title is stored in the
session label column and pushed to clients via session.updated event.
Detect agents stuck in read-only loops (consecutive non-mutating tool
calls) and same-result patterns (identical outputs from different args).
Warns at threshold, force-stops at critical.
- Add deleteSession() to useChatSessions hook
- Trash icon on session rows (hover-reveal, always visible on mobile)
- Simple confirm dialog (no typing required)
- If active session deleted, switch to next or create new
- i18n strings for en/vi/zh
Root cause: ErrorBoundary in AppLayout used key={location.pathname},
causing React to unmount/remount the entire page on every URL change.
- Add stableErrorBoundaryKey() that strips dynamic segments from pathname
- Merge /chat and /chat/:sessionKey into single /chat/:sessionKey? route
- Derive sessionKey from useParams() instead of duplicating into useState
(dual state caused race condition: setState + navigate → B→A→B bounce)
- Move session reset from render-time setState to useEffect
- Only show loading spinner when messages array is empty
Detect exit code 127 and "command not found" patterns in sandbox
execution paths and append a [SANDBOX] hint to the error output,
so the model can inform the user about sandbox environment limitations.
Only applies to sandbox execution — host paths are untouched.
- buildNewSessionKey uses agent:{id}:ws:direct:{uuid} format
- sessions.list sends channel:"ws" to filter WS-only sessions
- isOwnSession supports both new and legacy key formats
- Read-only bar now floating with rounded corners
Runs idempotent regex migration at PGSessionStore init to convert
legacy WS session keys to canonical format. Handles multi-hyphen
userIDs (UUIDs) by matching last segment as timestamp.
- Add BuildWSSessionKey() and IsWSSession() helpers for ws:direct:{convId} format
- Change ChatID to userId (not clientID) for consistent team/workspace isolation
- Add Channel and UserID fields to SessionListOpts with dynamic WHERE builder
- Extract shared buildSessionFilter() for ListPaged and ListPagedRich
- Chat sidebar sends channel:"ws" to filter WS-only sessions per user
- Sessions admin page remains unfiltered (no channel param)
* fix(telegram): isolated transport, sticky fallback, and overall timeouts
* fix(telegram): improve networking isolation for high-concurrency
- Tune MaxIdleConnsPerHost to 64 (default 2 starves concurrent sends)
- Fix data race in enableIPv4Only using sync.Once
- Add force_ipv4 config option for explicit IPv4-only mode
- Narrow auto-detect heuristic to "unreachable" only (avoid false-positive on timeouts)
- Use bot username instead of token prefix in logs
- Extract applyIPv4Dialer helper for reuse between config and runtime paths
---------
Co-authored-by: viettranx <viettranx@gmail.com>
Require root cause analysis and production scenario thinking
(high concurrency, multi-tenant, failure cascades) before designing
fixes or features. Prefer explicit config over runtime heuristics.
Agent runs were tied to the HTTP request context, so navigating away
cancelled the LLM stream. Now uses context.WithoutCancel() to survive
page navigation while preserving explicit chat.abort support.
Team task announces rebuilt session keys via BuildScopedSessionKey(),
producing different format than WS sessions (ws:direct:uuid vs
ws-userId-ts). Now stores and forwards origin_session_key through
task metadata and dispatch, matching subagent announce pattern.
Also adds "ws" to InternalChannels to suppress spurious warnings.
* fix(memory): auto-backup + overwrite warning for memory files
When an agent writes to a memory file that already has content:
1. Old content is backed up to memory/.prev/{filename} (best-effort)
2. Write succeeds normally (no blocking)
3. Tool result includes warning with previous content (truncated to
4000 chars) so the agent can notice and merge if needed
Backup files are hidden from list_files and not indexed/embedded.
No database migration needed - uses existing PutDocument.
* fix(memory): support append mode + fix UTF-8 truncation in overwrite warning
- Add appendMode parameter to MemoryInterceptor.WriteFile() so
write_file(append=true) actually appends to memory files instead
of silently replacing content. Uses "\n\n---\n\n" separator.
- Fix UTF-8 truncation in overwrite warning: use []rune instead of
byte slicing to avoid cutting multi-byte characters.
- Fix char count in warning message: use len([]rune()) not len().
- Remove .prev/ backup mechanism (prevBackupPath, isPrevPath, backup
writes, ListFiles filter) in favor of simpler approach: append
mode prevents data loss, warning enables self-correction.
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
* feat(feishu): add list_group_members tool + native @mention support
Add agent tool to list all members in a Lark group chat via
GET /open-apis/im/v1/chats/{chat_id}/members with auto-pagination.
- New tool `list_group_members`: returns member IDs + display names
- Native @mention conversion: @ou_xxx in outbound messages becomes
real Lark mentions (post: "at" element, card: <at> markdown tag)
- Auto-sync discovered members into ContactStore
- GroupMemberProvider interface on Channel for extensibility
- Add im:chat.members:read scope to channel setup UI
Requires Lark app scope: im:chat.members:read
* fix(feishu): URL-encode API params + channel-aware tool filtering
- URL-encode chatID (PathEscape) and pageToken (QueryEscape) in
ListChatMembers to prevent path traversal and query corruption
- Add slog.Warn when ListGroupMembers fails for observability
- Add ChannelAware interface so tools can declare required channel types
- ListGroupMembersTool now declares RequiredChannelTypes=["feishu"]
- loop.go filters out channel-specific tools when channel type mismatches,
so list_group_members only appears for Feishu conversations
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
Subagent announce messages told the parent LLM "There are still X active
runs" — a bare number. The LLM had to reconstruct which subagents were
done vs running from conversation history, causing hallucination when
results arrived out of order.
Replace with a full roster listing each subagent by label and status,
including per-agent config limits. Remove dead CountRunningForParent()
and unused rosterFunc field on AnnounceQueue.
When a Feishu/Lark user replies to an existing message, the agent now
sees the quoted message content via the GetMessage API, formatted as
[Replying to SenderName]...[/Replying] — matching the Telegram channel
behavior. Previously parent_id was parsed but the content was silently
discarded, so the agent had no idea what the user was replying to.
Requires im:message:readonly scope on the Lark app.
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
* fix(channels): collect contacts for DM and group-mentioned messages
Previously, EnsureContact was only called inside the "not mentioned →
record history → return" branch, so contacts were never saved for
messages that the bot actually processed (DMs and group @mentions).
This caused the contacts list to show empty names ("—") for active
users across all channels.
Move contact collection to the main processing path (before
PublishInbound / HandleMessage) so every processed message upserts
the sender's display name. The existing ContactCollector 30-minute
cache prevents redundant DB writes.
Affected channels: Feishu, Telegram, Discord, Slack, Zalo, WhatsApp.
* fix(slack): use clean userID for EnsureContact cache key
The senderID param in HandleMessage is the compound "U123|DisplayName"
form, which produces a different cache key than the clean "U123" used
in the group-history path (handlers.go:178). Use userID (extracted
clean ID) to ensure cache dedup works correctly.
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
The HTTP allowlist (agentAllowedFields) was missing three fields that
the web UI sends when saving agent settings: is_default,
budget_monthly_cents, and subagents_config. These fields were silently
filtered out by filterAllowedKeys(), so saves appeared successful but
values never persisted to the database.
Also adds exclusive-default logic: when setting is_default=true on an
agent, any previously-default agent is automatically unset first.
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
- Fix HeartbeatLog interface field names (snake_case → camelCase) to match
backend JSON tags — fixes white screen crash on logs dialog open
- Format duration with formatDuration(), show token usage per log entry
- Fix logs dialog padding to match standard pattern + close button overlap
- Show model override and last error on heartbeat card
- Replace status badge with colored dot in agent header
- Compact header badges on mobile (icon-only for evolving/static)
- Switch sidebar to tablet breakpoint (1024px) for earlier collapse
Track per-tool execution time statistics in session metadata. When a tool
call exceeds its adaptive threshold (2x historical max, min 120s default),
send a direct outbound notification to the user.
- ToolTimingMap: parse/serialize/record/threshold from session metadata
- StartSlowTimer: fires once per tool call, auto-cancels on completion
- Team config: slow_tool toggle (default on, always direct, never leader)
- UI: toggle in team settings with i18n (en/vi/zh)
- Store: add GetSessionMetadata to session store interface
- Team lead: no completion language after delegating, no question phrasing
- Group chat: inject reply context hint (NO_REPLY when reply addresses others)
- Both v1 and v2 team lead sections updated
go-semantic-release creates tags via GITHUB_TOKEN, which doesn't
trigger other workflows (GitHub anti-loop policy). This caused
docker-publish.yaml to stop firing since v1.2.0.
Move Docker image builds (7 variants + web) directly into release.yaml
as parallel jobs alongside build-binaries.
Go can serialize empty slices as null in JSON. The events page
accessed p.changes.length without null checking, causing a crash
when viewing team.updated or agent_link.updated events with null
changes arrays.
Fixes#192
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
* feat(ui): show required API scopes for Feishu/Lark channels
Add a collapsible info panel listing the required Lark/Feishu API
permissions (scopes) on the channel create/edit dialog and config
detail tab. Includes reminder about Contact Range and app publishing.
* fix(feishu): annotate DM messages with sender name
Feishu DMs were missing the [From: ...] annotation, so the agent
couldn't identify who was messaging. Group messages already had this.
Align with Telegram channel which annotates both DM and group messages.
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
* fix(providers): auto-clamp max_tokens on model rejection + fix verify for reasoning models
When OpenAI-compat models reject max_tokens as too large (e.g. gpt-3.5-turbo
supports 4096 but we send 8192), parse the model's stated limit from the 400
error, clamp the value, and retry once. This fixes agent creation for models
with lower output token limits without hardcoding model names.
Also increase the provider verify endpoint's max_tokens from 1 to 50 so
reasoning models (gpt-5, o-series) have enough headroom for internal
reasoning during the check call.
Closes#248, closes#245
* refactor(providers): extract chat retry closure + fix clamp log key
- Extract duplicate retry closure into chatRequestFn() to follow DRY
- Fix slog logging wrong key: body["max_tokens"] was nil for reasoning
models that use max_completion_tokens — now uses clampedLimit() helper
- Remove unnecessary _ = resp in provider verify endpoint
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- Replace timezone text input with Select dropdown using shared IANA_TIMEZONES
- Extract IANA_TIMEZONES to constants.ts (reused by cron-section)
- Shrink interval input width (w-24 → w-[4.5rem]) for 3-digit fit
- Fix schedule section layout: flex-based with fixed-width time inputs
- Default isolatedSession to false
* fix(tts): config save + Edge provider registration + dark mode chat bubbles
- Wrap TTS config payload in `raw` field for config.patch RPC (#229)
- Always register Edge TTS provider (free, no API key) instead of gating on `enabled` flag
- Fix low-contrast user message bubbles in dark mode chat
* fix(tts): skip duplicate media dispatch when temp file already delivered
When both the agent loop and the message tool dispatch the same TTS
temp file, the first dispatch succeeds and cleanup deletes it. Filter
out missing temp media files before sending to prevent "file not found"
errors and spurious error notifications on Telegram/Slack/Discord.
* feat(tts): include edge-tts in Docker image when Python enabled
Edge TTS is free (no API key) and serves as a universal TTS fallback.
Install it alongside Python in both ENABLE_PYTHON and ENABLE_FULL_SKILLS builds.
* chore(docker): expose build args from .env for compose builds
Pass ENABLE_OTEL, ENABLE_PYTHON, ENABLE_FULL_SKILLS as env-driven
build args so .env can control Docker build features without editing
docker-compose.yml directly.
* fix(tts): hot-reload TTS config on settings change via pub/sub
TTS providers were only registered at startup, so changing provider/API
key via the Web UI had no effect until container restart. Add a
tts-config-reload bus subscriber that rebuilds the TTS manager on
config changes, matching the pattern used by quota, cron, and web_fetch.
Always create a TtsTool at startup (even without providers) so the
reload subscriber can populate it when settings are first configured.
* fix(tts): protect TtsTool.UpdateManager with RWMutex to prevent data race
UpdateManager() can be called from the config reload goroutine while
Execute() reads t.manager concurrently from agent goroutines. Add
sync.RWMutex following the same pattern as WebFetchTool.UpdatePolicy().
Also update setupTTS doc comment which incorrectly stated it could
return nil — Edge TTS is now always registered.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- Add ProviderModelSelect to heartbeat config dialog (allowEmpty, verify button)
- Backend: accept providerName in HEARTBEAT.SET, resolve to UUID via GetProviderByName
- Add ModelOverride to RunRequest, used by Loop when set (cheaper model for heartbeat)
- Ticker passes heartbeat model override to agent RunRequest
- Fix: InvalidateCache after UpdateState so ListDue picks up new next_run_at immediately
- i18n: add sectionModel/modelHint keys (en/vi/zh)