* 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(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>
Backend — WSClient protocol fixes (larkws.go):
- Parse service_id from WS URL query params instead of hardcoding 0
- Update all 4 server config values from pong payload (PingInterval,
ReconnectCount, ReconnectInterval, ReconnectNonce)
- Use server-configured reconnect params instead of hardcoded 120s wait
- Return HTTP 500 in ACK when event handler fails (enables Lark retry)
- Filter data frames by type header — only process "event" frames
- Report actual processing time in biz_rt header (was hardcoded "0")
Backend — event adapter (feishu.go):
- Return parse error from HandleEvent so ACK reflects failure status
UI — fix incorrect Feishu channel form labels:
- Remove "webhook only" from Lark Global domain label (WebSocket works
on both Lark Global and Feishu China)
- Remove "Feishu only" from WebSocket option label
- Change default connection_mode from "webhook" to "websocket" (matches
backend default)
- Add showWhen conditional field support to ChannelFields component
- Hide webhook_port, webhook_path, encrypt_key, verification_token when
WebSocket mode is selected
- Update i18n labels in all 3 locales (en, vi, zh)
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
- Skip FinalizeStream on tool.call — keep streamed message visible
- Gate tool_status placeholder_update to non-streaming runs only
- Prevents streamed text from being overwritten by tool status emoji
Cherry-picked valuable changes from PR #206:
- hasReadImageProvider supports chain format {"providers":[...]} config
- create_image/video/audio verify file persistence after write with diagnostic logging
- HistoryEntry gains Media field + CollectMedia() for group media context on @mention
- Zalo extractContentAndMedia refactored: all media types via DetectMIMEType/BuildMediaTags, 20MB limit
- Discord/Zalo pass media paths to Record() and collect historical media on @mention
- Zalo send_helpers logs directory contents when checkFileSize stat fails
Telegram:
- Fix context cancellation in reaction timer callbacks — use context.Background()
so emoji reactions still work after request context is cancelled
- Add comma-ok safety on sync.Map type assertions in OnReactionEvent/ClearReaction
Zalo:
- Add comma-ok safety on sync.Map type assertions for typing controllers
- Validate non-empty senderID before processing text/image messages
Feishu/Lark:
- Handle json.Unmarshal errors in all 7 LarkClient messaging methods instead of
silently returning empty values
- Add 30s HTTP timeout for WebSocket endpoint request (was using DefaultClient)
- Eliminate double decryption in webhook handler — cache first result
- Replace goroutine+sleep with time.AfterFunc for dedup cleanup
- Add comma-ok safety on pairing debounce type assertion
- Use json.Marshal instead of fmt.Sprintf for image/file key JSON construction
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
* fix(telegram): thread transport policy into media downloads with SSRF guard
* fix(telegram): trust configured APIServer during media downloads
* fix(telegram): use proper download timeout and clone DefaultTransport
- Clone http.DefaultTransport when proxy is configured to preserve
connection pool, TLS handshake timeout, and keep-alive defaults
- Use dedicated 5-minute context timeout for media downloads instead
of the shared 30s client timeout, preventing large file timeouts
(local Bot API supports up to 200 MB)
---------
Co-authored-by: viettranx <viettranx@gmail.com>
* feat(telegram): implement robust message splitting and dynamic HTML retry logic
* fix(telegram): fix sendHTML error chain regression and add split depth limit
- Re-check err.Error() in thread-not-found handler instead of stale errStr,
restoring the original chained fallback behavior
- Add maxSplitDepth (5) to prevent unbounded recursion when Telegram
repeatedly rejects split chunks
- Rename misleading test case to reflect actual monolithic fallback behavior
---------
Co-authored-by: viettranx <viettranx@gmail.com>
sendMessageDraft causes "reply to deleted message" artifacts on some
Telegram clients (tdesktop#10315). Disable by default so content
streaming uses message transport (same as reasoning stream).
- Change WS pairing check from fail-open to fail-closed on DB error
(router.go: previously granted RoleOperator on any IsPaired() error)
- Add "browser" to InternalChannels so it's properly excluded from
outbound dispatch without ad-hoc helpers
- Rate-limit browser.pairing.status endpoint to prevent sender_id
enumeration (reuses server RateLimiter via PairingMethods injection)
- Add expires_at column to paired_devices with 30-day TTL for
defense-in-depth; IsPaired() now checks expiry, ListPaired() prunes
- Add confidence_score column to team_tasks, team_messages,
team_task_comments
- Bump RequiredSchemaVersion to 21
- Move task dispatch from mid-turn to post-turn to prevent dependent
tasks from completing before the current agent's run finishes
- Add team create lock to serialize list→create flows across concurrent
group chat sessions, preventing duplicate task creation
- Require list-before-create gate: agents must call team_tasks(list)
before creating tasks
- Make assignee required on task creation
- Add pagination (50 per page) to task list with offset support
- Slim task list/get/search responses with dedicated structs to reduce
context token usage
- Add task board snapshot in announce messages to leader
- Workspace: allow subdirectory paths in read/delete, show directories
in list output
- UI: reduce kanban card title font size for better visual balance
- Add terminal-state check in executeCreate(): reject blocked_by
referencing completed/cancelled/failed tasks with actionable error
- Add full validation in executeUpdate(): batch query via GetTasksByIDs,
check existence + team membership + terminal state
- Add GetTasksByIDs batch query to TeamStore interface + pg implementation
- Refactor: modularize gateway, skills store, and team tools into
focused files
- Update TEAM.md leader prompt: prefer delegation, plan full task graph
upfront, create tasks in order with blocked_by UUIDs
Replace all hardcoded ~/.goclaw path constructions with configurable
sources (cfg.ResolvedDataDir() for service dirs, cfg.Agents.Defaults.Workspace
for agent workspaces). This fixes data persistence issues in Docker
deployments where paths differ from local dev.
- Add DataDir field to Config with ResolvedDataDir() resolver
- Add ResolvedDataDirFromEnv() package-level helper for packages without Config
- Populate StoreConfig.SkillsStorageDir (was never set, caused hardcoded fallback)
- Agent workspaces now use subdirectory format (workspace/{key}) for volume compatibility
- Remove dead GOCLAW_SESSIONS_STORAGE env/config (sessions moved to PostgreSQL)
- Fix deploy-stg.sh trailing space after backslash + remove deprecated GOCLAW_MODE
- Add GOCLAW_SKILLS_DIR override in docker-compose for volume persistence
Major refactoring of the team system with multiple improvements:
## Removed legacy delegation tools
- Delete `delegate.go`, `delegate_async.go`, `delegate_sync.go`, `delegate_events.go`,
`delegate_policy.go`, `delegate_prep.go`, `delegate_state.go`, `delegate_search_tool.go`
- Delete `evaluate_loop_tool.go`, `handoff_tool.go`
- Remove all references and registrations from tool manager and policy
- Clean up TEAM_PLAYBOOK_IDEAS.md and TEAM_SYSTEM.md (moved to docs)
## Rename await_reply → ask_user
- Rename action `await_reply` → `ask_user`, `clear_followup` → `clear_ask_user`
- Rename functions `executeAwaitReply` → `executeAskUser`, `executeClearFollowup` → `executeClearAskUser`
- Update system prompt with stronger wording to prevent model misuse
- Model was confusing "await_reply" with general waiting; "ask_user" is unambiguous
## Fix auto-followup false positives
- Add `HasActiveMemberTasks(ctx, teamID, excludeAgentID)` store method
- Guard `autoSetFollowup()` in consumer: skip when lead has active member tasks
- Prevents auto-followup when lead is orchestrating teammates (not waiting for user)
## Task identifier zero-padding
- Change format from `T-1-xxxx` → `T-001-xxxx` (3-digit minimum)
## Refactor workspace WS handlers to filesystem-only
- Rewrite `teams.workspace.list/read/delete` to use pure filesystem (os.ReadDir/ReadFile/Remove)
- Remove DB dependency from workspace WS handlers
- Consistent with storage handler and workspace tools
- Simplify TeamWorkspaceFile type and frontend hook
## Add team events listing API
- New WS method `teams.events.list` with team_id, limit, offset params
- New HTTP endpoint `GET /v1/teams/{id}/events` with bearer auth
- New `ListTeamEvents(ctx, teamID, limit, offset)` store method
- JOIN with team_tasks for team-wide event filtering
## Extract team access policy
- New `team_access_policy.go` — centralized team tool access control
## Migration 000019: team_id columns
- Add team_id foreign key columns to relevant tables
## Other improvements
- Add team_id propagation through agent loop, tracing, sessions
- Update i18n locale files (en/vi/zh) for new tool labels
- Update frontend builtin-tools page and require-setup component
- Bump RequiredSchemaVersion for migration 000019
Add complete i18n support for channel config tab — field labels, help text,
and select option translations for en/vi/zh. Enable DM streaming by default
for Telegram and Slack channels.
* feat(workspace): add team shared workspace for file collaboration
- Add workspace_write and workspace_read tools for agents to share files across team members
- Create team_workspaces DB table with migration 000017 (file metadata, pinning, tags)
- Implement PostgreSQL store layer for workspace CRUD operations
- Add RPC handlers for workspace list/read/delete from web UI
- Build React workspace tab with file listing, content preview, and delete
- Propagate workspace channel/chatID scope through delegation chain
- Auto-allow workspace tools in agent tool policy when agent belongs to a team
- Inject team workspace guidance into system prompt for team agents
- Add /reset command handler for clearing session history
- Harden MCP bridge context middleware to reject headers when no gateway token
- Add i18n strings for workspace UI in en/vi/zh locales
* feat(teams): add comprehensive task management with followup reminders and recovery
- Add task followup/reminder system with auto-set on lead agent reply and auto-clear when user responds on channel
- Add task recovery ticker to re-dispatch stale/pending tasks periodically
- Add task scopes, filtering by status/channel/chatID, and task events
- Add WS RPC handlers for task CRUD, assignments, comments, events, and bulk operations (teams_tasks.go)
- Add task detail dialog, settings UI for followup config, and scope filtering in web dashboard
- Add migrations 000018 (team_tasks_v2) and 000019 (task_followup)
- Extend team_tasks_tool with await_reply, clear_followup actions
- Auto-complete/fail team tasks when delegate agent finishes
- Add workspace file listing and team tool manager enhancements
* docs(teams): add team system architecture and playbook ideas documentation
- Add TEAM_SYSTEM.md with full architecture design covering task management, shared workspace, and delegation engine subsystems
- Add TEAM_PLAYBOOK_IDEAS.md outlining future team coordination layers (playbook, member capabilities, auto-learned patterns)
- Document data models, status flows, tool actions, followup reminder system, task ticker, execution locking, and workspace scope model
* fix(teams): resolve 6 critical bugs in team task system
- Fix unblock SQL: check array_length after array_remove (not before)
- Enforce single-team leadership in team creation
- Add requireLead() for approve/reject tool actions
- Validate cross-team dependency references in blocked_by
- Add team_id to handoff route for multi-team isolation
- Set blocked_by DEFAULT '{}' to prevent NULL array issues
* refactor(workspace): use stable userID as scope key instead of connection UUID
Workspace scope changed from (team_id, channel, chat_id) to (team_id, userID).
Fixes workspace fragmentation across WS tab refreshes and reconnections.
* feat(teams): add V1/V2 versioning with feature gating and optimized prompts
- IsTeamV2() helper gates advanced features (locking, followup, review, audit)
- V2 tool actions rejected for V1 teams with clear error message
- Ticker, gateway consumer, delegation hooks respect version flag
- TEAM.md renders v1/v2 sections conditionally
- Tool descriptions and params optimized (~38% token reduction)
- UI: version toggle in settings, V2 Beta badge, conditional rendering
- i18n: version modal keys for en/vi/zh
* fix(migration): use VARCHAR(255) for user ID columns and add metadata JSONB
- assignee_user_id, user_id, actor_id: TEXT → VARCHAR(255)
- Add metadata JSONB to team_task_comments and team_task_attachments
---------
Co-authored-by: Nam Nguyen Ngoc <namnn.0911@gmail.com>
IsPaired() silently returned false on DB query failures (connection
timeout, pool exhaustion), causing the system to treat already-paired
users/groups as unpaired and create spurious pairing requests.
Changed IsPaired signature to return (bool, error). All 13 callers
across all channels (Telegram, Discord, Slack, Feishu, WhatsApp, Zalo)
and gateway (browser WS, pairing method) now fail-open on DB errors:
log a warning and assume paired, avoiding false rejections.
BuildContext() and GetEntries() only read from RAM, so after gateway
restart or LRU eviction the agent never saw pending group messages
despite them existing in the database. Add lazy loadFromDB() that
queries ListByKey on cache miss and populates RAM for subsequent calls.
* feat(telegram): support custom Bot API server URL
Add api_server credential field for Telegram channels to allow
using a self-hosted Telegram Bot API server (e.g. for 2GB upload
limits). Uses telego.WithAPIServer() option.
* feat(telegram): local Bot API media download + config improvements
- Support local Bot API --local mode: detect absolute file paths and
copy directly from filesystem (requires shared volume mount)
- Fall back to HTTP download via custom API server URL for non-local mode
- Move api_server/proxy from credentials to config (non-secret fields);
credentials still accepted for backward compat
- Add media_max_mb config field (friendlier than media_max_bytes);
deprecated media_max_bytes still works
- Default to 200 MB max when local Bot API is configured (vs 20 MB cloud)
- UI: move api_server/proxy to config section, use MB for media size
* fix: use 127.0.0.1 instead of localhost in API server placeholder
Docker containers cannot resolve localhost to the host machine.
127.0.0.1 works reliably with Docker's default bridge network.
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
- A1+C2: Include token usage in run.completed event payload for WS clients
- A2: Cost tracking with model pricing config, cost calculation, and cost summary API
- A3: Budget enforcement per agent with monthly budget limits (migration 000015)
- C1: External wake/trigger API (POST /v1/agents/{id}/wake) for orchestrators
- C3: Activity audit trail with structured logging and queryable API
- UI: Activity page, cost stat card on overview, budget section in agent detail
- i18n: Complete en/vi/zh translations for all new features
Zalo CDN photo URLs are auth-restricted and expire quickly. Download
images to local temp files before passing to the agent, falling back
to the raw URL on failure. Also adds PhotoURL field support.
Credentials Update() now handles []byte type in the switch case,
preventing incorrect encryption when credentials arrive as raw bytes.
MaybeCompact relied on RAM count which resets to 0 on restart (LoadFromDB
is a no-op). Messages accumulated in DB but never triggered compaction.
Now falls back to CountByKey DB query when RAM count is below threshold.
* fix(channels): annotate DM messages with sender identity
Telegram and Zalo group messages already include [From: sender] prefix
so the agent knows who is talking, but DM messages were sent without
any sender context — the agent had no way to address the user by name.
- Telegram DM: add [From: @username] (or FirstName if no username)
- Zalo DM: add [From: displayName] when dName is present in payload
* fix(tests): add missing EnsureUserProfile to test stubs
AgentStore interface gained EnsureUserProfile in 4fce731 but the test
stub implementations were not updated, breaking CI on main.
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Pass contactCollector through channel manager to all channel handlers
(Telegram, Discord, Feishu, Slack, Zalo) so contacts are automatically
collected when users interact with the agent.
- Defer Telegram media download until after mention gate — pending
history now uses lightweight tags (no download) saving bandwidth
and avoiding errors on large files
- Fix compaction status query: has_summary now false when new messages
arrive after last compaction, re-enabling the compact button
- Fix HTTP compact endpoint: detach context from request so LLM
summarization isn't cancelled when browser closes connection
Add Provider, Model, MaxTokens to PendingCompactionConfig so users can
override the LLM used for pending message summarization via the config
UI. Falls back to agent's provider/model when not set. Increase default
max_tokens from 512 to 4096. Add allowEmpty prop to ProviderModelSelect
to prevent auto-selecting first provider when empty means "use default".
- Fix compact endpoint using random provider instead of agent's configured provider+model
- Wire auto-compaction for all 5 channel types (telegram, discord, slack, feishu, zalo_personal)
via PendingCompactable interface and InstanceLoader
- Add global PendingCompactionConfig (threshold, keep_recent) to ChannelsConfig
- Wire global config through InstanceLoader and PendingMessagesHandler
- Increase compaction timeout from 45s to 180s for slow providers
- Add pending compaction config card to Behavior tab in config page
- Add HowItWorksCard (expanded by default) and toast notifications to pending messages page
- Add i18n support for all new strings (en/vi/zh)
Extract CompactGroup() from runCompaction() for shared use by both
auto-compact and HTTP compact endpoint. Wire provider registry into
PendingMessagesHandler so /v1/pending-messages/compact performs actual
LLM summarization (keep 15 recent + summary) instead of hard delete.
Add rows-affected guard in Compact() to prevent duplicate summaries
when concurrent compactions race on the same key.
Move count assignment before RAM trim in Record() so MaybeCompact
receives pre-trim count (51) that exceeds threshold (50), allowing
compaction to fire. Previously count was always ≤ limit after trim.
- Update go.mod and Dockerfile to Go 1.26
- Apply `go fix ./...` stdlib modernizations across 170+ files
- Add `go fix` to post-implementation checklist in CLAUDE.md
- Fix go fix misapplied rewrite in loop_history.go
- Add tool status display on channels during tool execution (streaming preview + reactions)
- Emit agent.activity events at phase transitions (thinking, tool_exec, compacting)
- Enrich delegation progress with per-member activity and tool info
- Add LLM-based intent classifier for DM status queries when agent is busy
- Keyword fast-path for cancel/status patterns (no LLM cost)
- Falls back to LLM classification with 5s timeout
- Supports status_query (immediate reply) and cancel (abort run) intents
- Register/unregister runs in makeSchedulerRunFunc for channel inbound tracking
- Add sessionRuns secondary index in Router for O(1) IsSessionBusy lookups
- Add intent_classify config toggle (global default + per-agent override)
- Add tool_status config toggle for channel tool status display
- Add i18n keys and translations (en/vi/zh) for status messages
- Add web UI config toggles for intent_classify and tool_status
HandleMessage() always checked IsAllowed(senderID), which re-gates
individual senders against the allowlist even after the channel-specific
group policy (checkGroupPolicy) already approved access.
This caused group members who weren't individually listed in AllowFrom
to be silently blocked — even when GroupPolicy was set to "open" or the
group ID was in the allowlist.
Affected channels: Zalo Personal, Slack (any channel using HandleMessage
with peerKind "group"). Telegram was unaffected because it publishes
directly to the bus.
Now HandleMessage only enforces the allowlist for DMs. Group access
control is fully delegated to each channel's checkGroupPolicy.
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Zalo API does not support any markup rendering. LLM output containing
markdown artifacts (**bold**, `code`, headers, etc.) was sent as-is,
making messages hard to read. Add StripMarkdown() to convert markdown
to clean plain text, and inject a system prompt hint instructing the
model to output plain text on Zalo channels.
The Zalo personal listener's control event handler assumed content.data
was always an object with a URL field. In practice it can also be a
plain string, causing JSON unmarshal errors. Use json.RawMessage and
inspect the first byte to handle both forms.
* fix(agent): use ChannelType in system prompt for proper channel context
The system prompt was using the channel instance name (e.g. "zep-lao") instead
of the platform type (e.g. "zalo_personal"), causing the LLM to not understand
which messaging platform it's running on. This led to context confusion where
the bot would ask users which channel to send to instead of using the current one.
Changes:
- Add ChannelType field to RunRequest and SystemPromptConfig
- Thread channel type from consumer/cron → agent loop → system prompt
- Add WithToolChannelType/ToolChannelTypeFromCtx for tool context
- Register channel types for both config-based and DB-loaded instances
- Fix Zalo group thread type detection with approvedGroups cache
- Update cron handler to resolve channel type for cron-triggered runs
* refactor(channels): add Type() to Channel interface, remove channelTypes map
Move channel type from a separate map in Manager to the Channel interface
itself. BaseChannel.Type() falls back to Name() for config-based channels
where name == type. Extracts resolveChannelType helper to DRY up 6
repeated resolution blocks across consumer and cron handlers.
* feat(zalo): add pending group history for conversation context
Zalo personal groups now record non-@mentioned messages in a ring buffer
(default 50, configurable via history_limit). When the bot IS mentioned,
pending history is flushed as context — matching Telegram/Discord/Feishu.
Separated mention gating from policy gating in checkGroupPolicy for
cleaner control flow.
Extract shared media utilities (MediaInfo, BuildMediaTags, TranscribeAudio,
DetectMIMEType) into internal/channels/media/ and refactor Telegram to use
them. Add full inbound/outbound media support to Discord and Feishu channels
(STT transcription, document extraction, media tags, voice agent routing).
Add WebSocket media upload/serve endpoints and MIME-aware media tags in
chat.send. Split large channel files for maintainability.
- Fix create_video: use predictLongRunning API instead of generateContent
(async polling flow: POST → poll every 10s → download video from URI)
- Fix durationSeconds as int (not string) per actual Gemini API requirement
- Fix MediaRef collection order: historical first, current last, so
refs[len-1] always returns the most recent file (fixes read_audio
picking up old file instead of current voice message)
- Remove misleading "video not yet supported" text from Telegram handler
that prevented LLM from calling read_video tool
- Add isNonChatModel() to skip chat-based verify for generation models
(veo-*, dall-e-*, imagen-*, gemini-*-image)
- Add persistent media storage (internal/media/) replacing temp file deletion
- Add MediaRef type for lightweight media references in session messages
- Refactor media pipeline to use bus.MediaFile{Path, MimeType} across all channels
- Add read_document builtin tool for PDF/DOCX/XLSX analysis via Gemini native API
- Move image sanitization from Telegram to shared agent/media layer
- Add media reload for multi-turn conversations (images from last 5 messages)
- Add reply-to-message media resolution for Telegram (re-download on reply)
- Add media inventory to compaction summary to preserve awareness after truncation
- Fix coreToolSummaries for read_image, read_document, create_image tools
- Add real-time trace update events via WebSocket broadcast
- Improve trace detail UI with media refs and tool result display
- Use errors.Is() instead of direct sentinel comparison (13 instances)
- Convert if/else-if chains to switch/case for same-variable comparisons
- Remove redundant bitwise OR with zero
- Add post-implementation checklist to CLAUDE.md
Older UI versions saved select-based bool fields (block_reply, etc.)
as strings ("true"/"false") instead of JSON booleans. This caused
json.Unmarshal to fail when loading channel instances. Normalize
config in loadInstance before passing to factories.