Add early detection and graceful degradation when the model exhausts its
iteration budget without producing a text response:
- Inject 75% budget nudge telling model to start summarizing
- Strip all tool schemas on final iteration to force text-only response
- Adaptive web_fetch maxChars (60K→20K→10K) via iteration progress context
- Bump DefaultMaxIterations 20→30 for web-research workloads
- Add userFriendlyError() helper to hide 5xx errors from users
- Add Loader2 animate-spin to all save/update/create/delete buttons
- Add useMinLoading(600ms) to StickySaveBar for visible feedback
- Move toast notifications from components into hooks (single source)
- Remove inline saved/saveError state in favor of toast system
- Add toast to all mutation hooks: agents, channels, skills, builtin-tools,
sessions, teams, storage, tts, cron, providers, config
- Add i18n toast keys for en/vi/zh across all namespaces
- Deduplicate toast calls in team board components
Model confused context-history media tags (<media:image>) with current
message media, triggering read_image on non-existent images. Change
lightweightMediaTags() to use descriptive brackets (e.g. [sent an image])
so LLMs can distinguish history descriptions from actionable media tags.
- Reduce quickClassify threshold from 60 bytes to 15 runes to prevent
false positives (e.g. "làm đơn giản thôi" no longer matches "thôi")
- Use utf8.RuneCountInString for accurate Unicode length check
- Replace substring match with whole-word boundary check to avoid
false positives like "nonstop" matching "stop"
- Remove statusKeywords; only exact "?" is fast-pathed as status query,
all other status queries go through LLM classification
- Increase LLM classify timeout from 5s to 10s
- Differentiate steer (inject into running loop) from newTask (queue
behind active run) instead of treating both as mid-run injection
- Fix TrimLeft → TrimPrefix to strip only one underscore separator
- Add registryName to indexedResult for parallel path cache
- Use registryName for bootstrapToolAllowlist and loopDetector checks
- Add server-side sanitization for ToolCallPrefix input
- Remove dead chatMessages alias and unrelated ParseStripAssistantPrefill
- Remove orphaned stripAssistantPrefill i18n keys
- Add 13 unit tests for StripToolPrefix
* feat(agent): support tool call prefix stripping for proxy providers
Proxy providers like LiteLLM and OpenRouter may prepend a prefix to
tool call names returned by the model (e.g. "proxy_exec" instead of
"exec"). This broke tool policy validation, registry lookup, and
hardcoded name checks for "team_tasks" and "spawn" in both serial
and parallel execution paths.
Add per-agent toolCallPrefix configuration that strips the configured
prefix from incoming tool call names before registry resolution. The
stripping is applied at resolveToolCallName() which is called before
permission checks, registry execution, and spawn/team_tasks detection.
- Add StripToolPrefix() supporting literal ("proxy_") and template
("{tool_name}") patterns
- Add toolCallPrefix to ToolPolicySpec with backward compat from old
"toolPrefix" JSON key
- Fix config save using spread operator to prevent dropping new fields
- Add UI input in Tool Policy section with i18n (en/vi/zh)
* chore: ignore AI tool config directories
Add .gemini/, .claude/, .opencode/ to .gitignore to prevent
committing user-specific AI tool configurations.
The scanner only tracked subdirectories as local modules, so cross-file
imports like `from extract_form_field_info import ...` were incorrectly
reported as missing pip packages, causing install failures.
On first start with a fresh Docker volume, mkdir for .runtime
subdirectories can fail due to a volume initialisation race condition,
causing the container to restart loop. The directories are only needed
for agent-installed packages (pip/npm), so the failure is non-fatal.
Address issues identified in PR #137 with a cleaner approach:
- Sandbox isolation: add SandboxCwd/ResolveSandboxPath helpers to map
filesystem tool paths to agent-scoped container subdirectories,
preventing cross-agent file access via read/write/edit/list tools
- DooD volume mounting: add resolveHostWorkspacePath with multi-strategy
container ID detection (/proc/self/mountinfo, HOSTNAME, os.Hostname)
and 5s timeout on docker inspect
- Sandbox hints: expand from 1 pattern (binary not found) to 6 patterns
(permission denied, network disabled, read-only FS, missing file,
resource limits) with MaybeFsBridgeHint for filesystem tools
- Nginx DNS: add Docker resolver (127.0.0.11) with dynamic upstream
variable to handle backend container IP changes
- MCP args: switch from comma-separated to space-separated parsing
with quote support for --flag="value with spaces" patterns
- Refactor: rename ExecTool.workingDir to workspace for consistency,
extract sandbox.DefaultContainerWorkdir constant
Replace single-value idRemap with queue-based idQueue so multiple tool
results sharing the same original ID pair correctly in encounter order.
Previously the map overwrote on duplicates, causing the second result to
be dropped and a synthetic placeholder to be emitted instead.
* fix(agent): handle duplicate tool call IDs with occurrence-aware sanitization
* refactor(agent): simplify duplicate tool call ID handling
Replace complex occurrence-aware queue system with simpler approach:
- uniquifyToolCallIDs: append 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, no queue/counter logic needed
- Restore full problem description in sanitizeHistory docstring
- Add unit tests for both dedup paths and edge cases
---------
Co-authored-by: viettranx <viettranx@gmail.com>
The squash merge of #291 missed the parallel tool execution path.
Tool errors running in parallel still displayed as "Done" instead
of "Failed" when reloading chat history.
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.