Heartbeat's ProviderID was stored in DB but never used during execution —
the agent's default provider always won. Add ProviderOverride to RunRequest,
resolve hb.ProviderID in the ticker, and use the override in the agent loop.
Session keys must use agentKey (human-readable, e.g. "default") not UUID.
Cache invalidation via InvalidateAgent(agentKey) only matches agentKey suffix,
so UUID-keyed entries were never cleared — causing stale provider/model after
agent updates from the web UI.
- heartbeat: use agentKey (already resolved) instead of UUID for session key
and HasActiveSessionsForAgent check
- cron: resolve agentKey from UUID via agentStore before building session key
- agents_update: also invalidate by UUID as safety net for legacy entries
- startup migration: auto-fix existing UUID-based session keys in DB
- tests: 7 new tests covering session key format and cache invalidation
- session_status: GetOrCreate → Get to prevent phantom session creation
- sessions_history: replace string concat with json.Marshal (2 paths) to prevent JSON injection
- sessions_send: use s.Label from List() directly (eliminates N+1 Get calls), add self-send protection, use json.Marshal for response
- All tools: fail-closed agentID check (error when empty instead of skipping validation)
- Add 22 unit tests covering all session tools
When sending the first message (no session key), handleSend calls
navigate() then send(). The navigation triggers a session-change effect
that resets expectingRunRef=false before run.started arrives, causing
all streaming events to be silently dropped.
Only reset expectingRunRef when switching between existing sessions,
not on the "" → new-key transition that is part of the send flow.
When a team member creates an image and completes a task, the announce
flow already carries media to the leader agent, which dispatches it to
the channel via ForwardMedia. If the leader also calls the message tool
to send the same image to its own chat, the image appears twice.
Block message tool calls where channel+target match the agent's own
context, returning an error that guides the LLM to include content in
its normal response instead.
Read-only streak detector was a dumb counter that killed agents exploring
unique files (12 reads = kill regardless of uniqueness). Team tasks were
then falsely auto-completed with the kill message as "result."
Changes:
- Add uniqueness ratio to read-only streak: exploration mode (>60% unique)
uses relaxed thresholds (warn=24, kill=36) vs stuck mode (warn=8, kill=12)
- Classify team_tasks by action: list/get/search=read-only, progress=neutral,
create/complete/cancel/comment/etc.=mutating
- Add LoopKilled flag to RunResult, propagated through all 3 kill paths
- Consumer routes LoopKilled to FailTask (not CompleteTask) with clear signal
- 40 tests covering uniqueness detection, team_tasks classification,
exact #506 trace replay, boundary values, and edge cases
File downloads in web chat returned 401 when loading session history
because MediaRefs paths were not signed with ?ft= HMAC tokens.
- Add SignMediaPath() to clean legacy corrupted paths (stacked
/v1/files/ prefixes, stale ?ft= tokens) and produce fresh signed URLs
- Sign MediaRefs in chat.history and sessions.get WS handlers
- Strip ?ft= from filename display in frontend
- Add path traversal defense-in-depth check
- Add unit tests for SignMediaPath legacy data healing
Closes#519
Sandbox overlay's cap_add replaces (not merges) the base compose, dropping
SETUID, SETGID, CHOWN. This causes credential copy to fail with Permission
denied when combining sandbox + claude-cli overlays.
Changes:
- Re-include base capabilities in sandbox overlay's cap_add
- Use umask 077 for atomic permission-safe credential copy
- Add ENABLE_CLAUDE_CLI build arg to pre-install Claude CLI in image
- Add runtime warning when credentials mounted but CLI binary missing
- Add WITH_CLAUDE_CLI to Makefile for overlay consistency
- Add security warning comment for sandbox overlay attack surface
KG traversal only followed outgoing edges, returning empty results for
entities with only incoming relations (e.g. Kuwait had 1 incoming but 0
outgoing). Add 3-tier fallback: (1) outgoing deep traversal, (2) direct
connections via ListRelations (bidirectional, cap 10), (3) search query
fallback. Also cap traversal results at 20 and search relations at 5 per
entity with hints for the model to query further.
About tab:
- Show clickable database path that opens data folder
- Add "Reset Database" button with RESET confirmation modal
- Reset deletes DB files and restarts app via RestartApp()
- Add GetDataDir/ResetDatabase Wails bindings
Onboarding i18n:
- Replace all hardcoded English strings in ProviderStep,
ModelVerifyStep, and AgentStep with t() calls
- Add missing i18n keys to en/vi/zh common.json and desktop.json
- Fix model Combobox permanently disabled on reopen: load models for
existing chain entries once providers list is fetched
- Add drag-and-drop reordering via @dnd-kit/sortable (matching web UI)
- Use stable UUIDs as React keys instead of array indices
- Show model display names (e.g. "Gemini 2.0 Flash") instead of raw IDs
- Disable Save button when any entry is missing provider or model
- Extract MediaProviderChainForm into its own file
Root cause: pool.go applied busy_timeout PRAGMA via db.Exec() which only
affects one connection in the pool. Other connections had no busy_timeout,
causing immediate SQLITE_BUSY errors during concurrent startup operations
(agent creation, WebSocket connect, health checks). This silently aborted
context file seeding — BOOTSTRAP.md, USER.md, AGENTS.md all missing from
system prompt on first interaction.
Fix (3 layers):
1. pragmaConnector: wraps sql.Driver to apply PRAGMAs (busy_timeout, WAL,
etc.) on every new connection, not just one. All SQLite queries benefit.
2. CacheInvalidateFunc: clears ContextFileInterceptor cache after seeding
so LoadContextFiles sees newly seeded files on the first turn.
3. fallbackBootstrap: if DB seed still fails, injects embedded templates
in-memory so the first turn still gets onboarding. Clears after use.
- Guard uuid.Nil before converting to string in loop.go to avoid
sending zero UUID as X-Tenant-ID header when no tenant is set
- Return (ok, tenantVerified) from VerifyBridgeContext so callers
only inject tenant_id when HMAC actually covers it (level 1)
- Add unit tests for SignBridgeContext and VerifyBridgeContext
including security test for tenant injection via HMAC fallback
Problem:
Memory documents saved via write_file → MemoryInterceptor are stored
in memory_documents (PutDocument succeeds via fail-open tenant fallback),
but IndexDocument fails with "tenant_id required" because the MCP bridge
chain never propagates tenant_id. This results in 0 memory_chunks,
making memory_search return empty for all agents using claude_cli provider.
Root cause:
- BridgeContext struct lacks TenantID field
- writeMCPConfigInternal never sets X-Tenant-ID header
- bridgeContextMiddleware never extracts/injects tenant_id into context
- PutDocument uses tenantIDForInsert() (fail-open, defaults to master)
- IndexDocument uses scopeClause() → requireTenantID() (fail-closed)
Fix:
- Add OptTenantID constant and BridgeContext.TenantID field
- Pass tenant_id from agent loop context → provider opts → MCP headers
- Add X-Tenant-ID to bridge request headers (HMAC-signed)
- Extract X-Tenant-ID in bridgeContextMiddleware → store.WithTenantID()
- Backward-compatible HMAC verification with 3 fallback levels
(with tenant, without tenant, without workspace+tenant)
Affected: all agents using claude_cli provider type.
Also fixes: "KG embeddings backfill failed: tenant_id required"
Co-authored-by: admin <admin@admins-MacBook-Pro.local>
Compaction was triggering too early because prompt tokens include
system prompt + context files + tools, not just history. Raising
the threshold reduces unnecessary compaction cycles.
Team members dispatched via team tasks can now read the leader's memory
files (MEMORY.md, memory/*.md) as a fallback when they have no own
memory. Memory writes are blocked for members — only the leader can
save memory files. This follows the existing team_workspace context
propagation pattern: dispatch metadata → RunRequest → context → tools.
Affected tools: read_file (interceptor), memory_get, memory_search,
list_files (interceptor). All include leader fallback with per-user →
global scope cascade.
Extract TeamToolBackend interface from concrete *TeamToolManager to enable
unit testing of the 17 team task action handlers without DB or message bus.
- Add TeamToolBackend interface (19 methods) in team_tool_backend.go
- Add exported wrappers on TeamToolManager (cache, helpers, dispatch, manager)
- Migrate 133 call sites across 5 action handler files (pure casing rename)
- Change TeamTasksTool.manager field from *TeamToolManager to TeamToolBackend
- Add mock backend infrastructure (baseNoopTeamStore + mockTaskStore + mockBackend)
- Add 94 unit subtests across 6 test files covering all actions:
lifecycle (claim/complete/cancel/review/approve/reject),
create validation, blocker escalation, mutations (comment/progress/attach/update),
followup (ask_user/clear_ask_user/retry), read actions (notification/policy/list/search/get)
WorkspaceInterceptor and PostTurnProcessor remain on concrete *TeamToolManager.
No behavioral changes — all existing logic preserved.
When adding a CLI credential with "Manual (no preset)" mode, the env var
input section was hidden because it only rendered when a preset was
selected. Add a dynamic key-value form allowing users to define custom
environment variables for non-preset CLI tools.
Closes#477
- Bump DefaultAPIVersion from 665 to 671 to match current Zalo web client
- Batch getmg-v2 requests (50 groups/batch) — Zalo rejects large payloads,
causing error 114 for accounts with many groups (e.g. 250+)
- Fix encryptType=1 WebSocket event decryption: add gzip decompression
after base64 decode (matches zca-js pako.inflate behavior)
- Split contacts picker into Friends/Groups tabs for better usability
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
- Add filteredToolNamesForChannel() so system prompt Tooling section
excludes channel-specific tools for wrong channels (e.g. list_group_members
hidden for Telegram, create_forum_topic hidden for Feishu)
- Add ChannelAware interface to CreateForumTopicTool (Telegram-only)
- Add coreToolSummaries for list_group_members and create_forum_topic
(were showing as "(custom tool)")
- Inject group chat title into system prompt identity line
(e.g. "telegram (group chat \"My Group\")" instead of "a group chat")
- Sanitize ChatTitle to prevent prompt injection (strip quotes/newlines,
truncate to 100 chars)
All Telegram bot commands (/writers, /addwriter, /removewriter, /reset,
/tasks, /task_detail) and callback queries were missing tenant_id in
context, causing "tenant_id required" errors from scopeClause() or
silent fallback to MasterTenantID breaking multi-tenant isolation.
SQLite pairing:
- Fix rows.Scan failing on Go time.Time strings with monotonic suffix
- Scan timestamps as strings, parse with parseTimeToMillis() that strips m=+
- Use time.Now().Round(0) for all SQLite time comparisons
- Wire SetOnRequest for SQLite store via interface assertion (not PG-only)
Desktop UI:
- Fix ChannelManagersTab i18n keys: managers.xxx → detail.managers.xxx
- Fix ChannelAdvancedDialog i18n keys and save logic (wrap in config)
- Convert Advanced from modal dialog to inline tab (4th tab)
- Add "advanced" tab label to all 3 locale files
SetOnRequest was only wired for PGPairingStore via concrete type assertion.
Use interface-based assertion so SQLitePairingStore also gets the callback.
This fixes desktop app not receiving real-time pairing request notifications.
- Create response_helpers.go with writeError(w, status, code, msg) for structured errors
- Move writeJSON to response_helpers.go for co-location
- Migrate agents.go, channel_instances.go, api_keys.go to writeError with protocol error codes
- Replace raw err.Error() with i18n messages + slog.Error for debugging
- Fix channel creation: map camelCase input to snake_case for backend HTTP API
- Fix PairedDevicesSection reload button: use RefreshButton component
Channel Management:
- Add Channels settings tab with full CRUD for Telegram/Discord (Lite: max 1 each)
- Channel detail panel with tabs: General, Credentials, Managers
- Advanced settings dialog (network, limits, streaming, behavior, access control)
- Schema-driven field renderer with Combobox selects, Switch toggles
- Paired Devices section: approve/deny pending, revoke paired, WS event auto-refresh
- Pairing notification badge in sidebar footer with pending count
- i18n support (en/vi/zh) for all channel strings
Bug Fixes:
- Fix Vietnamese slug generation (NFD normalize + đ/Đ handling) in lib/slug.ts
- Make agent key field editable instead of read-only
- Fix trace detail input/output: use MarkdownRenderer with copy button instead of forced-dark CodePreview
- Fix API error parsing to handle both {error: "string"} and {error: {message}} formats
- Add Accept-Language header to desktop API client for i18n error messages
- Wrap raw err.Error() with i18n messages in HTTP handlers (agents, channel_instances)
- Increase SQLite MaxOpenConns from 2 to 4 to reduce SQLITE_BUSY contention
- Add retryOnBusy wrapper for context file seeding writes
- Update EditionCompareModal: channels moved from "false" to "1 Telegram + 1 Discord"
When group_stream=false (default), channels implementing StreamingChannel
still processed chunk events, creating stream messages alongside block
replies — causing each intermediate message to appear twice.
Gate the streaming handler on rc.Streaming so it only activates when
streaming is actually enabled for the run.
- Split EnsureUserFilesFunc into EnsureUserProfileFunc (profile + workspace)
and SeedUserFilesFunc (context file seeding) for single-responsibility
- Merge userWorkspaces + userFilesSeeded sync.Maps into unified userSetups
struct to prevent desync between workspace and seeding state
- Add skipIfAnyExist param to SeedUserFiles to encapsulate the
"seed only for brand-new users" logic within the bootstrap package
- Extract getOrCreateUserSetup helper for clean per-user initialization
- Add bootstrap state tests covering all 4 system prompt branches
- Keep legacy EnsureUserFilesFunc as fallback for backward compatibility
- Separate file seeding from workspace resolution so agents without
workspace still get BOOTSTRAP.md and USER.md seeded
- Always seed context files for existing profiles that have zero files
(handles EnsureUserProfile pre-creation via HTTP API)
- Add persistent "USER PROFILE INCOMPLETE" nudge in system prompt when
BOOTSTRAP.md is cleaned up but USER.md remains blank
- Move bootstrap auto-cleanup nudge before session flush so the reminder
is persisted to history
- Add userFilesSeeded sync.Map to avoid redundant seeding calls
- Capture workspace from seeding call to eliminate double DB roundtrip
- Remove USER_PREDEFINED.md from summoning file list (web + desktop)
- Soften canvas-dots pattern (smaller dots, wider spacing)
- Require at least 1 member when creating a team
- Prevent removing the last non-lead member from team
Browse, upload, download, move, and delete workspace files from the
desktop app. File tree with drag-and-drop move (@dnd-kit/core), syntax
highlighting (react-syntax-highlighter), markdown/CSV/image viewers,
upload dialog with drag-drop + validation, and SSE storage size streaming.
The case expression `taskActionFlags.Progressed || ...` always evaluates
to `true/false`, never matching the switch value. Merge it into the
default branch so non-terminal actions consistently auto-complete.
PR #511 removed WithMediaImages context in file-ref mode, breaking
read_image when LLM omits the path param. Restore base64 context as
fallback (costs Go memory, not LLM tokens). Also improve error messages
to distinguish missing provider config vs provider failures.
The Makefile `net` target creates goclaw-net via `docker network create`
without the `com.docker.compose.network` label. Docker Compose then rejects
the network on macOS Docker Desktop. Compose already manages this network
automatically with correct labels.
Closes#488
* fix(vision): file-ref mode + media pipeline fixes for image visibility
Replace inline base64 image loading with file path references when
read_image provider is configured. LLM calls read_image(path=...)
instead of receiving 25K-250K tokens of base64 per image.
Changes:
- Agent loop: skip base64 context storage in file-ref mode, only
load historical images for inline fallback
- New enrichImagePaths() enriches ALL user messages with file paths
(not just current turn) so historical images are accessible
- System prompt: imperative tool summaries ("REQUIRED when you see
<media:image> tags") + dedicated "Media Files" section
- Slack: store mediaPaths in pending history + CollectMedia on mention
- Feishu: add CollectMedia for group context history media
- RC-1 fix: save enriched content (with media IDs/paths) to DB
instead of raw req.Message
Closes#509
* fix(vision): resolve Claude review — Feishu early media + skip loadImages in file-ref mode
- Feishu: download media BEFORE mention gate (step 4) and store file
paths in pending history via Media field. Reuse early-resolved
MediaInfo at step 10 to avoid double-download. CollectMedia now
returns actual paths instead of empty.
- Agent loop: guard loadImages() behind !deferToReadImageTool so
file-ref mode avoids unnecessary disk I/O + base64 encoding.