The root cause is missing default initialization of JSON field in update flow, leading to NOT NULL violations in both SQLite and Postgres.
Fix ensures consistent behavior across Lite and Standard deployments.
Co-authored-by: Viet Tran <viettranx@gmail.com>
Co-authored-by: Chunning Ha <cn@ha.td>
Rework of #911 against dev with correctness fixes.
Public-facing channels (Facebook, Telegram, Discord, Feishu, WhatsApp,
Zalo OA, Zalo Personal, Pancake, Slack) no longer receive raw internal
error text when an agent run fails — an empty outbound is published
instead so channels still clean up placeholders / typing indicators.
Errors continue to be logged server-side at Error level.
Applied on both hot paths:
- cmd/gateway_consumer_normal.go (agent run failure)
- cmd/gateway_subagent_announce_queue.go (announce lead run failure)
Fixes vs #911:
- Whitelist uses channels.Type* constants (compile-time safe) instead
of string literals. The original PR's literals ("zalo", "line") did
not match any real channel type, so Zalo OA/Personal and Pancake
leaked errors while "line" was a dead branch.
- Added TypePancake and TypeSlack which were missing.
- Empty Content in Facebook.Send and Pancake.Send now short-circuits
(matching Telegram/Discord/Slack). Previously, suppressed errors
flowed to Graph/Pancake APIs as empty payloads, triggering 400s and
channel health degradation.
- Added table-driven test for isExternalChannel covering all real
channel types + empty + unknown + legacy "zalo" short form.
Closes#911
A single hung ch.Start() (e.g. Telegram GetMe stalled on DNS/TCP) blocks
InstanceLoader.mu and wedges every subsequent Reload(), leaving new
channels stuck in "checking" forever.
Wrap Start() in a goroutine bounded by reloadStartTimeout (90s) so Reload
can move on to the next instance. On timeout, Stop the channel in a
bounded window, record a typed failure for the UI, and asynchronously
drain the late-returning Start to detect channels that ignore context
cancellation.
Critically, pass the caller's ctx — not a timeout-wrapped ctx — into
ch.Start. Channels routinely derive long-running goroutines from the
ctx they receive (Telegram's pollCtx is WithCancel(ctx)). A timeout-
wrapped ctx would silently kill polling 90s after a successful Reload.
Add regression tests covering both the hang-then-timeout path and the
caller-context preservation invariant.
Closes#914
Gemini 2.5/3 default to high thinking via OpenAI-compat, consuming the
entire max_tokens budget and truncating titles to a single word (and
adding latency). Title generation is a trivial task that does not
benefit from reasoning, so set OptThinkingLevel="off" and bump
max_tokens from 50 to 256 for headroom across reasoning-capable
providers.
Also update mapGeminiReasoningEffort to forward "off" as "low" (the
minimum effort all Gemini models accept via OpenAI-compat), since not
forwarding causes the server to fall back to "high".
Red-team feedback: the new `name` field on role=tool messages was
being sent to ALL OpenAI-compat hosts including Together, Groq, vLLM.
Those hosts typically reject unknown fields with HTTP 400 — Gemini
is the only backend that requires `name` (via FunctionResponse.name
in Google's OpenAI-compat shim). Scope the field to Gemini-only via
supportsThoughtSignature, matching how we already gate
thought_signature forwarding.
Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
The previous MANDATORY phrasing ("MUST ALSO call write_file for
USER.md and BOOTSTRAP.md before your response ends") was making
small models emit write_file({}) when they had no real user info
to persist, tripping HTTP 400 on Google's Gemini OpenAI-compat shim.
Rewrite the predefined+BOOTSTRAP.md branch with softer "get to know
the user" copy plus explicit hard rules forbidding empty/placeholder
arguments and session-identifier content in USER.md. Safety net is
preserved: the USER PROFILE INCOMPLETE branch nudges the model every
subsequent turn until USER.md is populated.
Sync BOOTSTRAP_PREDEFINED.md template to remove the contradictory
MANDATORY block and replace with matching hard rules.
Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
Gemini returns finish_reason="tool_calls" (not "length") even when the
thinking budget exhausted max_tokens before tool arguments could be
emitted. Extend the existing truncation retry condition to detect this
case via an allowlist of mutating tools (write_file, edit, exec,
create_image, read_file) that virtually never legitimately call with
empty args. Nullary tools (datetime, heartbeat) pass through unchanged.
Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
Gemini 3 defaults to "high" thinking budget internally when no
reasoning_effort is set, consuming the full max_tokens budget and
leaving no tokens for tool call arguments on small models. Map
OptThinkingLevel to reasoning_effort on Gemini routes (native endpoint
or proxy-by-model), with "medium" downgraded to "high" per Gemini 3
Preview constraint. Non-Gemini OpenAI-compat hosts unchanged.
Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
Google Gemini's OpenAI-compat shim maps role=tool messages to native
FunctionResponse{name, response}; an empty name trips HTTP 400
("Name cannot be empty"). Build raw-ID → tool-name index at serialize
time and populate the name field on role=tool wire messages.
Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
Port goclaw context pruning to match upstream TS design in
openclaw/src/agents/pi-hooks/context-pruning/:
- Opt-in default: prune only when mode="cache-ttl" (was opt-out)
- Remove Pass 0 per-result 30% guard (duplicated Pass 1 with different
suffix, caused wobble)
- Dedupe double prune call per iteration: PruneStage owns the single
entry point; loop_history only runs limitHistoryTurns + sanitizeHistory
- Add cache-TTL gate for Anthropic prompt cache: skip prune while cache
is live, scoped per-session via sync.Map
- Add context.pruned event emission for observability
- Configurable TTL as Go duration string ("5m", "30s")
BREAKING CHANGE: context pruning now opt-in. Add
contextPruning.mode: "cache-ttl" to config.agents.defaults to restore.
Migration 51 / SQLite v19 backfills mode="cache-ttl" for agents with
existing custom context_pruning config missing the mode field, so
previously-configured agents keep pruning after the opt-in flip.
NULL configs stay NULL (new opt-in default applies).
Web UI adds Cache TTL input + toggle wiring mode to cache-ttl/off.
Add tts namespace to desktop i18n (en/vi/zh). Extend tools.json with STT form keys including WhatsApp privacy banner. Mirrors web ui/web locale structure.
Port web voice picker and STT provider form to desktop frontend. Singleton audio preview (hide when preview_url null), whatsapp_enabled toggle with privacy banner. Reuses desktop's custom Combobox primitive. Voice id persists via other_config.tts_voice_id merge in AgentDetailPanel. ToolSettingsDialog routes stt tool to new SttProviderForm.
Add stt-provider-form.tsx React component with form fields for model, language, and API key configuration. Mirror TTS provider form structure for consistency. Integrate into builtin-tool-settings-dialog.tsx. Includes comprehensive unit tests for form validation, submission, and error handling.
Add STT builtin tools seeding:
- PG: migration 000050 (49→50) with stt_scribe and stt_proxy entries
- SQLite: schema version 17→18 with inline seed
- Both: register in gateway_builtin_tools.go with encrypted API key support
Migrations support rollback. Version bumps gated to prevent schema drift.
Implement legacy STT bridge adapter to support existing per-channel STTProxyURL database configuration. Wraps arbitrary proxy endpoints and auto-registers them via Manager at boot. Includes integration tests for URL validation, authentication, and error handling.
Add Transcribe() method to Manager with STT provider dispatch and channel-specific overrides. Implements channelSTTOverrides map, WithChannel context propagation, RegisterChannelSTT registration, and resolveSTTChain fallback logic. Includes manager-level unit tests.
Implement STT proxy provider that wraps arbitrary HTTP-based STT services. Ported 12 test cases from legacy Telegram STT integration to validate multipart/form-data submission, language/model overrides, and API error handling.
Implement native ElevenLabs Scribe STT provider with POST /v1/speech-to-text endpoint. Supports 20MB audio cap, multipart form submission, configurable model and language. Includes comprehensive unit tests for happy path, audio size validation, API errors, and edge cases.
Add STTInput, STTOptions, and TranscriptResult types to support native STT providers (ElevenLabs Scribe). Includes audio_duration_secs JSON tag for transcription metadata.
Update architecture overview with streaming TTS provider layer. Expand tools
system docs with voice/model resolution. Update HTTP and WebSocket RPC
documentation for voice endpoints. Record Phase 02 completion in changelog.
Update CLAUDE.md with voice picker and streaming TTS patterns.
Add VoicePicker with live search and preview button. Integrate into prompt
settings section. Add voice API hooks (useVoices, useRefreshVoices). Localize
UI strings for en/vi/zh. Fix snake_case JSON field mapping (voice_id, preview_url).
Add StreamingTTSProvider interface for audio streaming support. Implement voice
caching with TTL+LRU. Add ElevenLabs model validation and voice list retrieval.