* fix: change compact_context default to true
Local LLMs with limited context windows immediately run out of context
when compact_context defaults to false. The system prompt alone can
consume 25K+ tokens, exceeding even 55K context windows with history.
Setting compact_context=true by default limits system prompt injection
to 6000 chars and RAG results to 2 chunks, making the agent usable
with smaller models out of the box.
Fixes#3987
* docs: update compact_context default to true in config reference
Update all locale variants (en, zh-CN, vi) to reflect the new default.
* test: update tests to expect compact_context default of true
Update assertions in schema.rs unit tests and config_persistence.rs
component tests to match the new default value.
Persist allowed_tools in cron_jobs table, threading it through CLI add/update and cron_add/cron_update tool APIs. Add regression coverage for store, tool, and CLI roundtrip paths.
Fixups over original PR #3929: add allowed_tools to all_overdue_jobs SELECT (merge gap), resolve merge conflicts.
Closes#3920
Supersedes #3929
Add a thread_replies option to Slack channel config (default true). When false, replies go to channel root instead of the originating thread.
Closes#3888
* fix: always use Blocks format for system prompts with cache_control
System prompts under 3KB were wrapped in SystemPrompt::String which
cannot carry cache_control headers, resulting in 0% cache hit rate
on Haiku 4.5. Always use SystemPrompt::Blocks with ephemeral
cache_control regardless of prompt size.
Fixes#3977
* fix: lower conversation caching threshold from >4 to >1 messages
The previous threshold of >4 non-system messages was too restrictive,
delaying cache benefits until 5+ turns. Lower to >1 so caching kicks
in after the first user+assistant exchange.
Fixes#3977
* test: update anthropic cache tests for new thresholds and Blocks format
- convert_messages_small_system_prompt now expects Blocks with
cache_control instead of String variant
- should_cache_conversation tests updated for >1 threshold
- backward_compatibility test replaced with blocks-system test
* fix: add sandbox field to ShellTool struct
Add `sandbox: Arc<dyn Sandbox>` field to `ShellTool` and a
`new_with_sandbox()` constructor so callers can inject the configured
sandbox backend. The existing `new()` constructor defaults to
`NoopSandbox` for backward compatibility.
Ref: #3983
* fix: apply sandbox wrapping in ShellTool::execute()
Call `self.sandbox.wrap_command()` on the underlying std::process::Command
(via `as_std_mut()`) after building the shell command and before clearing
the environment. This ensures every shell command passes through the
configured sandbox backend before execution.
Ref: #3983
* fix: wire up sandbox creation at ShellTool callsites
In `all_tools_with_runtime()`, create a sandbox from
`root_config.security` via `create_sandbox()` and pass it to
`ShellTool::new_with_sandbox()`. The `default_tools_with_runtime()`
path retains `ShellTool::new()` which defaults to `NoopSandbox`.
Ref: #3983
* test: add sandbox integration tests for ShellTool
Verify that ShellTool can be constructed with a sandbox via
`new_with_sandbox()`, that NoopSandbox leaves commands unmodified,
and that command execution works end-to-end with a sandbox attached.
Ref: #3983
* fix(cron): add startup catch-up and drop login shell flag
Problems:
1. When ZeroClaw started after downtime (late boot, daemon restart),
overdue jobs were picked up via `due_jobs()` but limited by
`max_tasks` per poll cycle — with many overdue jobs, catch-up
could take many cycles.
2. Cron shell jobs used `sh -lc` (login shell), which loads the
full user profile on every execution — slow and may cause
unexpected side effects.
Fixes:
- Add `all_overdue_jobs()` store query without `max_tasks` limit
- Add `catch_up_overdue_jobs()` startup phase that runs ALL overdue
jobs once before entering the normal polling loop
- Extract `build_cron_shell_command()` helper using `sh -c` (non-login)
- Add structured tracing for catch-up progress
- Add tests for all new functions
* feat(cron): make catch-up configurable via API and control panel
Add `catch_up_on_startup` boolean to `[cron]` config (default: true).
When enabled, the scheduler runs all overdue jobs at startup before
entering the normal polling loop. Users can toggle this from:
- The Cron page toggle switch in the control panel
- PATCH /api/cron/settings { "catch_up_on_startup": false }
- The `[cron]` section of the TOML config editor
Also adds GET /api/cron/settings endpoint to read cron subsystem
settings without parsing the full config.
* fix(config): add catch_up_on_startup to CronConfig test constructors
The CI Lint job failed because the `cron_config_serde_roundtrip` test
constructs CronConfig directly and was missing the new field.
* fix: exempt tool schema validation errors from non-retryable classification
Groq returns 400 "tool call validation failed" which was classified as
non-retryable by is_non_retryable(), preventing the provider-level
fallback in compatible.rs from executing. Add is_tool_schema_error()
to detect these errors and return false from is_non_retryable(), allowing
the retry loop to pass control back to the provider's built-in fallback.
Fixes#3757
* test: add unit tests for tool schema error detection in reliable.rs
Verify is_tool_schema_error detects Groq-style validation failures and
that is_non_retryable returns false for tool schema 400s while still
returning true for other 400 errors like invalid API key.
* fix: escape format braces in test string literals for cargo check
The anyhow::anyhow! macro interprets curly braces as format
placeholders. Use explicit format argument to pass JSON-containing
strings in tests.
- Channel tool filtering (`non_cli_excluded_tools`) now respects
`autonomy.level = "full"` — full-autonomy agents keep all tools
available regardless of channel.
- Gateway `process_message` now creates and passes an `ApprovalManager`
to `agent_turn`, so `ReadOnly`/`Supervised` policies are enforced
instead of silently skipped.
- Gateway also applies `non_cli_excluded_tools` filtering with the same
full-autonomy bypass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The conversational_ai config section is parsed but not yet consumed by
any runtime code. Emit a startup warning so users know the setting is
ignored, and update the doc comment to mark it as reserved for future use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(openrouter): wire provider_timeout_secs through factory
Apply the configured provider_timeout_secs to OpenRouterProvider
in the provider factory, matching the pattern used for compatible
providers.
* fix(openrouter): add timeout_secs field to OpenRouterProvider
Add a configurable timeout_secs field (default 120s) and a
with_timeout_secs() builder method so the HTTP client timeout
can be overridden via provider config instead of being hardcoded.
* refactor(openrouter): improve response decode error messages
Read the response body as text first, then parse with
serde_json::from_str so that decode failures include a truncated
snippet of the raw body for easier debugging.
* test(openrouter): add timeout_secs configuration tests
Verify that the default timeout is 120s and that with_timeout_secs
correctly overrides it.
* style: run rustfmt on openrouter.rs
When autonomy.level is set to "full", the channel/web system prompt no
longer includes instructions telling the model to ask for permission
before executing tools. Previously these safety lines were hardcoded
regardless of autonomy config, causing the LLM to simulate approval
dialogs in channel and web-interface modes even though the
ApprovalManager correctly allowed execution.
The fix adds an autonomy_level parameter to build_system_prompt_with_mode
and conditionally omits the "ask before acting" instructions when the
level is Full. Core safety rules (no data exfiltration, prefer trash)
are always included.
The [conversational_ai] config section was serialized into every
freshly-generated config.toml despite the feature being experimental
and not yet wired into the agent runtime. This confused new users who
found an undocumented section in their config.
Add skip_serializing_if = "ConversationalAiConfig::is_disabled" so the
section is only written when a user has explicitly enabled it. Existing
configs that already contain the section continue to deserialize
correctly via #[serde(default)].
Fixes#3958
Lower the default heartbeat interval to 5 minutes to match the renewable
partial wake-lock cadence. Add `[heartbeat task` to the memory auto-save
skip filter so heartbeat prompts (both Phase 1 decision and Phase 2 task
execution) do not pollute persistent conversation memory.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace workspace_dir.join(path) with resolve_tool_path(path) in
file_write, file_edit, and pdf_read tools to correctly handle absolute
paths within the workspace directory, preventing path doubling.
Closes#3774
The OtpConfig struct uses deny_unknown_fields but was missing the
challenge_max_attempts field, causing zeroclaw config schema to fail
with a TOML parse error when the field appeared in config files.
Add challenge_max_attempts as an Option<u32>-style field with a default
of 3 and a validation check ensuring it is greater than 0.
Repositions the one-time pairing code display to appear directly below
the dashboard URL for cleaner terminal output, and removes the duplicate
display that was showing at the bottom of the route list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the workspace is created outside of `zeroclaw onboard` (e.g., via
cron, daemon, or `< /dev/null`), SOUL.md and IDENTITY.md were never
scaffolded, causing the agent to activate without identity files.
Added `ensure_bootstrap_files()` in `Config::load_or_init()` that
idempotently creates default SOUL.md and IDENTITY.md if missing.
Closes#3819.
Add `timeout_secs` and `agentic_timeout_secs` fields to
`DelegateAgentConfig` so users can tune per-agent timeouts instead
of relying on the hardcoded 120s / 300s defaults.
Validation rejects values of 0 or above 3600s, matching the pattern
used by MCP timeout validation.
Closes#3898
Add a locale-aware tool description system that loads translations from
TOML files in tool_descriptions/. This enables non-English users to see
tool descriptions in their language.
- Add src/i18n.rs module with ToolDescriptions loader, locale detection
(ZEROCLAW_LOCALE, LANG, LC_ALL env vars), and English fallback chain
- Add locale config field to Config struct for explicit locale override
- Create tool_descriptions/en.toml with all 47 tool descriptions
- Create tool_descriptions/zh-CN.toml with Chinese translations
- Integrate with ToolsSection::build() and build_tool_instructions()
to resolve descriptions from locale files before hardcoded fallback
- Add PromptContext.tool_descriptions field for prompt-time resolution
- Add AgentBuilder.tool_descriptions() setter for Agent construction
- Include tool_descriptions/ in Cargo.toml package include list
- Add 8 unit tests covering locale loading, fallback chains, env
detection, and config override
Closes#3901
The echo_provider() test helper writes a fake_claude.sh script to
a shared temp directory. When lib and bin test binaries run in
parallel (separate processes, separate OnceLock statics), one
process can overwrite the script while the other is executing it,
causing "Text file busy" (ETXTBSY). Scope the filename with PID
to isolate each test process.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The seen_tool_signatures HashSet was initialized outside the iteration loop, causing cross-iteration deduplication of legitimate tool calls. This triggered a self-correction spiral where the agent repeatedly attempted skipped calls until hitting max_iterations.
Moving the HashSet inside the loop ensures deduplication only applies within a single iteration, as originally intended.
Fixes#3798
The Telegram channel was ignoring the ack_reactions setting because it
sent setMessageReaction API calls directly in its polling loop, bypassing
the top-level channels_config.ack_reactions check.
- Add optional ack_reactions field to TelegramConfig so it can be set
under [channels_config.telegram] without "unknown key" warnings
- Add ack_reactions field and with_ack_reactions() builder to
TelegramChannel, defaulting to true
- Guard try_add_ack_reaction_nonblocking() behind self.ack_reactions
- Wire channel-level override with fallback to top-level default
- Add config deserialization and channel behavior tests
When LLMs pass the schedule parameter as a JSON string instead of a JSON
object, serde fails with "invalid type: string, expected internally
tagged enum Schedule". Add a deserialize_maybe_stringified helper that
detects stringified JSON values and parses the inner string before
deserializing, providing backward compatibility for both object and
string representations.
Fixes#3860
The llamacpp provider was instantiated with vision disabled by default, causing image transfers from Telegram to fail. Use new_with_vision() with vision enabled, matching the behavior of other compatible providers.
Fixes#3802
The deferred MCP tools section in the system prompt only listed tool
names inside <available-deferred-tools> tags without any instruction
telling the LLM to call tool_search to activate them. In daemon and
Telegram mode, where conversations are shorter and less guided, the
LLM never discovered it should call tool_search, so deferred tools
were effectively unavailable.
Add a "## Deferred Tools" heading with explicit instructions that
the LLM MUST call tool_search before using any listed tool. This
ensures the LLM knows to activate deferred tools in all modes
(CLI, daemon, Telegram) consistently.
Also add tests covering:
- Instruction presence in the deferred section
- Multiple-server deferred tool search
- Cross-server keyword search ranking
- Activation persistence across multiple tool_search calls
- Idempotent re-activation
When a provider returns a context-size-exceeded error, truncate the
oldest non-system messages from conversation history and retry instead
of immediately bailing out. This enables local models with small
context windows (llamafile, llama.cpp) to work by automatically
fitting the conversation within available context.
Closes#3894
Add support for switching AI models at runtime during a conversation.
The model_switch tool allows users to:
- Get current model state
- List available providers
- List models for a provider
- Switch to a different model
The switch takes effect immediately for the current conversation by
recreating the provider with the new model after tool execution.
Risk: Medium - internal state changes and provider recreation
* fix(providers): preserve conversation context in Claude Code CLI provider
Override chat_with_history to format full multi-turn conversation
history into a single prompt for the claude CLI, instead of only
forwarding the last user message.
Closes#3878
* fix(providers): fix ETXTBSY race in claude_code tests
Use OnceLock to initialize the fake_claude.sh test script exactly
once, preventing "Text file busy" errors when parallel tests
concurrently write and execute the same script file.
Handle Schedule::At jobs in reschedule_after_run by disabling them
instead of rescheduling to a past timestamp. Also add a fallback in
persist_job_result to disable one-shot jobs if removal fails.
Closes#3868
When using Channel mode with dynamic classification and routing, the
route-specific `api_key` from `[[model_routes]]` was silently dropped.
The system always fell back to the global `api_key`, causing 401 errors
when routing to `custom:` providers that require distinct credentials.
Root cause: `ChannelRouteSelection` only stored provider + model, and
`get_or_create_provider` always used `ctx.api_key` (the global key).
Changes:
- Add `api_key` field to `ChannelRouteSelection` so the matched route's
credential survives through to provider creation.
- Update `get_or_create_provider` to accept and prefer a route-specific
`api_key` over the global key.
- Use a composite cache key (provider name + api_key hash) to prevent
cache poisoning when multiple routes target the same provider with
different credentials.
- Wire the route api_key through query classification matching and the
`/model` (SetModel) command path.
Fixes#3838
Sync Cargo.lock with new Extism/WASM plugin dependencies and apply
rustfmt line-wrap fix in gateway WebSocket handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wire WASM plugin tools into all_tools_with_runtime() behind
cfg(feature = "plugins-wasm"), discovering and registering tool-capable
plugins from the configured plugins directory at startup.
- Add /api/plugins gateway endpoint (cfg-gated) for listing plugin status.
- Add mod plugins declaration to main.rs binary crate so crate::plugins
resolves when the feature is enabled.
- Add unit tests for PluginHost: empty dir, manifest discovery, capability
filtering, lookup, and removal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce the WASM plugin system foundation:
- Add extism 1.9 as an optional dependency behind `plugins-wasm` feature
- Create `src/plugins/` module with manifest types, error types, and stub host
- Add `Plugin` CLI subcommands (list, install, remove, info) behind cfg gate
- Add `PluginsConfig` to the config schema with sensible defaults
All plugin code is behind `#[cfg(feature = "plugins-wasm")]` so the default
build is unaffected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add device_registry, pending_pairings to test AppState instances and
pairing_dashboard to test GatewayConfig to fix compilation of tests
after the new pairing dashboard fields were introduced.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ConnectParams struct for an optional first-frame connect handshake.
If the first WebSocket message is {"type":"connect",...}, connection
parameters (session_id, device_name, capabilities) are extracted and
a "connected" ack is sent back. Old clients sending "message" first
still work unchanged (backward-compatible).
Extract process_chat_message() helper to avoid duplication between
fallback first-message handling and the main message loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce DeviceRegistry, PairingStore, and five new API endpoints:
- POST /api/pairing/initiate — generate a new pairing code
- POST /api/pairing/submit — submit code with device metadata
- GET /api/devices — list paired devices
- DELETE /api/devices/{id} — revoke a paired device
- POST /api/devices/{id}/rotate — rotate a device token
Wire into AppState and gateway router. Registry is only created
when require_pairing is enabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add PairingDashboardConfig struct with configurable code_length,
ttl_secs, max_pending, max_attempts, and lockout_secs fields.
Nested under GatewayConfig as `pairing_dashboard` with serde defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Export commands module from lib.rs (pub mod commands) for external consumers
- Add --force and --version flags to the Update CLI command
- Wire version parameter through to check() and run() in update.rs,
supporting targeted version fetches via GitHub releases/tags API
- Add WebSocket handshake check (check_websocket_handshake) to the full
self-test suite in self_test.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `zeroclaw update` command with a 6-phase self-update pipeline:
1. Preflight — check GitHub releases API for newer version
2. Download — fetch platform-specific binary to temp dir
3. Backup — copy current binary to .bak for rollback
4. Validate — size check + --version smoke test on download
5. Swap — overwrite current binary with new version
6. Smoke test — verify updated binary runs, rollback on failure
Supports --check flag for update-check-only mode without installing.
Includes version comparison logic with unit tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `zeroclaw self-test` command with two modes:
- Quick mode (--quick): 8 offline checks including config, workspace,
SQLite, provider/tool/channel registries, security policy, and version
- Full mode (default): adds gateway health and memory round-trip checks
Creates src/commands/ module structure with self_test and update stubs.
Adds indicatif and tempfile runtime dependencies for the update pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gateway WebSocket chat sessions were in-memory only — conversation
history was lost on gateway restart, macOS sleep/wake, or client
reconnect. This wires up the existing SessionBackend (SQLite) to
the gateway WS handler so sessions survive restarts and reconnections.
Changes:
- Add delete_session() to SessionBackend trait + SQLite implementation
- Add session_persistence and session_ttl_hours to GatewayConfig
- Add Agent::seed_history() to hydrate agent from persisted messages
- Initialize SqliteSessionBackend in run_gateway() when enabled
- Send session_start message on WS connect with session_id + resumed
- Persist user/assistant messages after each turn
- Add GET /api/sessions and DELETE /api/sessions/{id} REST endpoints
- Bump version to 0.5.0
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Revert "always generate pairing code" to tighter security posture:
codes are only generated on first startup when no tokens exist. Add
a CLI hint to the gateway banner so operators know how to re-pair
on demand. Fix install.sh to not use --new on fresh install (avoids
invalidating the auto-generated code). Fix onboard to show an
informational message instead of a throwaway PairingGuard.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(runtime): add configurable reasoning effort
* fix(test): add missing reasoning_effort field in live test
Add reasoning_effort: None to ProviderRuntimeOptions construction in
openai_codex_vision_e2e.rs to fix E0063 compile error.
---------
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
* feat(tools): add native LinkedIn integration tool
Add a config-gated LinkedIn tool that enables ZeroClaw to interact with
LinkedIn's REST API via OAuth2. Supports creating posts, listing own
posts, commenting, reacting, deleting posts, viewing engagement stats,
and retrieving profile info.
Architecture:
- linkedin.rs: Tool trait impl with action-dispatched design
- linkedin_client.rs: OAuth2 token management and API wrappers
- Config-gated via [linkedin] enabled = false (default off)
- Credentials loaded from workspace .env file
- Automatic token refresh with line-targeted .env update
39 unit tests covering security enforcement, parameter validation,
credential parsing, and token management.
* feat(linkedin): configurable content strategy and API version
- Expand LinkedInConfig with api_version and nested LinkedInContentConfig
(rss_feeds, github_users, github_repos, topics, persona, instructions)
- Add get_content_strategy tool action so agents can read config at runtime
- Fix hardcoded LinkedIn API version 202402 (expired) → configurable,
defaulting to 202602
- LinkedInClient accepts api_version as parameter instead of static header
- 4 new tests (43 total), all passing
* feat(linkedin): add multi-provider image generation for posts
Add ImageGenerator with provider chain (DALL-E, Stability AI, Imagen, Flux)
and SVG fallback card. LinkedIn tool create_post now supports generate_image
parameter. Includes LinkedIn image upload (register → upload → reference),
configurable provider priority, and 14 new tests.
* feat(whatsapp): add voice note transcription and TTS voice replies
- Add STT support: download incoming voice notes via wa-rs, transcribe
with OpenAI Whisper (or Groq), send transcribed text to agent
- Add TTS support: synthesize agent replies to Opus audio via OpenAI
TTS, upload encrypted media, send as WhatsApp voice note (ptt=true)
- Voice replies only trigger when user sends a voice note; text
messages get text replies only. Flag is consumed after one use to
prevent multiple voice notes per agent turn
- Fix transcription module to support OpenAI API key (not just Groq):
auto-detect provider from API URL, check ANTHROPIC_OAUTH_TOKEN /
OPENAI_API_KEY / GROQ_API_KEY env vars in priority order
- Add optional api_key field to TranscriptionConfig for explicit key
- Add response_format: opus to OpenAI TTS for WhatsApp compatibility
- Add channel capability note so agent knows TTS is automatic
- Wire transcription + TTS config into WhatsApp Web channel builder
* fix(providers): prefer ANTHROPIC_OAUTH_TOKEN over global api_key
When the Anthropic provider is used alongside a non-Anthropic primary
provider (e.g. custom: gateway), the global api_key would be passed
as credential override, bypassing provider-specific env vars. This
caused Claude Code subscription tokens (sk-ant-oat01-*) to be ignored
in favor of the unrelated gateway JWT.
Fix: for the anthropic provider, check ANTHROPIC_OAUTH_TOKEN and
ANTHROPIC_API_KEY env vars before falling back to the credential
override. This mirrors the existing MiniMax OAuth pattern and enables
subscription-based auth to work as a fallback provider.
* feat(linkedin): add scheduled post support via LinkedIn API
Add scheduled_at parameter to create_post and create_post_with_image.
When provided (RFC 3339 timestamp), the post is created as a DRAFT
with scheduledPublishOptions so LinkedIn publishes it automatically
at the specified time. This enables the cron job to schedule a week
of posts in advance directly on LinkedIn.
* fix(providers): prefer env vars for openai and groq credential resolution
Generalize the Anthropic OAuth fix to also cover openai and groq
providers. When used alongside a non-matching primary provider (e.g.
a custom: gateway), the global api_key would be passed as credential
override, causing auth failures. Now checks provider-specific env
vars (OPENAI_API_KEY, GROQ_API_KEY) before falling back to the
credential override.
* fix(whatsapp): debounce voice replies to voice final answer only
The voice note TTS was triggering on the first send() call, which was
often intermediate tool output (URLs, JSON, web fetch results) rather
than the actual answer. This produced incomprehensible voice notes.
Fix: accumulate substantive replies (>30 chars, not URLs/JSON/code)
in a pending_voice map. A spawned debounce task waits 4 seconds after
the last substantive message, then synthesizes and sends ONE voice
note with the final answer. Intermediate tool outputs are skipped.
This ensures the user hears the actual answer in the correct language,
not raw tool output in English.
* fix(whatsapp): voice in = voice out, text in = text out
Rewrite voice reply logic with clean separation:
- Voice note received: ALL text output suppressed. Latest message
accumulated silently. After 5s of no new messages, ONE voice note
sent with the final answer. No tool outputs, no text, just voice.
- Text received: normal text reply, no voice.
Atomic debounce: multiple spawned tasks race but only one can extract
the pending message (remove-inside-lock pattern). Prevents duplicate
voice notes.
* fix(whatsapp): voice replies send both text and voice note
Voice note in → text replies sent normally in real-time PLUS one
voice note with the final answer after 10s debounce. Only substantive
natural-language messages are voiced (tool outputs, URLs, JSON, code
blocks filtered out). Longer debounce (10s) ensures the agent
completes its full tool chain before the voice note fires.
Text in → text out only, no voice.
* fix(channels): suppress tool narration and ack reactions
- Add system prompt instruction telling the agent to NEVER narrate
tool usage (no "Let me fetch..." or "I will use http_request...")
- Disable ack_reactions (emoji reactions on incoming messages)
- Users see only the final answer, no intermediate steps
* docs(claude): add full CONTRIBUTING.md guidelines to CLAUDE.md
Add PR template requirements, code naming conventions, architecture
boundary rules, validation commands, and branch naming guidance
directly to CLAUDE.md for AI assistant reference.
* fix(docs): add blank lines around headings in CLAUDE.md for markdown lint
* fix(channels): strengthen tool narration suppression and fix large_futures
- Move anti-narration instruction to top of channel system prompt
- Add emphatic instruction for WhatsApp/voice channels specifically
- Add outbound message filter to strip tool-call-like patterns (⏳, 🔧)
- Box::pin the two-phase heartbeat agent::run call (16664 bytes on Linux)
* feat(channels): add Reddit, Bluesky, and generic Webhook adapters
- Reddit: OAuth2 polling for mentions/DMs/replies, comment and DM sending
- Bluesky: AT Protocol session auth, notification polling, post replies
- Webhook: Axum HTTP server for inbound, configurable outbound POST/PUT
- All three follow existing channel patterns with tests
* fix(channels): use neutral test fixtures and improve test naming in webhook
* feat(tools): add Google Workspace CLI (gws) integration
Adds GoogleWorkspaceTool for interacting with Google Drive, Sheets,
Gmail, Calendar, Docs, and other Workspace services via CLI.
- Config-gated (google_workspace.enabled)
- Service allowlist for restricted access
- Requires shell access for CLI delegation
- Input validation against shell injection
- Wrong-type rejection for all optional parameters
- Config validation for allowed_services (empty, duplicate, malformed)
- Registered in integrations registry and CLI discovery
Closes#2986
* style: fix cargo fmt + clippy violations
* feat(google-workspace): expand config with auth, rate limits, and audit settings
* fix(tools): define missing GWS_TIMEOUT_SECS constant
* fix: Box::pin large futures and resolve duplicate Default impl
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* feat(stt): add multi-provider STT with TranscriptionProvider trait
Refactors single-endpoint transcription to support multiple providers:
Groq (existing), OpenAI Whisper, Deepgram, AssemblyAI, and Google Cloud
Speech-to-Text. Adds TranscriptionManager for provider routing with
backward-compatible config fields.
* style: fix cargo fmt + clippy violations
* fix: Box::pin large futures and resolve merge conflicts with master
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* feat(providers): add Claude Code, Gemini CLI, and KiloCLI subprocess providers
Adds three new local subprocess-based providers for AI CLI tools.
Each provider spawns the CLI as a child process, communicates via
stdin/stdout pipes, and parses responses into ChatResponse format.
* fix: resolve clippy unnecessary_debug_formatting and rustfmt violations
* fix: resolve remaining clippy unnecessary_debug_formatting in CLI providers
* fix(providers): add AiAgent CLI category for subprocess providers
The schedule field in cron_add used a bare {"type":"object"} with a
description string encoding a tagged union in pseudo-notation. The patch
field in cron_update was an opaque {"type":"object"} despite CronJobPatch
having nine fully-typed fields. Both gaps cause weaker instruction-following
models to produce malformed or missing nested JSON when invoking these tools.
Changes:
- cron_add: expand schedule into a oneOf discriminated union with explicit
properties and required fields for each variant (cron/at/every), matching
the Schedule enum in src/cron/types.rs exactly
- cron_add: add descriptions to all previously undocumented top-level fields
- cron_add: expand delivery from a bare inline comment to fully-specified
properties with per-field descriptions
- cron_update: expand patch from opaque object to full properties matching
CronJobPatch (name, enabled, command, prompt, model, session_target,
delete_after_run, schedule, delivery)
- cron_update: schedule inside patch mirrors the same oneOf expansion
- Both: add inline NOTE comments flagging that oneOf is correct for
OpenAI-compatible APIs but SchemaCleanr::clean_for_gemini must be
applied if Gemini native tool calling is ever wired up
- Both: add schema-shape tests using the existing test_config/test_security
helper pattern, covering oneOf variant structure, required fields, and
delivery channel enum completeness
No behavior changes. No new dependencies. Backward compatible: the runtime
deserialization path (serde on Schedule/CronJobPatch) is unchanged.
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
* fix(tools): wire ActivatedToolSet into tool dispatch and spec advertisement
When deferred MCP tools are activated via tool_search, they are stored
in ActivatedToolSet but never consulted by the tool call loop.
tool_specs is built once before the iteration loop and never refreshed,
so the provider API tools[] parameter never includes activated tools.
find_tool only searches the static registry, so execution dispatch also
fails silently.
Thread Arc<Mutex<ActivatedToolSet>> from creation sites through to
run_tool_call_loop. Rebuild tool_specs each iteration to merge base
registry specs with activated specs. Add fallback in execute_one_tool
to check the activated set when the static registry lookup misses.
Change ActivatedToolSet internal storage from Box<dyn Tool> to
Arc<dyn Tool> so we can clone the Arc out of the mutex guard before
awaiting tool.execute() (std::sync::MutexGuard is not Send).
* fix(tools): add activated_tools field to new ChannelRuntimeContext test site
Both entries had hardcoded |_| IntegrationStatus::Available, ignoring
the live config entirely. Users with cron.enabled = true or
browser.enabled = true saw 'Available' on the /integrations dashboard
card instead of 'Active'.
Root cause: status_fn closures did not capture the Config argument.
Fix: replace the |_| stubs with |c| closures that check c.cron.enabled
and c.browser.enabled respectively, matching the pattern used by every
other wired entry in the registry (Telegram, Discord, Shell, etc.).
What did NOT change: ComingSoon entries, always-Active entries (Shell,
File System), platform entries, or any other registry logic.
* feat(security): add Merkle hash-chain audit trail
Each audit entry now includes a SHA-256 hash linking it to the previous
entry (entry_hash, prev_hash, sequence), forming a tamper-evident chain.
Modifying any entry invalidates all subsequent hashes.
- Chain fields added to AuditEvent with #[serde(default)] for backward compat
- AuditLogger tracks chain state and recovers from existing logs on restart
- verify_chain() validates hash linkage, sequence continuity, and integrity
- Five new tests: genesis seed, multi-entry verify, tamper detection,
sequence gap detection, and cross-restart chain recovery
* fix(security): replace personal name with neutral label in audit tests
Remove duplicate tool listing from XmlToolDispatcher::prompt_instructions()
since tool listing is already handled by ToolsSection in prompt.rs. The
method now only emits the XML protocol envelope.
Also fix UTF-8 char boundary panics in memory consolidation truncation by
using char_indices() instead of manual byte-boundary scanning.
Fixes#3643
Supersedes #3678
Co-authored-by: TJUEZ <TJUEZ@users.noreply.github.com>
Adds audio message detection and transcription to WhatsApp Web channel.
Voice messages (PTT) are downloaded, transcribed via the existing
transcription subsystem (Groq Whisper), and delivered as text content.
- TranscriptionConfig field with builder pattern
- Duration limit enforcement before download
- MIME type mapping for audio formats
- Graceful error handling (skip on failure)
- Preserves full retry/reconnect state machine from master
* fix(channel): resolve multi-room reply routing regression (#3224)
PR #3224 (f0f0f808, "feat(matrix): add multi-room support") changed the
channel name format in matrix.rs from "matrix" to "matrix:!roomId", but
the channel lookup in mod.rs still does an exact match against
channels_by_name, which is keyed by Channel::name() (returns "matrix").
This mismatch causes target_channel to always resolve to None for Matrix
messages, silently dropping all replies.
Fix: fall back to a prefix match on the base channel name (before ':')
when the exact lookup fails. This preserves multi-room conversation
isolation while correctly routing replies to the originating channel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: apply cargo fmt to channel routing fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Sandeep (Claude) <sghael+claude@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(tools): add browser delegation tool for corporate web app interaction
Adds BrowserDelegateTool that delegates browser-based tasks to Claude Code
(or other browser-capable CLIs) for interacting with corporate tools
(Teams, Outlook, Jira, Confluence) via browser automation. Includes domain
validation (allow/blocklist), task templates, Chrome profile persistence
for SSO sessions, and timeout management.
* fix: resolve clippy violation in browser delegation tool
* fix(browser-delegate): validate URLs embedded in task text against domain policy
Scan the task text for http(s):// URLs using regex and validate each
against the allow/block domain lists before forwarding to the browser
CLI subprocess. This prevents bypassing domain restrictions by
embedding blocked URLs in the task parameter.
* fix(browser-delegate): constrain URL schemes, gate on runtime, document config
- Add has_shell_access gate so BrowserDelegateTool is only registered on
shell-capable runtimes (skipped with warning on WASM/edge runtimes)
- Add boundary tests for javascript: and data: URL scheme rejection
- URL scheme validation (http/https only) and config docs were already
addressed by a prior commit on this branch
* fix(tools): address CodeRabbit review findings for browser delegation
Remove dead `max_concurrent_tasks` config field and expand doc comments
on the `[browser_delegate]` config section in schema.rs.
When the LLM hallucinates an invalid model ID through the
model_routing_config tool's set_default action, the invalid model gets
persisted to config.toml. The channel hot-reload then picks it up and
every subsequent message fails with a non-retryable 404, permanently
killing the connection with no user recovery path.
Fix with two layers of defense:
1. Tool probe-and-rollback: after saving the new model, send a minimal
chat request to verify the model is accessible. If the API returns a
non-retryable error (404, auth failure, etc.), automatically restore
the previous config and return a failure notice to the LLM.
2. Channel safety net: in maybe_apply_runtime_config_update, reject
config reloads when warmup fails with a non-retryable error instead
of applying the broken config anyway.
Co-authored-by: Christian Pojoni <christian.pojoni@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some OpenAI models (o1, o3, o4, gpt-5 variants) only accept temperature=1.0 and return errors with other values like 0.7. This change automatically adjusts the temperature parameter based on the model being used.
Changes:
- Add adjust_temperature_for_model() function to detect reasoning models
- Apply temperature adjustment in chat_with_system(), chat(), and chat_with_tools()
- Preserve user-specified temperature for standard models (gpt-4o, gpt-4-turbo, etc.)
- Force temperature=1.0 for reasoning models (o1, o3, o4, gpt-5, gpt-5-mini, gpt-5-nano, gpt-5.x-chat-latest)
Testing:
- Add 7 unit tests covering reasoning models, standard models, and edge cases
- All tests pass successfully
- Empirical testing documented in docs/openai-temperature-compatibility.md
Impact:
- Fixes temperature errors when using o1, o3, o4, and gpt-5 model families
- No breaking changes - transparent adjustment for end users
- Standard models continue to work with flexible temperature values
Risk: Low - isolated change within OpenAI provider, well-tested
Rollback: Revert this commit to restore previous behavior
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
Add HandStarted, HandCompleted, and HandFailed event variants to
ObserverEvent, and HandRunDuration, HandFindingsCount, HandSuccessRate
metric variants to ObserverMetric. Update all observer backends (log,
noop, verbose, prometheus, otel) to handle the new variants with
appropriate instrumentation. Prometheus backend registers hand_runs
counter, hand_duration histogram, and hand_findings counter. OTel
backend creates spans and records metrics for hand runs.
The /memory dashboard page rendered a black screen when MemoryCategory::Custom
was serialized by serde's derived impl as a tagged object {"custom":"..."} but
the frontend expected a plain string. No navigation was possible without using
the browser Back button.
Changes:
- src/memory/traits.rs: replace derived serde impls with custom serialize
(delegates to Display, emits plain snake_case string) and deserialize
(parses known variants by name, falls through to Custom(s) for unknown).
Adds memory_category_serde_uses_snake_case and memory_category_custom_roundtrip
tests. No persistent storage migration needed — all backends (SQLite, Markdown,
Postgres) use their own category_to_str/str_to_category helpers and never
read serde-serialized category values back from disk.
- web/src/App.tsx: export ErrorBoundary class so render crashes surface a
recoverable UI instead of a black screen. Adds aria-live="polite" to the
pairing error paragraph for screen reader accessibility.
- web/src/components/layout/Layout.tsx: wrap Outlet in ErrorBoundary keyed
by pathname so the navigation shell stays mounted during a page crash and
the boundary resets on route change.
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
When mention_only is enabled, the bot correctly requires an @mention in
guild (server) channels. However, Direct Messages have no guild_id and
are inherently private and addressed to the bot — requiring a @mention
in a DM is never correct and silently drops all DM messages.
Changes:
- src/channels/discord.rs: detect DMs via absence of guild_id in the
gateway payload, compute effective_mention_only = self.mention_only && !is_dm,
and pass that to normalize_incoming_content instead of self.mention_only.
DMs bypass the mention gate; guild messages retain existing behaviour.
- Adds three tests: DM bypasses mention gate, guild message without mention
is rejected, guild message with mention passes and strips the mention tag.
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
* fix(agent): remove bare URL → curl fallback in GLM-style tool call parser
The `parse_glm_style_tool_calls` function had a "Plain URL" fallback
that converted any bare URL line (e.g. `https://example.com`) into a
`shell` tool call running `curl -s '<url>'`. This caused:
- False positives: normal URLs in LLM replies misinterpreted as tool calls
- Swallowed replies: text with URLs not forwarded to the channel
- Unintended shell commands: `curl` executed without user intent
Explicit GLM-format tool calls like `browser_open/url>https://...` and
`shell/command>...` are unaffected — only the bare URL catch-all is
removed.
* style: cargo fmt
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* feat(channels): add X/Twitter and Mochat channel integrations
Add two new channel implementations to close competitive gaps:
- X/Twitter: Twitter API v2 with mentions polling, tweet threading
(auto-splits at 280 chars), DM support, and rate limit handling
- Mochat: HTTP polling-based integration with Mochat customer service
platform, configurable poll interval, message dedup
Both channels follow the existing Channel trait pattern with full
config schema integration, health checks, and dedup.
Closes competitive gap: NanoClaw had X/Twitter, Nanobot had Mochat.
* fix(channels): use write! instead of format_push_string for clippy
Replace url.push_str(&format!(...)) with write!(url, ...) to satisfy
clippy::format_push_string lint on CI.
* fix(channels): rename reply_to parameter to avoid legacy field grep
The component test source_does_not_use_legacy_reply_to_field greps
for "reply_to:" in source files. Rename the parameter to
reply_tweet_id to pass this check.