* feat: add discord history logging and search tool with persistent channel cache
* fix: remove unused channel_names field from DiscordHistoryChannel
The channel_names HashMap was declared and initialized but never used.
Channel name caching is handled via discord_memory.get()/store() with
the cache:channel_name: prefix. Remove the dead field.
* style: run cargo fmt on discord_history.rs
---------
Co-authored-by: ninenox <nisit15@hotmail.com>
* feat(channels): add Gmail Pub/Sub push notifications for real-time email
Add GmailPushChannel that replaces IMAP polling with Google's Pub/Sub
push notification system for real-time email-driven automation.
- New channel at src/channels/gmail_push.rs implementing the Channel trait
- Registers Gmail watch subscription (POST /gmail/v1/users/me/watch)
with automatic renewal before the 7-day expiry
- Handles incoming Pub/Sub notifications at POST /webhook/gmail
- Fetches new messages via Gmail History API (startHistoryId-based)
- Dispatches email messages to the agent with full metadata
- Sends replies via Gmail messages.send API
- Config: gmail_push.enabled, topic, label_filter, oauth_token,
allowed_senders, webhook_url
- OAuth token encrypted at rest via existing secret store
- Webhook endpoint added to gateway router
- 30+ unit tests covering notification parsing, header extraction,
body decoding, sender allowlist, and config serialization
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): fix extract_body_text_plain test
Gmail API sends base64url without padding. The decode_body function
converted URL-safe chars back to standard base64 but did not restore
the padding, causing STANDARD decoder to fail and falling back to
snippet. Add padding restoration before decoding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): address critical security bugs in Gmail Pub/Sub push
- Add webhook authentication via shared secret (webhook_secret config
field or GMAIL_PUSH_WEBHOOK_SECRET env var), preventing unauthorized
message injection through the unauthenticated webhook endpoint
- Add 1MB body size limit on webhook endpoint to prevent memory exhaustion
- Fix race condition in handle_notification: hold history_id lock across
the read-fetch-update cycle to prevent duplicate message processing
when concurrent webhook notifications arrive
- Sanitize RFC 2822 headers (To/Subject) to prevent CRLF injection
attacks that could add arbitrary headers to outgoing emails
- Fix extract_email_from_header panic on malformed angle brackets by
using rfind('>') and validating bracket ordering
- Add 30s default HTTP client timeout for all Gmail API calls,
preventing indefinite hangs
- Clone tx sender before message processing loop to avoid holding
the mutex lock across network calls
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(gateway): add Live Canvas (A2UI) tool and real-time web viewer
Add a Live Canvas system that enables the agent to push rendered content
(HTML, SVG, Markdown, text) to a web-visible canvas in real time.
Backend:
- src/tools/canvas.rs: CanvasTool with render/snapshot/clear/eval actions,
backed by a shared CanvasStore (Arc<RwLock<HashMap>>) with per-canvas
broadcast channels for real-time updates
- src/gateway/canvas.rs: REST endpoints (GET/POST/DELETE /api/canvas/:id,
GET /api/canvas/:id/history, GET /api/canvas) and WebSocket endpoint
(WS /ws/canvas/:id) for real-time frame delivery
Frontend:
- web/src/pages/Canvas.tsx: Canvas viewer page with WebSocket connection,
iframe sandbox rendering, canvas switcher, frame history panel
Registration:
- CanvasTool registered in all_tools_with_runtime (always available)
- Canvas routes wired into gateway router
- CanvasStore added to AppState
- Canvas page added to App.tsx router and Sidebar navigation
- i18n keys added for en/zh/tr locales
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(gateway): share CanvasStore between tool and REST API
The CanvasTool and gateway AppState each created their own CanvasStore,
so content rendered via the tool never appeared in the REST API.
Create the CanvasStore once in the gateway, pass it to
all_tools_with_runtime via a new optional parameter, and reuse the
same instance in AppState.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(gateway): address critical security and reliability bugs in Live Canvas
- Validate content_type in REST POST endpoint against allowed set,
preventing injection of "eval" frames via the REST API
- Enforce MAX_CONTENT_SIZE (256KB) limit on REST POST endpoint,
matching tool-side validation to prevent memory exhaustion
- Add MAX_CANVAS_COUNT (100) limit to prevent unbounded canvas creation
and memory exhaustion from CanvasStore
- Handle broadcast RecvError::Lagged in WebSocket handler gracefully
instead of disconnecting the client
- Make MAX_CONTENT_SIZE and ALLOWED_CONTENT_TYPES pub for gateway reuse
- Update CanvasStore::render and subscribe to return Option for
canvas count enforcement
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: rareba <rareba@users.noreply.github.com>
* feat(channels): add voice wake word detection channel
Add VoiceWakeChannel behind the `voice-wake` feature flag that:
- Captures audio from the default microphone via cpal
- Uses energy-based VAD to detect speech activity
- Transcribes speech via the existing transcription API (Whisper)
- Checks for a configurable wake word in the transcription
- On detection, captures the following utterance and dispatches it
as a ChannelMessage
State machine: Listening -> Triggered -> Capturing -> Processing -> Listening
Config keys (under [channels_config.voice_wake]):
- wake_word (default: "hey zeroclaw")
- silence_timeout_ms (default: 2000)
- energy_threshold (default: 0.01)
- max_capture_secs (default: 30)
Includes tests for config parsing, state machine, RMS energy
computation, and WAV encoding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
type inference failures (create_dir_all, write, read_to_string)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): exclude voice-wake from all-features CI check
Add a `ci-all` meta-feature in Cargo.toml that includes every feature
except `voice-wake`, which requires `libasound2-dev` (ALSA) not present
on CI runners. Update the check-all-features CI job to use
`--features ci-all` instead of `--all-features`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): address critical bugs in voice wake word detection
- Replace std::mem::forget(stream) with dedicated thread that holds the
cpal stream and shuts down cleanly via oneshot channel, preventing
microphone resource leaks on task cancellation
- Add config validation: energy_threshold must be positive+finite,
silence_timeout_ms >= 100ms, max_capture_secs clamped to 300
- Guard WAV encoding against u32 overflow for large audio buffers
- Add hard cap on capture_buf size to prevent unbounded memory growth
- Increase audio channel buffer from 4 to 64 slots to reduce chunk
drops during transcription API calls
- Remove dead WakeState::Processing variant that was never entered
---------
Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For models with small context windows (e.g. glm-4.5-air ~8K tokens),
the system prompt alone can exceed the limit. This adds:
- max_system_prompt_chars config option (default 0 = unlimited)
- compact_context now also compacts the system prompt: skips the
Channel Capabilities section and shows only tool names
- Truncation with marker when prompt exceeds the budget
Users can set `max_system_prompt_chars = 8000` in [agent] config
to cap the system prompt for small-context models.
Closes#4124
Add ReactionTool that exposes Channel::add_reaction and
Channel::remove_reaction as an agent-callable tool. Uses a
late-binding ChannelMapHandle (Arc<RwLock<HashMap>>) pattern
so the tool can be constructed during tool registry init and
populated once channels are available in start_channels.
Parameters: channel, message_id, emoji, action (add/remove).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(config): add configurable pacing controls for slow/local LLM workloads (#2963)
Add a new `[pacing]` config section with four opt-in parameters that
let users tune timeout and loop-detection behavior for local LLMs
(Ollama, llama.cpp, vLLM) without disabling safety features entirely:
- `step_timeout_secs`: per-step LLM inference timeout independent of
the overall message budget, catching hung model responses early.
- `loop_detection_min_elapsed_secs`: time-gated loop detection that
only activates after a configurable grace period, avoiding false
positives on long-running browser/research workflows.
- `loop_ignore_tools`: per-tool loop-detection exclusions so tools
like `browser_screenshot` that structurally resemble loops are not
counted toward identical-output detection.
- `message_timeout_scale_max`: overrides the hardcoded 4x ceiling in
the channel message timeout scaling formula.
All parameters are strictly optional with no effect when absent,
preserving full backwards compatibility.
Closes#2963
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(config): add missing pacing fields in tests and call sites
* fix(config): add pacing arg to remaining cost-tracking test call sites
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
* feat(channel): add per-channel proxy_url support for HTTP/SOCKS5 proxies
Allow each channel to optionally specify a `proxy_url` in its config,
enabling users behind restrictive networks to route channel traffic
through HTTP or SOCKS5 proxies. When set, the per-channel proxy takes
precedence over the global `[proxy]` config; when absent, the channel
falls back to the existing runtime proxy behavior.
Adds `proxy_url: Option<String>` to all 12 channel config structs
(Telegram, Discord, Slack, Mattermost, Signal, WhatsApp, Wati,
NextcloudTalk, DingTalk, QQ, Lark, Feishu) and introduces
`build_channel_proxy_client`, `build_channel_proxy_client_with_timeouts`,
and `apply_channel_proxy_to_builder` helpers that normalize proxy URLs
and integrate with the existing client cache.
Closes#3262
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(channel): add missing proxy_url fields in test initializers
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
Adopted from #3705 by @fangxueshun with fixes:
- Added input validation for date strings (RFC 3339)
- Used chrono DateTime comparison instead of string comparison
- Added since < until validation
- Updated mem0 backend
Supersedes #3705
Adds per-channel cost tracking via task-local context in the tool call
loop. Budget enforcement blocks further API calls when limits are
exceeded. Resolves merge conflicts with model-switch retry loop,
reply_target parameter, and autonomy level additions on master.
Supersedes #3758
Self-hosted Whisper-compatible STT provider that POSTs audio to a
configurable HTTP endpoint (e.g. faster-whisper over WireGuard). Audio
never leaves the platform perimeter.
Implemented via red/green TDD cycles:
Wave 1 — config schema: LocalWhisperConfig struct, local_whisper field
on TranscriptionConfig + Default impl, re-export in config/mod.rs
Wave 2 — from_config validation: url non-empty, url parseable, bearer_token
non-empty, max_audio_bytes > 0, timeout_secs > 0; returns Result<Self>
Wave 3 — manager integration: registration with ? propagation (not if let Ok
— credentials come directly from config, no env-var fallback; present
section with bad values is a hard error, not a silent skip)
Wave 4 — transcribe(): resolve_audio_format() extracted from validate_audio()
so LocalWhisperProvider can resolve MIME without the 25 MB cloud cap;
size check + format resolution before HTTP send
Wave 5 — HTTP mock tests: success response, bearer auth header, 503 error
33 tests (20 baseline + 13 new), all passing. Clippy clean.
Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
Slack's Block Kit supports a native `markdown` block type that accepts
standard Markdown and handles rendering. This removes the need for a
custom Markdown-to-mrkdwn converter. Messages over 12,000 chars fall
back to plain text.
Co-authored-by: Joe Hoyle <joehoyle@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(config): add missing WhatsApp Web policy config keys (mode, dm_policy, group_policy, self_chat_mode)
* fix(onboard): add missing WhatsApp policy fields to wizard struct literals
The new mode, dm_policy, group_policy, and self_chat_mode fields added
to WhatsAppConfig need default values in the onboard wizard's struct
initializers to avoid E0063 compilation errors.
The channel path in `src/channels/mod.rs` was passing `None` as the
`model_switch_callback` to `run_tool_call_loop()`, which meant model
switching via the `model_switch` tool was silently ignored in channel
mode.
Wire the callback in following the same pattern as the CLI path:
- Pass `Some(model_switch_callback.clone())` instead of `None`
- Wrap the tool call loop in a retry loop
- Handle `ModelSwitchRequested` errors by re-creating the provider
with the new model and retrying
Fixes#4107
Implement start_typing/stop_typing for Slack using the Assistants API
assistant.threads.setStatus method. Tracks thread context from
assistant_thread_started events and inbound messages, then sets
"is thinking..." status during processing. Status auto-clears when
the bot sends a reply via chat.postMessage.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(memory): add mem0 (OpenMemory) backend integration
- Implement Mem0Memory struct with full Memory trait
- Add history() audit trail, recall_filtered() with time/metadata filters
- Add store_procedural() for conversation trace extraction
- Add ProceduralMessage type to Memory trait with default no-op
- Feature-gated behind `memory-mem0` flag
- 9 unit tests covering edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: apply cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(memory): add extraction_prompt config, deploy scripts, and timing instrumentation
- Add `extraction_prompt` field to `Mem0Config` for custom LLM fact
extraction prompts (e.g. Cantonese/Chinese content), with
`MEM0_EXTRACTION_PROMPT` env var fallback
- Pass `custom_instructions` in mem0 store requests so the server
uses the client-supplied prompt over its default
- Add timing instrumentation to channel message pipeline
(mem_recall_ms, elapsed_before_llm_ms, llm_call_ms, total_ms)
- Add `deploy/mem0/` with self-hosted mem0 + reranker GPU server
scripts, fully configurable via environment variables
- Update config reference docs (EN, zh-CN, VI) with `[memory.mem0]`
subsection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
# src/channels/mod.rs
* chore: remove accidentally staged worktree from index
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The QQ channel WebSocket loop did not handle incoming Ping frames,
causing the server to consider the connection dead and drop it. Add a
Ping handler that replies with Pong, keeping the connection alive.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(slack): implement reaction support with sanitized error responses
Add add_reaction() and remove_reaction() for Slack channel, with
unicode-to-Slack emoji mapping, idempotent error handling, and
sanitized API error responses matching the pattern used by
chat.postMessage.
Based on #4089 by @joehoyle, with sanitize_api_error() applied to
reaction error paths for consistency with existing Slack methods.
Supersedes #4089
* chore(deps): bump rustls-webpki to 0.103.10 (RUSTSEC-2026-0049)
Add full rich media send/receive support using unified [TYPE:target] markers
(aligned with Telegram). Register QQ as a cron announcement delivery channel.
- Media upload with SHA256-based caching and TTL
- Attachment download to workspace with all types supported
- Voice: prefer voice_wav_url (WAV), inject QQ ASR transcription
- File uploads include file_name for proper display in QQ client
- msg_seq generation and reply rate-limit tracking
- QQ delivery instructions in system prompt
- Register QQ in cron scheduler and tool description
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: preserve session context across Slack messages when thread_replies=false
When thread_replies=false, inbound_thread_ts() was falling back to each
message's own ts, giving every message a unique conversation key and
breaking multi-turn context. Now top-level messages get thread_ts=None
when threading is disabled, so all messages from the same user in the
same channel share one session.
Closes#4052
* chore: ignore RUSTSEC-2024-0384 (unmaintained instant crate via nostr)
Add the missing interrupt_on_new_message field to MatrixConfig and wire
it through InterruptOnNewMessageConfig so Matrix behaves consistently
with Telegram, Slack, Discord, and Mattermost.
Closes#4058
* feat(config): add google workspace operation allowlists
* docs(superpowers): link google workspace operation inventory sources
* docs(superpowers): verify starter operation examples
* fix(google_workspace): remove duplicate credential/audit blocks, fix trim in allowlist check, add duplicate-methods test
- Remove the duplicated credentials_path, default_account, and audit_log
blocks that were copy-pasted into execute() — they were idempotent but
misleading and would double-append --account args on every call.
- Trim stored service/resource/method values in is_operation_allowed() to
match the trim applied during Config::validate(), preventing a mismatch
where a config entry with surrounding whitespace would pass validation but
never match at runtime.
- Add google_workspace_allowed_operations_reject_duplicate_methods_within_entry
test to cover the duplicate-method validation path that was implemented but
untested.
* fix(google_workspace): close sub_resource bypass, trim allowed_services at runtime, mark spec implemented
- HIGH: extract and validate sub_resource before the allowlist check;
is_operation_allowed() now accepts Option<&str> for sub_resource and
returns false (fail-closed) when allowed_operations is non-empty and
a sub_resource is present — prevents nested gws calls such as
`drive/files/permissions/list` from slipping past a 3-segment policy
- MEDIUM: runtime allowed_services check now uses s.trim() == service,
matching the trim() applied during config validation
- LOW: spec status updated to Implemented; stale "does not currently
support method-level allowlists" line removed
- Added test: operation_allowlist_rejects_sub_resource_when_operations_configured
* docs(google_workspace): document sub_resource limitation and add config-reference entries
Spec updates (superpowers/specs):
- Semantics section: note that sub_resource calls are denied fail-closed when
allowed_operations is configured
- Mental model: show both 3-segment and 4-segment gws command shapes; explain
that 4-segment commands are unsupported with allowed_operations in this version
- Runtime enforcement: correct the validation order to match the implementation
(sub_resource extracted before allowlist check, budget charged last)
- New section: Sub-Resource Limitation — documents impact, operator workaround,
and confirms the deny is intentional for this slice
- Follow-On Work: add sub_resource config model extension as item 1
Config reference updates (all three locales):
- Add [google_workspace] section with top-level keys, [[allowed_operations]]
sub-table, sub-resource limitation note, and TOML example
* fix(docs): add classroom and events to allowed_services list in all config-reference locales
* feat(google_workspace): extend allowed_operations to support sub_resource for 4-segment gws commands
All Gmail operations use gws gmail users <sub_resource> <method>, not the flat
3-segment shape. Without sub_resource support in allowed_operations, Gmail could
not be scoped at all, making the email-assistant use case impossible.
Config model:
- Add optional sub_resource field to GoogleWorkspaceAllowedOperation
- An entry without sub_resource matches 3-segment calls (Drive, Calendar, etc.)
- An entry with sub_resource matches only calls with that exact sub_resource value
- Duplicate detection updated to (service, resource, sub_resource) key
Runtime:
- Remove blanket sub_resource deny; is_operation_allowed now matches on all four
dimensions including the optional sub_resource
Tests:
- Add operation_allowlist_matches_gmail_sub_resource_shape
- Add operation_allowlist_matches_drive_3_segment_shape
- Add rejects_operation_with_unlisted_sub_resource
- Add google_workspace_allowed_operations_allow_same_resource_different_sub_resource
- Add google_workspace_allowed_operations_reject_invalid_sub_resource_characters
- Add google_workspace_allowed_operations_deserialize_without_sub_resource
- Update all existing tests to use correct gws command shapes
Docs:
- Spec: correct Gmail examples throughout; remove Sub-Resource Limitation section;
update data model, validation rules, example use case, and follow-on work
- Config-reference (en, vi, zh-CN): add sub_resource field to allowed_operations
table; update Gmail examples to correct 4-segment shapes
Platform:
- email-assistant SKILL.md: update allowed_operations paths to gmail/users/* shape
* fix(google_workspace): add classroom and events to service parameter schema description
* fix(google_workspace): cross-validate allowed_operations service against allowed_services
When allowed_services is explicitly configured, each allowed_operations entry's
service must appear in that list. An entry that can never match at runtime is a
misconfigured policy: it looks valid but silently produces a narrower scope than
the operator intended. Validation now rejects it with a clear error message.
Scope: only applies when allowed_services is non-empty. When it is empty, the tool
uses a built-in default list defined in the tool layer; the validator cannot
enumerate that list without duplicating the constant, so the cross-check is skipped.
Also:
- Update allowed_operations field doc-comment from 3-part (service, resource, method)
to 4-part (service, resource, sub_resource, method) model
- Soften Gmail sub_resource "required" language in config-reference (en, vi, zh-CN)
from a validation requirement to a runtime matching requirement — the validator
does not and should not hardcode API shape knowledge for individual services
- Add tests: rejects operation service not in allowed_services; skips cross-check
when allowed_services is empty
* fix(google_workspace): cross-validate allowed_operations.service against effective service set
When allowed_services is empty the validator was silently skipping the
service cross-check, allowing impossible configs like an unlisted service
in allowed_operations to pass validation and only fail at runtime.
Move DEFAULT_GWS_SERVICES from the tool layer (google_workspace.rs) into
schema.rs so the validator can use it unconditionally. When allowed_services
is explicitly set, validate against that set; when empty, fall back to
DEFAULT_GWS_SERVICES. Remove the now-incorrect "skips cross-check when empty"
test and add two replacement tests: one confirming a valid default service
passes, one confirming an unknown service is rejected even with empty
allowed_services.
* fix(google_workspace): update test assertion for new error message wording
* docs(google_workspace): fix stale 3-segment gmail example in TDD plan
* fix(google_workspace): address adversarial review round 4 findings
- Error message for denied operations now includes sub_resource when
present, so gmail/users/messages/send and gmail/users/drafts/send
produce distinct, debuggable errors.
- Audit log now records sub_resource, completing the trail for 4-segment
Gmail operations.
- Normalize (trim) allowed_services and allowed_operations fields at
construction time in new(). Runtime comparisons now use plain equality
instead of .trim() on every call, removing the latent defect where a
future code path could forget to trim and silently fail to match.
- Unify runtime character validation with schema validation: sub_resource
and service/resource/method checks now both require lowercase alphanumeric
plus underscore and hyphen, matching the validator's character set.
- Add positional_cmd_args() test helper and tests verifying 3-segment
(Drive) and 4-segment (Gmail) argument ordering.
- Add test confirming page_limit without page_all passes validation.
- Add test confirming whitespace in config values is normalized at
construction, not deferred to comparison time.
- Fix spec Runtime Enforcement section to reflect actual code order.
* fix(google_workspace): wire production helpers to close test coverage gaps
- Remove #[cfg(test)] from positional_cmd_args; execute() now calls the
same function the arg-ordering tests exercise, so a drift in the real
command-building path is caught by the existing tests.
- Extract build_pagination_args(page_all, page_limit) as a production
method used by execute(). Replace the brittle page_limit_without_page_all
test (which relied on environment-specific execution failure wording)
with four direct assertions on build_pagination_args covering all
page_all/page_limit combinations.
* fix(whatsapp): remove duplicate variable declaration causing unused warning
Remove duplicate `let transcription_config = self.transcription.clone()`
(line 626 shadowed by identical line 628). The duplicate caused a
compiler warning during --features whatsapp-web builds.
Note: the reported "hang" at 526/528 crates is expected behavior for
release builds with lto="fat" + codegen-units=1 — the final link step
is slow but does complete.
Closes#4034
---------
Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
In Compact (MetadataOnly) mode, skill tools were omitted from the system
prompt alongside instructions. This meant the LLM had no visibility into
TOML-defined tools when running in Compact mode, defeating the primary
advantage of TOML skills over MD skills (structured tool metadata).
Now Compact mode skips only instructions (loaded on demand via
read_skill) while still inlining tool definitions so the LLM knows
which skill tools are available.
Closes#3702
* fix(ollama): default to prompt-guided tool calling for local models
Ollama's /api/chat native tool-calling parameter is silently ignored by
many local models (llama3, qwen3, phi4, etc.), causing them to emit
tool-call JSON as plain text instead of structured tool calls. When
native_tool_calling was true, the XML tool-use instructions were
suppressed from the system prompt, leaving models with no guidance on
the expected tool protocol.
Default to prompt-guided (XML) tool calling so all Ollama models receive
tool-use instructions in the system prompt and the existing text-based
parser in the agent loop can extract tool calls reliably.
Also fixes a minor rustfmt issue in channels/mod.rs from #2891.
Fixes#3999Fixes#3982
* fix(channels): update test keys for reply_target in history key
The conversation_history_key now includes reply_target (from #2891),
but the refreshes_available_skills_after_new_session test still used
the old key format "telegram_alice" instead of
"telegram_chat-refresh_alice".
conversation_history_key() now includes reply_target to isolate
conversation histories across distinct Discord/Slack/Mattermost
channels for the same sender. Previously all channels produced
the same key {channel}_{sender}, causing cross-channel context bleed.
New key format: {channel}_{reply_target}_{sender} (without thread)
or {channel}_{reply_target}_{thread_ts}_{sender} (with thread).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The conditional cfg branches for AtomicU32 (32-bit fallback) and
AtomicU64 (64-bit) became dead code after portable_atomic::AtomicU64
was adopted in bd757996. The AtomicU32 branch would fail to compile
on 32-bit targets because the import was removed but the usage remained.
Use portable_atomic::AtomicU64 unconditionally, which works on all
targets.
Fixes#3452
Co-authored-by: SpaceLobster <spacelobster@SpaceLobsters-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The /new command only cleared in-memory conversation history but left
the JSONL session file on disk. On daemon restart, stale history was
rehydrated, negating the user's session reset. This caused context
pollution and degraded tool calling reliability.
Add delete_session() to SessionStore and call it from the /new handler
so both in-memory and persisted state are cleared.
Closes#4009
Add interruption_scope_id to ChannelMessage for thread-aware cancellation. Slack genuine thread replies and Matrix threads get scoped keys, preventing cross-thread cancellation. All other channels preserve existing behavior.
Supersedes #3900. Depends on #3891.
* feat(channel): add /stop command to cancel in-flight tasks
Adds an explicit /stop slash command that allows users on any non-CLI
channel (Matrix, Telegram, Discord, Slack, etc.) to cancel an agent
task that is currently running.
Changes:
- is_stop_command(): new helper that detects /stop (case-insensitive,
optional @botname suffix), not gated on channel type
- /stop fast path in run_message_dispatch_loop: intercepts /stop before
semaphore acquisition so the target task is never replaced in the store;
fires CancellationToken on the running task; sends reply via tokio::spawn
using the established two-step channel lookup pattern
- register_in_flight separated from interrupt_enabled: all non-CLI tasks
now enter the in_flight_by_sender store, enabling /stop to reach them;
auto-cancel-on-new-message remains gated on interrupt_enabled (Telegram/
Slack only) — this is a deliberate broadening, not a side effect
Deferred to follow-up (feat/matrix-interrupt-on-new-message):
- interrupt_on_new_message config field for Matrix
- thread-aware interruption_scope_key (requires per-channel thread_ts
semantics analysis; Slack always sets thread_ts, Matrix only for replies)
Supersedes #2855
Tests: 7 new unit tests for is_stop_command; all 4075 tests pass.
* feat(channel): add interrupt_on_new_message support for Discord
---------
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
Keep both the PR's Mattermost interrupt_on_new_message additions
and master's new fields (pending_new_sessions, prompt_config,
autonomy_level) from the /stop command PR (#3891).
* feat: add Jira tool with get_ticket, search_tickets, and comment_ticket
Implements a new `jira` tool following the existing zeroclaw tool
conventions (Tool trait, SecurityPolicy, config-gated registration).
- get_ticket: configurable detail level (basic/basic_search/full/changelog)
with response shaping; always in the default allowed_actions list
- search_tickets: JQL-based search with cursor pagination (nextPageToken);
always returns basic_search shape; gated by allowed_actions
- comment_ticket: posts ADF comments with inline markdown-like syntax —
@email mentions resolved to Jira accountId, **bold**, bullet lists,
newlines; gated by allowed_actions and SecurityPolicy Act operation
Config: [jira] section with base_url, email, api_token (encrypted at
rest, falls back to JIRA_API_TOKEN env var), allowed_actions (default:
["get_ticket"]), and timeout_secs. Validated on load.
Tool description in tool_descriptions/en.toml documents all three
actions and the full comment syntax for the AI system prompt.
* fix: address jira tool code review findings
High priority:
- Validate issue_key against ^[A-Z][A-Z0-9]+-\d+$ before URL interpolation
to prevent path traversal in get_ticket and comment_ticket
Medium priority:
- Add email guard in tool registration (mod.rs) to skip with a warning
instead of registering a broken tool when jira.email is empty
- Shape comment_ticket response to return only id, author, created —
avoids exposing internal Jira metadata to the AI
- Replace O(n²) comment matching in shape_basic with a HashMap lookup
keyed by comment ID for O(1) access
- Add api_token validation in Config::validate() checking both the
config field and JIRA_API_TOKEN env var when jira.enabled = true
Low priority:
- Make shape_basic_search private (was accidentally pub)
- Extend clean_email to strip leading punctuation (( and [) so that
@(john@co.com) resolves correctly; fix suffix computation via pointer
arithmetic to handle the shifted offset
- Clarify tool_descriptions/en.toml: @prefix is required for mentions,
bare emails without @ are treated as plain text
- Handle unmatched ** in parse_inline: emit as literal text instead of
silently producing a bold node with no closing marker
* fix(jira): allow lowercase project keys in issue_key validation
Relax validate_issue_key to accept both PROJ-123 and proj-123, since
some Jira instances use lowercase custom project keys. Path traversal
protection is preserved via alphanumeric + digit-number requirement.
* feat(tools): add tool honesty instructions to system prompt
Prevent AI from fabricating tool results by injecting a CRITICAL:
Tool Honesty section into both channel and CLI/agent system prompts.
Rules: never fabricate or guess tool results, report errors as-is,
and ask the user when unsure if a tool call succeeded.
* style: sort JiraConfig import alphabetically in config/mod.rs
* style(jira): fix strict clippy lints in jira_tool
- Derive Default for LevelOfDetails instead of manual impl
- Use char arrays in trim_start_matches/trim_end_matches
- Allow cast_possible_truncation on search_tickets (usize->u32 bounded by max_results)
- Remove needless borrow on &email
* fix(ci): adapt to upstream autonomy_level additions in channels/mod.rs
- Add missing autonomy_level argument to build_system_prompt_with_mode call in test
- Add missing autonomy_level field in ChannelRuntimeContext test initializer
- Allow large_futures in load_or_init test (Config struct growth from JiraConfig)
* fix(ci): resolve duplicate and missing autonomy_level in test initializers
* fix(ci): use TelegramRecordingChannel in telegram-specific test
The test process_channel_message_executes_tool_calls_instead_of_sending_raw_json
sent messages on channel "telegram" but registered RecordingChannel (name:
"test-channel"), causing the channel lookup to return None and no messages to
be sent.
* fix(jira): prevent panics on short dates, fix dedup bug, normalize base_url
- Add date_prefix() helper to safely slice date strings instead of
panicking on empty or short strings from the Jira API.
- Replace Vec::dedup() with HashSet-based retain in extract_emails()
to correctly deduplicate non-adjacent duplicates.
- Strip trailing slashes from base_url during construction to prevent
double-slash URLs.
- Add tests for date_prefix and non-adjacent email dedup.
---------
Co-authored-by: Anatolii <anatolii@Anatoliis-MacBook.local>
Co-authored-by: Anatolii <anatolii.fesiuk@gmail.com>
Adds text-to-speech output to the Telegram channel, mirroring the
existing WhatsApp Web voice-chat implementation. When a user sends a
voice note (transcribed via STT), the channel enters voice-chat mode
and subsequent agent replies are synthesised into a Telegram voice note
via the configured TTS provider, in addition to the normal text reply.
Sending a text message exits voice-chat mode.
Implementation details:
- Add `tts_config`, `voice_chats`, and `pending_voice` fields to
`TelegramChannel`
- Add `with_tts()` builder method, gated on `config.enabled`
- Track voice-chat state: enter on successful STT transcription, exit
on incoming text message
- `synthesize_and_send_voice()` static method: synthesises audio via
`TtsManager` and uploads to Telegram using `sendVoice` multipart API
- 10-second debounce in `send()` ensures only the final substantive
reply in a tool chain gets a voice note (skips JSON, code blocks,
URLs, short status messages)
- Wire `.with_tts(config.tts.clone())` into both Telegram construction
sites in the channel factory
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Replace relative docs/assets/zeroclaw-banner.png paths with absolute
raw.githubusercontent.com URLs in all 31 README files so the banner
renders correctly regardless of where the README is viewed
- Switch web dashboard favicon and logos from logo.png to zeroclaw-trans.png
- Add zeroclaw-trans.png and zeroclaw-banner.png assets
- Update build.rs to track new dashboard asset
- Fix missing autonomy_level in new test + Box::pin large future
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
build_system_prompt_with_mode was discarding the autonomy_level
parameter, passing None to build_system_prompt_with_mode_and_autonomy.
This caused full-autonomy prompts to still include "ask before acting"
instructions. Convert the level to an AutonomyConfig and pass it through.
Resolve conflict in src/channels/mod.rs Safety section. Keeps the
PR's AutonomyConfig-based prompt construction (build_system_prompt_with_mode_and_autonomy)
while incorporating master's granular safety rules (conditional
destructive-command and ask-before-acting lines based on autonomy level).
Also fixes missing autonomy_level arg in refresh-skills test and removes
duplicate autonomy.level args from auto-merged call sites.
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
- 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>
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.