Two fixes:
1. Remove assistant prefill from team task reminders. The injected
[user]+[assistant]+[user] pattern caused LLMs to treat the canned
ack as "turn complete", returning NO_REPLY for every user message
in group sessions with active tasks. Reminders are now merged into
the user message as prefix tags.
2. Add PeerKind propagation to team notification routing. TaskTicker
and progress notifications were missing PeerKind on InboundMessage,
causing them to route to phantom DM sessions instead of the correct
group session. PeerKind is now carried through event payloads,
notify queue metadata, and all inbound message publications.
Extract wake_heartbeat and stateless from JSON payload into first-class
columns on cron_jobs. Adds migration 000033 with backfill from existing
payload data. Updates PG + SQLite stores, RPC handlers, and UI i18n.
Root cause: runLoop() had no recover() — panics killed the goroutine while
leaving the session queue's activeRuns entry orphaned. Subsequent messages
to the same session would hang indefinitely (zombie state).
Fix: Two-layer panic recovery:
1. loop.go: runLoop() now returns error on panic instead of crashing goroutine,
allowing normal cleanup flow (session queue, traces, events)
2. queue.go: executeRun() defense-in-depth recovery ensures activeRuns cleanup
and scheduleNext() even if agent-level recovery fails
CreateProvider now uses ON CONFLICT (tenant_id, name) DO UPDATE instead of
plain INSERT. When a provider with the same name already exists (e.g. orphaned
after agent deletion), it updates the existing record instead of failing with
a unique constraint violation.
Applied to both PG and SQLite implementations.
- manager.go: add resolveEnvVars() to expand env:VARNAME in MCP headers (copy-safe)
- manager_env_test.go: 3 test cases for env resolution
- validate.go: add "name" to channelInstanceAllowedFields
- validate_test.go: test channel instance name field retention
- bus/types.go: add TopicAgentDeleted event + AgentDeletedPayload
- agents_delete.go: emit agent:deleted event with provider name for async cleanup
- gateway.go: log-only subscriber for orphaned provider warning (no auto-delete — FK safety)
- resolver.go: fallback to deps.Workspace for master tenant agents with empty ag.Workspace
(fixes filesystem escape where filepath.Join("","system") resolved to /system)
- sessions_list.go (PG): check rowsAffected after UPDATE, INSERT with ON CONFLICT DO UPDATE
when session not yet in DB (cron/heartbeat sessions)
- sessions_ops.go (SQLite): same UPSERT pattern with ? placeholders
- memory_handlers.go: fallback to X-GoClaw-User-Id header when body.user_id is empty
- loop.go: replace blocking range with select+ctx.Done() for parallel tool collection;
document finalization trade-off on early cancel
- loop_cancel_test.go: tests for context cancellation and normal completion paths
- gateway_consumer_post_turn.go: always auto-complete team task in default case,
fallback message when outcome.Result is nil
- gateway.go: call sched.Stop() with 5s drain before context cancel on shutdown
- anthropic_stream.go: check ctx.Err() in scanner loop, bounds check toolCallJSON index,
accumulate signature_delta events into ThinkingSignature
- anthropic_request.go: include signature field in buildRawBlock for thinking blocks
- types.go: add ThinkingSignature field to ChatResponse
- loop_run.go: nil guard on result before FinishTrace access
- gateway.go: defensive ApplyDBSecrets before setupTTS in config reload subscriber
Add env var support for gateway.allowed_origins so container deployments
can set CORS origins reliably even if config file is overwritten by UI
save cycles. Follows same pattern as GOCLAW_OWNER_IDS.
- Backend: resolve real default agent UUID from DB when cron job has no
agent_id, instead of using literal "default" string that fails lookup
- UI: replace free-text agent input with dropdown in create form
- UI: wrap update params in `patch` object matching backend expectation
- UI: send empty string for agentId to explicitly clear agent selection
- UI: add name attribute to Select elements to fix form field warning
- Convert @mentions to <a href="https://t.me/username"> in HTML formatter
(Telegram Bot API does NOT auto-link @username in parse_mode: HTML)
- Protect @mentions from italic regex mangling underscored usernames
- Change read_image/read_audio/read_video system prompt from "REQUIRED"
to conditional — model only reads media when relevant to user's question
- Annotate history media tags so model distinguishes group context images
from current-turn images
* fix: skip JS ES module imports in Python string literals during dep scan
dep_scanner falsely matched JS `import mermaid from '...'` inside Python
f-strings as a pip dependency, causing functional skills to be archived.
Add jsFromStringRe check: if a line matching pyImportRe also contains
`from '...'`/`from "..."` (JS ES module pattern), skip it. Valid Python
`import X` never has `from '...'` on the same line.
Closes#544
* fix: add missing stub method + additional edge case tests
- Add MigrateUserDataOnMerge to stubAgentStore in context_file_interceptor_test.go
(fixes CI build failure — stub was missing new AgentStore interface method)
- Add TestScanFile_MultipleJSImportsInsidePythonString for broader false positive coverage
---------
Co-authored-by: viettranx <viettranx@gmail.com>
dispatchTaskToAgent() constructed InboundMessage without TenantID,
causing DB-level MCP servers to be invisible when dispatching team
tasks from the dashboard. Same root cause as #560.
All channel handlers (Telegram, Discord, Slack, Feishu) bypassed
BaseChannel.HandleMessage() and manually constructed InboundMessage
without setting TenantID. This caused the consumer to fall back to
MasterTenantID, making DB-level MCP server grants invisible on
non-master tenants while config-file MCP servers worked fine.
Closes#560
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The upload() method in http-client.ts was manually building headers
(only Authorization + User-Id), missing X-GoClaw-Tenant-Id. This caused
custom skills to always be created on the Master tenant. Replace with
authHeaders() which includes all required headers.
Closes#557
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add prepare-compose.sh for composible modules, options and overlays
* feat(prepare-compose): add --quiet and --skip-validation flags
Add loud() function for conditional output. Use --quiet to suppress
normal output, --skip-validation to bypass docker compose config check.
* chore: add mise config to gitignore
* chore: add GOCLAW_DIR env var and update context paths
- Add GOCLAW_DIR to prepare-compose.sh for .env generation
- Fix context paths in docker-compose files to use ${GOCLAW_DIR:-.}
- Restore symlinks in compose.d/ and compose.options/
* fix: broken symlink, safe .env update, export COMPOSE_FILE before validate
- Fix compose.d/00-goclaw.yml symlink: docker-compose.yml → ../docker-compose.yml
- Add compose.d/.gitkeep so directory is tracked
- Export COMPOSE_FILE before running docker compose config validation
- Replace rm+sed .env update pattern with safe temp file approach
- Quote GOCLAW_DIR value in .env to handle paths with spaces
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- fix(ui): combobox dropdown flips above input near viewport bottom using
bottom-anchored positioning so filtered items shrink upward correctly
- fix(ui): add onSelect callback to Combobox for commit-only events,
preventing keystroke-adds-badge bug in multi-select mode
- feat(ui): MultiUserPicker shared component for allow/deny user fields
- fix(ui): migrate channel allow_from textarea, team allowed/denied users,
and config allow_from input to MultiUserPicker with unified search
- fix(ui): remove dead getKnownUsers from use-teams hook
- feat: migrate per-user data on contact merge (context files, overrides,
profiles, memory) with DO NOTHING conflict strategy — canonical tenant
user data always wins over newly merged contact data
Admin users were seeing team task notifications from other channels
(Discord, Telegram, Lark) in their web UI chat due to bypassing the
UserID event filter. Added channel check to handleTeamTaskEvent matching
the existing pattern in handleAgentEvent.
- Enable merge UI for linking channel contacts to tenant_users
- Contact → tenant_user resolution with cached lookup (60s TTL)
- MCP per-user credentials via user-keyed connection pool
- Secure CLI per-user credentials with AES-256-GCM encryption
- Unified UserPickerCombobox searching contacts + tenant_users
- Group contact collection with chat title in all channels
- Group permission inheritance via wildcard user_id="*"
- Fix heartbeat using wrong userID in group chats
- Filter internal senders from contact collection
- Add contact_type column (user/group) to channel_contacts
- SQLite schema v2 migration for desktop edition
- GetGatewayURL/IsGatewayReady: use 127.0.0.1 to avoid WebKit resolving
localhost to ::1 (IPv6) when gateway only binds IPv4, causing "Load failed"
- Skip CORS warning for lite edition (localhost-only, no production exposure)
- Skip server IP detection for lite edition (no multi-tenant risk, avoids
leaking user's public IP in logs and unnecessary network calls)
Backend enables pruning by default (only skips when mode="off"), but
UI showed toggle OFF when no config was saved (null). Also fixed global
config dropdown defaulting to "off" instead of "auto".
Add provider-scoped runtime monitor for Codex pool owners:
- New `GET /v1/providers/{id}/codex-pool-activity` endpoint aggregates pool health across all agents
- New Pool Activity section on provider detail page (pool owners only)
- Shows aggregate member health, recent requests, top agents with drill-down links
- 7-day time window on provider-scoped span query for performance
- Reuses `buildCodexPoolActivity()` — zero duplicated aggregation logic
- i18n complete (en/vi/zh), accessible markup
Closes#499
- recompute next_run_at transactionally on toggle/update instead of non-atomic read/compute/write
- keep repeated EnableJob(true) idempotent by preserving existing next_run_at
- reject re-enabling expired one-shot at jobs with ErrCronJobNoFutureRun
- claim due jobs conditionally, reload before execution, conditional writeback
- align PG and SQLite behavior with shared helpers (DRY)
- atomic scheduler writeback via single UPDATE with CASE expression
Closes#378
- Replace ILIKE search with PostgreSQL tsvector/GIN full-text search
for KG entities (migration 000031)
- Add entity deduplication system with dual-threshold strategy:
auto-merge at 0.98+ similarity with Jaro-Winkler name match,
flag 0.90-0.98 as candidates for manual review
- Add ScanDuplicates for bulk on-demand duplicate detection
- Add MergeEntities with advisory lock and tenant-scoped relation
re-pointing (delete-then-update to avoid ON CONFLICT on UPDATE)
- Wire dedup inline after KG extraction pipeline
- Fix BackfillKGEmbeddings: was failing silently due to
context.Background() missing tenant_id; now runs cross-tenant
- Fix BackfillKGEmbeddings: break on error → continue with failed
ID tracking and max consecutive error cap
- Add EmbedEntity helper; UpsertEntity now generates embeddings
in background goroutine
- Add HTTP endpoints: POST /kg/dedup/scan, GET /kg/dedup,
POST /kg/merge, POST /kg/dedup/dismiss
- Add web UI: Dedup dialog with Scan All button, side-by-side
entity comparison, merge/dismiss actions
- Add Jaro-Winkler similarity algorithm with 34 unit tests
- Update IngestExtraction to return upserted entity IDs
- Bump RequiredSchemaVersion to 31
Sessions page shows raw IDs instead of sender names because these channels
didn't include display_name in their message metadata. The gateway's
extractSessionMetadata() already looks for this key but it was missing
from Feishu, Zalo Personal, and Slack message payloads.
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
DRY: replace hardcoded Novita API base/model across 3 files with
store.NovitaDefaultAPIBase and store.NovitaDefaultModel constants.
Add Novita to desktop frontend provider list.
Add Novita AI as a new LLM provider with OpenAI-compatible API endpoint.
- Add ProviderNovita constant and valid type entry
- Add Novita to ProvidersConfig with APIBaseForType and HasAnyProvider
- Register Novita provider in gateway startup (config and DB paths)
- Add Novita case in HTTP handler's registerInMemory
- Add Novita to UI provider selection dropdown
Default model: moonshotai/kimi-k2.5
Default endpoint: https://api.novita.ai/openai
Message count-based compaction trigger (default 200) was a legacy safety
net from when token estimation was unreliable. With calibrated overhead
estimation using actual LLM PromptTokens, the token threshold alone is
sufficient — matching industry standard (Claude Code, Anthropic API,
LangChain all use token-only).
Also fixes EstimateTokens vs EstimateHistoryTokens inconsistency in
end-of-run overhead calculation (loop_history.go), and updates stale
CLAUDE.md docs from 75% to 85%.
Management endpoints (MCP, Agents, Skills, Channels, Builtin Tools)
previously used auto-detect auth which allowed Operator-level users
to perform CRUD operations. Now consistent with WS policy: Admin+
required for writes, Viewer+ for reads.
Also moves WS skills.update to isAdminMethod and improves 403 error
messages to include required role info.
Root cause: compaction threshold compared total prompt tokens (including
system prompt, tool definitions, context files) against budget. Sessions
with only 50-60k of conversation could trigger compaction when overhead
pushed total to 120k+.
Changes:
- Enable context pruning by default for all providers (was Anthropic-only)
- Add EstimateHistoryTokens() that excludes system messages from count
- Fix EstimateTokens() to include ToolCall argument sizes
- Calibrate overhead on first LLM response, subtract from threshold checks
- Mid-loop: prune old tool results first (at 70% budget), compact only if
still over budget after pruning
- maybeSummarize: subtract overhead from calibration base
- Remove applyContextPruningDefaults() (Anthropic-only auto-enable)
- Simplify context pruning UI: show only Keep Last Assistants by default,
hide advanced params behind collapsible, remove Mode selector
LLMs often send empty strings, "null", "SHOULD_NOT_BE_HERE" etc. for optional
MCP tool parameters instead of omitting them, causing servers to reject invalid
values (e.g. UUID validation fails on "").
- Add stripEmptyOptionalArgs() to clean optional args before MCP calls
- Detect known placeholders (null/none/nil) and all-caps patterns
- Add system prompt guidance for optional parameter handling
- Empty string "" is intentionally NOT stripped (may be valid for text fields)
When MCP servers update their tools externally, the gateway keeps
stale cached connections. Add POST /v1/mcp/servers/{id}/reconnect
endpoint that evicts the pooled connection, forcing re-discovery
on next agent use. Exposed via reconnect button on both web and
desktop MCP management pages.
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.