Add StreamingTTSProvider interface for audio streaming support. Implement voice
caching with TTL+LRU. Add ElevenLabs model validation and voice list retrieval.
Add optional Audio *AudioConfig pointer field on Config with STT and
Music sub-structs. Nil-safe — absent in JSON5 decodes as nil, no
breaking change. cfg.Tts retained unchanged for backward compat.
setupAudioExtras stub wired for Phase 3/4 STT/Music provider
registration.
internal/tts becomes a thin backward-compat alias over internal/audio:
15 type aliases, 6 constants, 5 constructors, 5 compile-time signature
guards. All pre-refactor callers compile unchanged. alias_test.go
enforces symbol coverage and type identity. Old per-provider files
(manager, types, elevenlabs, openai, edge, minimax) are removed in
the same commit to keep history bisectable.
MCP servers with require_user_credentials (e.g. Notion) were defined
but never loaded into the agent's tool registry. Three gaps:
1. getUserMCPTools was defined but never called — add call in
makeBuildFilteredTools before FilterTools runs each iteration.
2. hasMCPTools stayed false when only user-credential servers existed,
so agentToolPolicyWithMCP never injected "group:mcp" into alsoAllow.
Now set true when mcpUserCredSrvs is non-empty.
3. Per-user BridgeTools were registered in the registry but never added
to the "mcp" tool group, so expandSpec("group:mcp") returned empty.
Add MergeToolGroup helper for additive group updates.
Also add debug log when getUserMCPTools skips due to empty userID.
Stale recovery sweeps traces by `start_time < NOW() - threshold`, which
measures trace age rather than inactivity. Any threshold low enough to
be useful (2-10 min) kills legitimate long-running agent runs: research
chains, large code generation, extended shell commands routinely exceed
10 minutes.
Disabled in Start() — function kept in place for easy re-enable once a
`last_span_at` column is added so recovery can gate on "no activity for
N minutes" instead of "started > N min ago".
Trade-off: zombie traces from gateway crashes may remain `running` in
DB. Accepted: primary abort path (router 2-phase + trace.status WS
event) handles the common case; safety-net gap preferred over false
kills of healthy runs.
Integration test RecoverStaleNow() still works (manual trigger, not
loop-dependent) so coverage of the recovery function itself is
preserved for when it's re-enabled.
matchesBinaryDeny used unanchored regex on joined args, causing `-v` pattern
to false-positive on `--version`. Split deny_verbose into matchesBinaryVerbose
with start-anchored per-arg matching: `-v` blocks `-v`, `-vv`, `-v=1` but not
`--version`. deny_args keeps joined matching for multi-token patterns.
v3 pipeline's parallel path (makeExecuteToolRaw) skipped the tool.call
WebSocket event, so web UI and desktop UI silently dropped tool cards
during real-time streaming. Only page refresh (which reloads history)
revealed the tool calls. Both UIs rely on tool.call to create entries
that later tool.result events can update.
Fix: mirror the sequential path's emission in makeExecuteToolRaw.
Bus.Broadcast is RWMutex-guarded, safe to call from parallel goroutines.
Add tests at two layers to prevent regression:
- Pipeline layer (stages_test.go): guards the dispatch contract —
multiple tool calls route through ExecuteToolRaw + ProcessToolResult
rather than ExecuteToolCall. Previously the parallel path had zero
test coverage, which is why this bug escaped.
- Agent layer (loop_pipeline_tool_callbacks_test.go): guards the
emission contract — both sequential and parallel wrappers emit
tool.call with correct payload and routing context. Mutation-verified.
- AddError() now skips broadcast after Finish() to prevent cancelled
goroutines from emitting error events to UI after user stops enrichment
- batchSummarize skips AddError when context is cancelled (expected on stop)
- Rescan always re-enqueues unenriched docs alongside new/updated files,
worker-level dedup prevents double-processing
* fix: handle ignored errors, unsafe type assertions, missing panic recovery
- Cron scheduler (PG + SQLite): check all ExecContext errors in
recomputeStaleJobs, run log insert, job delete, and post-run update.
Previously these errors were silently discarded, which could leave
job state inconsistent without any log trace.
- Discord: use comma-ok type assertions on sync.Map placeholder loads
to prevent potential panics from bare type assertions.
- Slack: use comma-ok type assertions in sweepMaps for dedup and
thread participation eviction to prevent potential panics.
- Feishu: add safego.Recover to WebSocket goroutine so a panic in
the WS client doesn't silently kill the goroutine.
- Agent export: add tenant owner/admin permission check to canExport.
Previously only agent owner and system owner could export — tenant
admins were incorrectly denied.
- Channel health: use errors.Is/errors.As for context.DeadlineExceeded,
net.DNSError, and net.OpError before falling back to string matching.
DNS NXDOMAIN is now correctly classified as non-retryable.
* fix(review): revert export to system-only + add missing rows.Err check
- Revert canExport tenant role check — export/import is restricted to
agent owner and system owner by design
- Add rows.Err() check after recomputeStaleJobs loop in PG cron
(parity with SQLite implementation)
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
* fix(facebook): preserve fb_mode metadata in outbound routing + admin reply detection
Two issues fixed:
1. Messenger auto-reply never delivered because fb_mode metadata was
stripped during outbound message construction. The routing whitelist
in gateway_consumer_normal.go and channels/events.go only copied
thread_id/local_key/group_id — facebook-specific keys (fb_mode,
sender_id, page_id, reply_to_comment_id) were dropped, causing
facebook.Send() to fall into the comment path and fail with
"reply_to_comment_id missing".
2. Added admin reply detection: before sending a bot reply, check via
Graph API if the last page message in the conversation was sent by
an admin (human) rather than the bot itself. Skips bot reply when
admin already responded, preventing duplicate messages.
Uses timestamp comparison with bot's own send history to distinguish
bot-sent vs admin-sent page messages (both have from.id = page_id).
* chore: exclude compiled binary from git
* fix(facebook): ignore bot echoes in messenger cooldown
* fix(facebook): add memory cleanup for admin reply maps and reduce echo window
- Add adminReplied and botSentAt eviction to runDedupCleaner to prevent
unbounded memory growth on high-traffic pages
- Reduce botEchoWindow from 60s to 15s to avoid misclassifying real admin
replies as bot echoes
- Restore doc comments on routingMetaKeys and copyRoutingMeta
- Add cross-reference comment between consumer and events routing key lists
- Simplify admin-reply skip log (remove redundant map re-read)
---------
Co-authored-by: khanhtran <>
Co-authored-by: Plateau Nguyen <nguyennlt.ncc@gmail.com>
Co-authored-by: viettranx <viettranx@gmail.com>
SSE progress hook crashed React when backend returned nested error
object from writeError ({"error": {"code": ..., "message": ...}})
instead of flat string. Now parses both formats correctly.
PolicyEngine.IsOwner lacked the "system" fallback that isHTTPOwnerID
already had — when owner_ids is empty, "system" user was rejected from
all backup/restore endpoints. Added consistent fallback logic.
Also added slog.Warn to all silent owner-check rejections across
backup, restore, tenant backup/restore, and S3 handlers.
- Add bgalert package: classify provider errors (auth, billing,
model_not_found), store alert in system_configs, broadcast WS event
- Wire AlertDeps into consolidation (episodic, semantic, dreaming)
and vault enrich workers to report failures after retry exhaustion
- Auto-clear alert when admin changes provider-related system configs
- Add BackgroundErrorBanner component with dismiss + "Fix in Settings"
- Lift settings modal state to AppLayout so banner can open it
- Add EventBackgroundError to admin-only WS event filter
- Add i18n translations (en/vi/zh) for alert messages and reasons
- Replace char-based heuristic (chars/4) with tiktoken BPE for accurate
token counting, especially for non-ASCII content (Vietnamese/Chinese)
- Add pruningEstimator wrapper with tiktoken/fallback dual-path
- Raise default soft trim budget from 3K to 6K chars (3K head + 3K tail)
- Media tools (read_image, read_document, read_audio, read_video) get
higher soft trim budget (8K: 4K head + 4K tail) and skip hard clear
entirely — their vision/audio descriptions are irreplaceable
- Add per-result context guard (Pass 0): force-trim any single tool
result exceeding 30% of context window
Both v3 mid-loop compaction and v2 background summarization were
dropping MediaRefs when summarizing old messages, making previously
shared images/documents permanently inaccessible to the agent.
Now collect up to 30 most recent MediaRefs from compacted messages
and attach them to the summary/first-kept message so they survive
the compaction cycle.
Extract shouldSkipEnrichment() replacing 4 scattered goclaw_gen_ checks.
Filter also skips UUID, hex hash, digit-only, short, and known junk filenames.
Fix cancelFuncs never being populated — Stop() was a no-op leaving UI stuck.
When share_memory=true, memory_search tool used MemoryUserID(ctx)=""
(cross-user) but auto-inject still passed raw userID, limiting episodic
L0 injection to the current user only. Now uses store.MemoryUserID(ctx)
so shared-memory agents get cross-user episodic summaries in auto-inject.
Group-scoped agents could read sessions from other groups via session
tools (sessions_list, sessions_history, session_status, sessions_send)
because they only checked agent_key, not group context. This caused
cron jobs to leak data from unrelated groups into reports.
Add isSessionInScope() guard to all 4 session tools with colon-bounded
chatID matching. New share_sessions setting (default false) controls
cross-group visibility, following the same pattern as share_memory and
share_knowledge_graph. Web UI toggle and i18n strings included.
63 test cases covering guild/DM/group users, realistic Zalo IDs,
boundary exactness, multi-colon chatIDs, and the exact bug scenario.
- Sigma.js graph: restore doc_type coloring (revert Louvain community detection)
- Fix animation flash after FA2 layout finishes by removing post-processing
camera reset and redundant noverlap/compactOrphans in stopLayout
- Fix vault tree "Load more" state bug when filtering by doc_type:
add treeVersion counter to force re-mount and reset auto-expand state
- Fix meta map loss: loadRoot now merges instead of replacing, preserving
subtree entries from previous loadSubtree calls
- Add compact graph DTO endpoints and hooks for KG and vault graphs
- Add semantic zoom tiers, adaptive FA2 settings, and node sizing
- Add tenant-level filesystem path restrictions via system_configs table
- Merge tenant paths with global skills directories in allowedWithTeamWorkspace()
- Propagate tenant paths to subagents via RunContext
- Seed allowed_paths from config.json to system_configs on startup
- Fix TestStoreTask_RaceToClaimSameTask: use composite PK for team members
- Add case-insensitive path comparison on Windows in isPathInside()
- Add allowed_paths config for cross-drive access on Windows
- Wire allowed_paths to read/write/edit/list file tools
- Add POST /v1/agents/sync-workspace endpoint to propagate workspace changes
- Add comprehensive tests for cross-drive, tenant isolation, symlink escape
Ensures model sees the hint to ask for timezone even after bootstrap
completes, preventing timezone from being permanently missed if user
skips the question during initial onboarding.
Instead of assuming Asia/Saigon or UTC for all users, the model now
asks for timezone when the user mentions times/schedules/reminders.
After first ask, timezone is stored in USER.md for future sessions.
Closes#833 discussion.
* fix(telegram): propagate local_key for forum topic routing in team notifications
Team task status messages (dispatched, completed, progress) were always
delivered to the General topic in Telegram forum groups because the
notification pipeline had no access to the originating topic's local_key.
Root cause: wireTeamProgressNotifySubscriber in gateway_events.go published
OutboundMessage with no Metadata, so the Telegram adapter had no
message_thread_id to route to the correct forum topic.
Fix has two parts:
1. Team notify path (root cause):
- Add LocalKey field to TeamTaskEventPayload (protocol)
- Extract local_key from tool context in WithContextInfo()
- Add LocalKey to NotifyRoutingMeta
- Pass LocalKey through to OutboundMessage Metadata in both
leader mode (InboundMessage) and direct mode (OutboundMessage)
2. MCP bridge context (supporting):
- Propagate local_key and session_key through bridge HTTP headers
- Add X-Local-Key and X-Session-Key to BridgeContext
- Extract and inject into tool context in gateway middleware
- Include in HMAC signature for integrity
Closes#798
* fix(telegram): pass LocalKey from task metadata in all dispatch/fail broadcast sites
The initial fix added LocalKey to the event payload and WithContextInfo(),
but 4 broadcast call sites use individual With* options instead of
WithContextInfo — so LocalKey was never populated for:
- fallback_dispatch (team_tasks_create.go)
- dispatch_unblocked (team_tool_dispatch.go)
- post_turn dispatch (team_tool_validation.go)
- blocker/fail (team_tasks_blocker.go)
Add WithLocalKey() option function and extract TaskMetaLocalKey from task
metadata at each site, matching the existing TaskMetaPeerKind pattern.
* test(mcp): add HMAC verification tests for extra params (localKey, sessionKey)
- Add tests for SignBridgeContext/VerifyBridgeContext with extra params
- Test backward compat fallback for pre-localKey sessions
- Test that param order matters in signature
- Add clarifying comment for routing context injection security model
---------
Co-authored-by: Jens Henke <jens@henke.dk>
Co-authored-by: viettranx <viettranx@gmail.com>
When rescan finds no new/updated files but some docs still lack summaries
(e.g. previous enrichment failed due to provider timeout), automatically
re-enqueue them for enrichment retry.
- Add VaultStore.ListUnenrichedDocs() to fetch docs with empty summary
- Add EnrichWorker.EnqueueUnenriched() to emit events for retry
- Add RescanResult.Reenqueued field to track re-enqueued count
- Update UI to show "X re-queued for enrichment" toast
SQLite implementations were missing tenant_id WHERE clauses that the
PostgreSQL equivalents have. While Lite edition is single-tenant in
practice, this maintains dual-DB parity and prevents potential issues
if the codebase evolves.
Follows the same pattern as other methods in the file (GetSkillFilePath,
CreateSkillManaged, etc.) that use store.TenantIDFromContext with
MasterTenantID fallback.
Only count BlockReplies when tool calls are present (matching when
EmitBlockReply actually fires in think_stage). Final answers without
tool calls must not increment the counter, otherwise gateway dedup
falsely suppresses delivery on non-streaming channels.
- Update ObserveStage to check len(resp.ToolCalls) > 0
- Rename test to reflect new behavior (tool calls required)
- Add regression test for #838 scenario
Closes#838
- Fix TestPrivateReply_ReturnsError: use HTTP 400 instead of 200 since
doRequest only returns error for status >= 400
- Remove unused MaxThreadDepth config field from CommentReplyOptions
- Log loaded system_configs (background.provider, agent.default_provider)
- Log provider lookup attempts with source and error details
- Warn when falling back to first registered provider
- Surface config load errors instead of silently swallowing them
Helps diagnose provider resolution failures in episodic/vault workers.
Backend:
- Add error tracking to EnrichProgress (error_count, last_error)
- Broadcast error events when LLM calls fail
- Add POST /v1/vault/enrichment/stop endpoint
- Wire enrichWorker to VaultHandler for stop functionality
Web UI:
- Add stop button (appears when enriching)
- Show error toast when enrichment errors occur
- Display error count in progress bar
- Add useStopEnrichment hook
i18n: en/vi/zh translations for new strings
- Increase classify max_tokens 1024→2048, summarize 1536→4096 to
prevent truncated JSON from models like gemini-2.5-pro
- Add debug logging: raw LLM output on parse failures, finish_reason
truncation warnings with model name
- Hot-swap vault enrichment provider/model on config change without
restart (wired into TopicSystemConfigChanged handler)
- Use RWMutex-guarded llm() accessor for thread-safe provider reads
Frontend sent null for empty emoji (emoji.trim() || null), which violated
the NOT NULL constraint on the promoted emoji column (migration 000037).
This caused all agent saves to fail with 500 when emoji was unset.
- Frontend: send empty string instead of null for emoji field
- Backend (PG + SQLite): add null-coercion for all promoted NOT NULL
columns — TEXT (emoji, agent_description, thinking_level) coerce to "",
INT (skill_nudge_interval, max_tokens) coerce to 0
9 fixes for vault enrichment pipeline:
1. Queue key = tenant-only (was per-agent, caused multiple batches
blocking EventBus workers and progress bar flashing)
2. Classify chunks 5 candidates per LLM call (prevents response
truncation that caused parse_still_failed errors)
3. Classify prompt improved: explicit "EXACTLY one entry per
candidate", 5-entry example, ctx capped at 30 words
4. max_tokens kept at 1024 (sufficient for 5 candidates)
5. Progress AddDone removes !running guard (safe before Start)
6. Rescan defers event publishing via PendingEvents — Start()
called before workers receive events, eliminating race
7. Upload handler same deferred publish pattern
8. Frontend enrichment timer cancels stale "complete" timeout
when new enrichment starts (prevents bar disappearing)
9. Sidebar tree reloads after rescan completes
Classify now searches across entire tenant (empty agentID) to
build cross-agent links for future vault sharing. Access control
enforced at query time — agents only see their own docs.