From f67abd9fc8b85efd6ef1883ed291bdc080617c55 Mon Sep 17 00:00:00 2001
From: argenis de la rosa
Date: Wed, 11 Mar 2026 15:48:43 -0400
Subject: [PATCH 01/15] docs(readme): remove retired social links
Remove WeChat, Xiaohongshu, and Telegram from the README social badges and aligned locale entry points.
---
README.el.md | 3 ---
README.fr.md | 5 +----
README.ja.md | 3 ---
README.md | 5 +----
README.ru.md | 3 ---
README.vi.md | 5 +----
README.zh-CN.md | 3 ---
7 files changed, 3 insertions(+), 24 deletions(-)
diff --git a/README.el.md b/README.el.md
index 6d7850df2..8a96eab12 100644
--- a/README.el.md
+++ b/README.el.md
@@ -14,9 +14,6 @@
-
-
-
diff --git a/README.fr.md b/README.fr.md
index 3ee852542..f26ce3191 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -14,9 +14,6 @@
-
-
-
@@ -94,7 +91,7 @@ Utilisez ce tableau pour les avis importants (changements incompatibles, avis de
| Date (UTC) | Niveau | Avis | Action |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Critique_ | Nous ne sommes **pas affiliés** à `openagen/zeroclaw` ou `zeroclaw.org`. Le domaine `zeroclaw.org` pointe actuellement vers le fork `openagen/zeroclaw`, et ce domaine/dépôt usurpe l'identité de notre site web/projet officiel. | Ne faites pas confiance aux informations, binaires, levées de fonds ou annonces provenant de ces sources. Utilisez uniquement [ce dépôt](https://github.com/zeroclaw-labs/zeroclaw) et nos comptes sociaux vérifiés. |
-| 2026-02-21 | _Important_ | Notre site officiel est désormais en ligne : [zeroclawlabs.ai](https://zeroclawlabs.ai). Merci pour votre patience pendant cette attente. Nous constatons toujours des tentatives d'usurpation : ne participez à aucune activité d'investissement/financement au nom de ZeroClaw si elle n'est pas publiée via nos canaux officiels. | Utilisez [ce dépôt](https://github.com/zeroclaw-labs/zeroclaw) comme source unique de vérité. Suivez [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (groupe)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), et [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) pour les mises à jour officielles. |
+| 2026-02-21 | _Important_ | Notre site officiel est désormais en ligne : [zeroclawlabs.ai](https://zeroclawlabs.ai). Merci pour votre patience pendant cette attente. Nous constatons toujours des tentatives d'usurpation : ne participez à aucune activité d'investissement/financement au nom de ZeroClaw si elle n'est pas publiée via nos canaux officiels. | Utilisez [ce dépôt](https://github.com/zeroclaw-labs/zeroclaw) comme source unique de vérité. Suivez [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (groupe)](https://www.facebook.com/groups/zeroclaw), et [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) pour les mises à jour officielles. |
| 2026-02-19 | _Important_ | Anthropic a mis à jour les conditions d'utilisation de l'authentification et des identifiants le 2026-02-19. L'authentification OAuth (Free, Pro, Max) est exclusivement destinée à Claude Code et Claude.ai ; l'utilisation de tokens OAuth de Claude Free/Pro/Max dans tout autre produit, outil ou service (y compris Agent SDK) n'est pas autorisée et peut violer les Conditions d'utilisation grand public. | Veuillez temporairement éviter les intégrations OAuth de Claude Code pour prévenir toute perte potentielle. Clause originale : [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Fonctionnalités
diff --git a/README.ja.md b/README.ja.md
index 6f1a86d29..fb1452e29 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -13,9 +13,6 @@
-
-
-
diff --git a/README.md b/README.md
index 295179b91..5c7c3434b 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,6 @@
-
-
-
@@ -94,7 +91,7 @@ Use this board for important notices (breaking changes, security advisories, mai
| Date (UTC) | Level | Notice | Action |
| ---------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Critical_ | We are **not affiliated** with `openagen/zeroclaw`, `zeroclaw.org` or `zeroclaw.net`. The `zeroclaw.org` and `zeroclaw.net` domains currently points to the `openagen/zeroclaw` fork, and that domain/repository are impersonating our official website/project. | Do not trust information, binaries, fundraising, or announcements from those sources. Use only [this repository](https://github.com/zeroclaw-labs/zeroclaw) and our verified social accounts. |
-| 2026-02-21 | _Important_ | Our official website is now live: [zeroclawlabs.ai](https://zeroclawlabs.ai). Thanks for your patience while we prepared the launch. We are still seeing impersonation attempts, so do **not** join any investment or fundraising activity claiming the ZeroClaw name unless it is published through our official channels. | Use [this repository](https://github.com/zeroclaw-labs/zeroclaw) as the single source of truth. Follow [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (Group)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), and [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) for official updates. |
+| 2026-02-21 | _Important_ | Our official website is now live: [zeroclawlabs.ai](https://zeroclawlabs.ai). Thanks for your patience while we prepared the launch. We are still seeing impersonation attempts, so do **not** join any investment or fundraising activity claiming the ZeroClaw name unless it is published through our official channels. | Use [this repository](https://github.com/zeroclaw-labs/zeroclaw) as the single source of truth. Follow [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (Group)](https://www.facebook.com/groups/zeroclaw), and [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) for official updates. |
| 2026-02-19 | _Important_ | Anthropic updated the Authentication and Credential Use terms on 2026-02-19. Claude Code OAuth tokens (Free, Pro, Max) are intended exclusively for Claude Code and Claude.ai; using OAuth tokens from Claude Free/Pro/Max in any other product, tool, or service (including Agent SDK) is not permitted and may violate the Consumer Terms of Service. | Please temporarily avoid Claude Code OAuth integrations to prevent potential loss. Original clause: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Features
diff --git a/README.ru.md b/README.ru.md
index 0dd3dd6f8..8e7079c53 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -13,9 +13,6 @@
-
-
-
diff --git a/README.vi.md b/README.vi.md
index 068ad75a7..f7a9ecc73 100644
--- a/README.vi.md
+++ b/README.vi.md
@@ -14,9 +14,6 @@
-
-
-
@@ -94,7 +91,7 @@ Bảng này dành cho các thông báo quan trọng (thay đổi không tương
| Ngày (UTC) | Mức độ | Thông báo | Hành động |
|---|---|---|---|
| 2026-02-19 | _Nghiêm trọng_ | Chúng tôi **không có liên kết** với `openagen/zeroclaw` hoặc `zeroclaw.org`. Tên miền `zeroclaw.org` hiện đang trỏ đến fork `openagen/zeroclaw`, và tên miền/repository đó đang mạo danh website/dự án chính thức của chúng tôi. | Không tin tưởng thông tin, binary, gây quỹ, hay thông báo từ các nguồn đó. Chỉ sử dụng [repository này](https://github.com/zeroclaw-labs/zeroclaw) và các tài khoản mạng xã hội đã được xác minh của chúng tôi. |
-| 2026-02-21 | _Quan trọng_ | Website chính thức của chúng tôi đã ra mắt: [zeroclawlabs.ai](https://zeroclawlabs.ai). Cảm ơn mọi người đã kiên nhẫn chờ đợi. Chúng tôi vẫn đang ghi nhận các nỗ lực mạo danh, vì vậy **không** tham gia bất kỳ hoạt động đầu tư hoặc gây quỹ nào nhân danh ZeroClaw nếu thông tin đó không được công bố qua các kênh chính thức của chúng tôi. | Sử dụng [repository này](https://github.com/zeroclaw-labs/zeroclaw) làm nguồn thông tin duy nhất đáng tin cậy. Theo dõi [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (nhóm)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), và [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) để nhận cập nhật chính thức. |
+| 2026-02-21 | _Quan trọng_ | Website chính thức của chúng tôi đã ra mắt: [zeroclawlabs.ai](https://zeroclawlabs.ai). Cảm ơn mọi người đã kiên nhẫn chờ đợi. Chúng tôi vẫn đang ghi nhận các nỗ lực mạo danh, vì vậy **không** tham gia bất kỳ hoạt động đầu tư hoặc gây quỹ nào nhân danh ZeroClaw nếu thông tin đó không được công bố qua các kênh chính thức của chúng tôi. | Sử dụng [repository này](https://github.com/zeroclaw-labs/zeroclaw) làm nguồn thông tin duy nhất đáng tin cậy. Theo dõi [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (nhóm)](https://www.facebook.com/groups/zeroclaw), và [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) để nhận cập nhật chính thức. |
| 2026-02-19 | _Quan trọng_ | Anthropic đã cập nhật điều khoản Xác thực và Sử dụng Thông tin xác thực vào ngày 2026-02-19. Xác thực OAuth (Free, Pro, Max) được dành riêng cho Claude Code và Claude.ai; việc sử dụng OAuth token từ Claude Free/Pro/Max trong bất kỳ sản phẩm, công cụ hay dịch vụ nào khác (bao gồm Agent SDK) đều không được phép và có thể vi phạm Điều khoản Dịch vụ cho Người tiêu dùng. | Vui lòng tạm thời tránh tích hợp Claude Code OAuth để ngăn ngừa khả năng mất mát. Điều khoản gốc: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Tính năng
diff --git a/README.zh-CN.md b/README.zh-CN.md
index e18aae51b..ee13acb60 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -13,9 +13,6 @@
-
-
-
From 95da0062de7e3095a563cad7f06c4ef8f6115f4f Mon Sep 17 00:00:00 2001
From: SimianAstronaut7 <79373020+SimianAstronaut7@users.noreply.github.com>
Date: Wed, 11 Mar 2026 22:23:38 +0000
Subject: [PATCH 02/15] chore: bump version to 0.1.9 for stable release (#3225)
Co-authored-by: Claude Opus 4.6
---
Cargo.lock | 435 ++++++++++++++++++++++-------------------------------
Cargo.toml | 2 +-
2 files changed, 178 insertions(+), 259 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 0d0c52431..da3250b4a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,7 +11,7 @@ dependencies = [
"macroific",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -188,7 +188,7 @@ dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -268,9 +268,9 @@ dependencies = [
[[package]]
name = "async-compression"
-version = "0.4.40"
+version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2"
+checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
dependencies = [
"compression-codecs",
"compression-core",
@@ -349,7 +349,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -360,7 +360,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -377,9 +377,9 @@ dependencies = [
[[package]]
name = "async-wsocket"
-version = "0.13.1"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a7d8c7d34a225ba919dd9ba44d4b9106d20142da545e086be8ae21d1897e043"
+checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069"
dependencies = [
"async-utility",
"futures",
@@ -414,9 +414,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
-version = "1.15.4"
+version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
+checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -424,9 +424,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.37.1"
+version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
+checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
dependencies = [
"cc",
"cmake",
@@ -496,7 +496,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -599,7 +599,7 @@ checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -678,9 +678,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.19.1"
+version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytecount"
@@ -705,7 +705,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -927,7 +927,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -947,9 +947,9 @@ dependencies = [
[[package]]
name = "cobs"
-version = "0.5.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ef0193218d365c251b5b9297f9911a908a8ddd2ebd3a36cc5d0ef0f63aee9e"
+checksum = "dd93fd2c1b27acd030440c9dbd9d14c1122aad622374fe05a670b67a4bc034be"
dependencies = [
"heapless",
"thiserror 2.0.18",
@@ -1265,7 +1265,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1289,7 +1289,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1300,7 +1300,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1391,7 +1391,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1405,7 +1405,7 @@ dependencies = [
"macroific",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1420,9 +1420,9 @@ dependencies = [
[[package]]
name = "deranged"
-version = "0.5.6"
+version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
@@ -1464,7 +1464,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1476,7 +1476,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
- "syn 2.0.116",
+ "syn 2.0.117",
"unicode-xid",
]
@@ -1542,7 +1542,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1562,7 +1562,7 @@ checksum = "11772ed3eb3db124d826f3abeadf5a791a557f62c19b123e3f07288158a71fdd"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1675,7 +1675,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -1844,7 +1844,7 @@ dependencies = [
"macroific",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -2022,7 +2022,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -2095,20 +2095,20 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "r-efi",
+ "r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
-version = "0.4.1"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
- "r-efi",
+ "r-efi 6.0.0",
"rand_core 0.10.0",
"wasip2",
"wasip3",
@@ -2250,7 +2250,7 @@ checksum = "149e3ea90eb5a26ad354cfe3cb7f7401b9329032d0235f2687d03a35f30e5d4c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -2325,17 +2325,17 @@ dependencies = [
[[package]]
name = "hidapi"
-version = "2.6.4"
+version = "2.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "565dd4c730b8f8b2c0fb36df6be12e5470ae10895ddcc4e9dcfbfb495de202b0"
+checksum = "d1b71e1f4791fb9e93b9d7ee03d70b501ab48f6151432fbcadeabc30fe15396e"
dependencies = [
"basic-udev",
"cc",
"cfg-if",
"libc",
- "nix 0.27.1",
+ "nix 0.30.1",
"pkg-config",
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2662,7 +2662,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -2728,9 +2728,9 @@ checksum = "365a784774bb381e8c19edb91190a90d7f2625e057b55de2bc0f6b57bc779ff2"
[[package]]
name = "image"
-version = "0.25.9"
+version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -2782,7 +2782,7 @@ checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -2860,9 +2860,9 @@ dependencies = [
[[package]]
name = "ipnet"
-version = "2.11.0"
+version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "iri-string"
@@ -2934,9 +2934,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.85"
+version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
+checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -3047,11 +3047,10 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
-version = "0.1.12"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
+checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [
- "bitflags 2.11.0",
"libc",
]
@@ -3066,12 +3065,6 @@ dependencies = [
"vcpkg",
]
-[[package]]
-name = "linux-raw-sys"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
-
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -3195,7 +3188,7 @@ dependencies = [
"proc-macro2",
"quote",
"sealed",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -3207,7 +3200,7 @@ dependencies = [
"proc-macro2",
"quote",
"sealed",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -3220,7 +3213,7 @@ dependencies = [
"macroific_core",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -3257,7 +3250,7 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -3295,7 +3288,7 @@ dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -3564,7 +3557,7 @@ dependencies = [
"macroific",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -3608,7 +3601,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -3676,9 +3669,9 @@ dependencies = [
[[package]]
name = "moka"
-version = "0.12.13"
+version = "0.12.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e"
+checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b"
dependencies = [
"async-lock",
"crossbeam-channel",
@@ -3696,9 +3689,9 @@ dependencies = [
[[package]]
name = "moxcms"
-version = "0.7.11"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
@@ -3739,17 +3732,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "nix"
-version = "0.27.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
-dependencies = [
- "bitflags 2.11.0",
- "cfg-if",
- "libc",
-]
-
[[package]]
name = "nix"
version = "0.29.0"
@@ -3927,15 +3909,15 @@ dependencies = [
[[package]]
name = "nusb"
-version = "0.2.2"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5750d884c774a2862b0049b0318aea27cecc9e873485540af5ed8ab8841247da"
+checksum = "8a330b3bc7f8b4fc729a4c63164b3927eeeaced198222a3ce6b8b6e034851b7a"
dependencies = [
"core-foundation",
"core-foundation-sys",
"futures-core",
"io-kit-sys 0.5.0",
- "linux-raw-sys 0.11.0",
+ "linux-raw-sys",
"log",
"once_cell",
"rustix",
@@ -4307,29 +4289,29 @@ dependencies = [
[[package]]
name = "pin-project"
-version = "1.1.10"
+version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
-version = "1.1.10"
+version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
name = "pin-project-lite"
-version = "0.2.16"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
@@ -4530,7 +4512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -4585,9 +4567,9 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
-version = "3.4.0"
+version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
@@ -4683,7 +4665,7 @@ dependencies = [
"itertools 0.14.0",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -4696,7 +4678,7 @@ dependencies = [
"itertools 0.14.0",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -4740,9 +4722,9 @@ dependencies = [
[[package]]
name = "pulldown-cmark"
-version = "0.13.0"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
+checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6"
dependencies = [
"bitflags 2.11.0",
"memchr",
@@ -4758,12 +4740,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "pxfm"
-version = "0.1.27"
+version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
-dependencies = [
- "num-traits",
-]
+checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
[[package]]
name = "qrcode"
@@ -4831,9 +4810,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.44"
+version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
@@ -4850,6 +4829,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
[[package]]
name = "radium"
version = "0.7.0"
@@ -4884,7 +4869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [
"chacha20 0.10.0",
- "getrandom 0.4.1",
+ "getrandom 0.4.2",
"rand_core 0.10.0",
]
@@ -5019,7 +5004,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -5047,9 +5032,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.9"
+version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reqwest"
@@ -5293,7 +5278,7 @@ dependencies = [
"quote",
"ruma-identifiers-validation",
"serde",
- "syn 2.0.116",
+ "syn 2.0.117",
"toml 0.9.12+spec-1.1.0",
]
@@ -5347,7 +5332,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
- "syn 2.0.116",
+ "syn 2.0.117",
"walkdir",
]
@@ -5385,7 +5370,7 @@ dependencies = [
"bitflags 2.11.0",
"errno",
"libc",
- "linux-raw-sys 0.12.1",
+ "linux-raw-sys",
"windows-sys 0.61.2",
]
@@ -5480,9 +5465,9 @@ dependencies = [
[[package]]
name = "schannel"
-version = "0.1.28"
+version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
@@ -5509,7 +5494,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -5544,7 +5529,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -5569,9 +5554,9 @@ dependencies = [
[[package]]
name = "security-framework"
-version = "3.6.0"
+version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.0",
"core-foundation",
@@ -5582,9 +5567,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
-version = "2.16.0"
+version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
@@ -5659,7 +5644,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -5670,7 +5655,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -5752,9 +5737,9 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.16.1"
+version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
+checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9"
dependencies = [
"base64",
"chrono",
@@ -5902,12 +5887,12 @@ dependencies = [
[[package]]
name = "socket2"
-version = "0.6.2"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -6011,7 +5996,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -6033,9 +6018,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.116"
+version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -6059,7 +6044,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -6076,12 +6061,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
-version = "3.26.0"
+version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.4.1",
+ "getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@@ -6124,7 +6109,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -6135,7 +6120,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -6240,13 +6225,13 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.6.0"
+version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -6410,7 +6395,7 @@ dependencies = [
"serde_spanned",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
- "winnow 0.7.14",
+ "winnow 0.7.15",
]
[[package]]
@@ -6425,7 +6410,7 @@ dependencies = [
"toml_datetime 1.0.0+spec-1.1.0",
"toml_parser",
"toml_writer",
- "winnow 0.7.14",
+ "winnow 0.7.15",
]
[[package]]
@@ -6448,14 +6433,14 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.23.10+spec-1.0.0"
+version = "0.25.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
+checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
dependencies = [
"indexmap",
- "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_datetime 1.0.0+spec-1.1.0",
"toml_parser",
- "winnow 0.7.14",
+ "winnow 0.7.15",
]
[[package]]
@@ -6464,7 +6449,7 @@ version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
- "winnow 0.7.14",
+ "winnow 0.7.15",
]
[[package]]
@@ -6475,9 +6460,9 @@ checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tonic"
-version = "0.14.4"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f32a6f80051a4111560201420c7885d0082ba9efe2ab61875c587bb6b18b9a0"
+checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
dependencies = [
"async-trait",
"base64",
@@ -6496,9 +6481,9 @@ dependencies = [
[[package]]
name = "tonic-prost"
-version = "0.14.4"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f86539c0089bfd09b1f8c0ab0239d80392af74c21bc9e0f15e1b4aca4c1647f"
+checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
dependencies = [
"bytes",
"prost 0.14.3",
@@ -6574,7 +6559,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -6698,7 +6683,7 @@ checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -6761,9 +6746,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
-version = "1.0.23"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-normalization"
@@ -6900,7 +6885,7 @@ version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
- "getrandom 0.4.1",
+ "getrandom 0.4.2",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -7074,7 +7059,7 @@ checksum = "75c03f610c9bc960e653d5d6d2a4cced9013bedbe5e6e8948787bbd418e4137c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -7240,9 +7225,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.108"
+version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
+checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
@@ -7253,9 +7238,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.58"
+version = "0.4.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
+checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
dependencies = [
"cfg-if",
"futures-util",
@@ -7267,9 +7252,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.108"
+version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
+checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -7277,22 +7262,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.108"
+version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
+checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.108"
+version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
+checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
@@ -7364,9 +7349,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.85"
+version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
+checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -7519,7 +7504,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -7530,7 +7515,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -7557,15 +7542,6 @@ dependencies = [
"windows-link",
]
-[[package]]
-name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-dependencies = [
- "windows-targets 0.48.5",
-]
-
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -7602,21 +7578,6 @@ dependencies = [
"windows-link",
]
-[[package]]
-name = "windows-targets"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
-dependencies = [
- "windows_aarch64_gnullvm 0.48.5",
- "windows_aarch64_msvc 0.48.5",
- "windows_i686_gnu 0.48.5",
- "windows_i686_msvc 0.48.5",
- "windows_x86_64_gnu 0.48.5",
- "windows_x86_64_gnullvm 0.48.5",
- "windows_x86_64_msvc 0.48.5",
-]
-
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -7650,12 +7611,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -7668,12 +7623,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
-
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -7686,12 +7635,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
-
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -7716,12 +7659,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
-
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -7734,12 +7671,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
-
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -7752,12 +7683,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
-
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -7770,12 +7695,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
-
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -7799,9 +7718,9 @@ dependencies = [
[[package]]
name = "winnow"
-version = "0.7.14"
+version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
@@ -7859,7 +7778,7 @@ dependencies = [
"heck",
"indexmap",
"prettyplease",
- "syn 2.0.116",
+ "syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -7875,7 +7794,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -7987,7 +7906,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
"synstructure",
]
@@ -7999,13 +7918,13 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
"synstructure",
]
[[package]]
name = "zeroclaw"
-version = "0.1.7"
+version = "0.1.9"
dependencies = [
"anyhow",
"async-imap",
@@ -8115,22 +8034,22 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.39"
+version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
+checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.39"
+version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
+checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -8150,7 +8069,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
"synstructure",
]
@@ -8171,7 +8090,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -8215,7 +8134,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
@@ -8226,14 +8145,14 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.116",
+ "syn 2.0.117",
]
[[package]]
name = "zlib-rs"
-version = "0.6.2"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8"
+checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
[[package]]
name = "zmij"
@@ -8249,9 +8168,9 @@ checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-jpeg"
-version = "0.5.12"
+version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
+checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c"
dependencies = [
"zune-core",
]
diff --git a/Cargo.toml b/Cargo.toml
index b6a1f7c6e..550df7536 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ resolver = "2"
[package]
name = "zeroclaw"
-version = "0.1.7"
+version = "0.1.9"
edition = "2021"
authors = ["theonlyhennygod"]
license = "MIT OR Apache-2.0"
From d0edcec1f92cc663d706edb319519234fa99a416 Mon Sep 17 00:00:00 2001
From: Abdullah Imad
Date: Wed, 11 Mar 2026 19:04:23 -0400
Subject: [PATCH 03/15] feat(prompt): refresh stale datetime (#3223)
The system prompt is built once at daemon startup and cached. The
"Current Date & Time" section becomes stale immediately. This patch
replaces it with a fresh timestamp every time build_channel_system_prompt
is called (i.e. per incoming message).
Co-authored-by: Claude Opus 4.6
---
src/channels/mod.rs | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/src/channels/mod.rs b/src/channels/mod.rs
index f93a40a0e..42e26dc07 100644
--- a/src/channels/mod.rs
+++ b/src/channels/mod.rs
@@ -426,6 +426,25 @@ fn build_channel_system_prompt(
) -> String {
let mut prompt = base_prompt.to_string();
+ // Refresh the stale datetime in the cached system prompt
+ {
+ let now = chrono::Local::now();
+ let fresh = format!(
+ "## Current Date & Time\n\n{} ({})\n",
+ now.format("%Y-%m-%d %H:%M:%S"),
+ now.format("%Z"),
+ );
+ if let Some(start) = prompt.find("## Current Date & Time\n\n") {
+ // Find the end of this section (next "## " heading or end of string)
+ let rest = &prompt[start + 24..]; // skip past "## Current Date & Time\n\n"
+ let section_end = rest
+ .find("\n## ")
+ .map(|i| start + 24 + i)
+ .unwrap_or(prompt.len());
+ prompt.replace_range(start..section_end, fresh.trim_end());
+ }
+ }
+
if let Some(instructions) = channel_delivery_instructions(channel_name) {
if prompt.is_empty() {
prompt = instructions.to_string();
From bd70c0f45b1f1d45c9a095f6ccb675afb9a32822 Mon Sep 17 00:00:00 2001
From: Abdullah Imad
Date: Wed, 11 Mar 2026 19:07:34 -0400
Subject: [PATCH 04/15] feat(observer): live tool call notifications (#3221)
Add ChannelNotifyObserver that wraps the observer to forward tool-call
events as real-time threaded messages on messaging channels. Include
tool arguments (truncated) in ToolCallStart events for better
visibility into what tools are doing. Auto-thread final replies when
tools were used.
Co-authored-by: Claude Opus 4.6
Co-authored-by: Argenis
---
src/agent/loop_.rs | 5 ++
src/channels/mod.rs | 102 +++++++++++++++++++++++++++++++-
src/gateway/sse.rs | 2 +-
src/observability/log.rs | 2 +-
src/observability/otel.rs | 1 +
src/observability/prometheus.rs | 2 +-
src/observability/traits.rs | 2 +-
src/observability/verbose.rs | 3 +-
8 files changed, 112 insertions(+), 7 deletions(-)
diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs
index 00da58a88..3da11d629 100644
--- a/src/agent/loop_.rs
+++ b/src/agent/loop_.rs
@@ -1922,8 +1922,13 @@ async fn execute_one_tool(
observer: &dyn Observer,
cancellation_token: Option<&CancellationToken>,
) -> Result {
+ let args_summary = {
+ let raw = call_arguments.to_string();
+ if raw.len() > 300 { format!("{}…", &raw[..300]) } else { raw }
+ };
observer.record_event(&ObserverEvent::ToolCallStart {
tool: call_name.to_string(),
+ arguments: Some(args_summary),
});
let start = Instant::now();
diff --git a/src/channels/mod.rs b/src/channels/mod.rs
index 42e26dc07..2d999d698 100644
--- a/src/channels/mod.rs
+++ b/src/channels/mod.rs
@@ -75,6 +75,7 @@ use crate::config::Config;
use crate::identity;
use crate::memory::{self, Memory};
use crate::observability::{self, runtime_trace, Observer};
+use crate::observability::traits::{ObserverEvent, ObserverMetric};
use crate::providers::{self, ChatMessage, Provider};
use crate::runtime;
use crate::security::SecurityPolicy;
@@ -91,6 +92,61 @@ use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant, SystemTime};
use tokio_util::sync::CancellationToken;
+/// Observer wrapper that forwards tool-call events to a channel sender
+/// for real-time threaded notifications.
+struct ChannelNotifyObserver {
+ inner: Arc,
+ tx: tokio::sync::mpsc::UnboundedSender,
+ tools_used: AtomicBool,
+}
+
+impl Observer for ChannelNotifyObserver {
+ fn record_event(&self, event: &ObserverEvent) {
+ match event {
+ ObserverEvent::ToolCallStart { tool, arguments } => {
+ self.tools_used.store(true, Ordering::Relaxed);
+ let detail = match arguments {
+ Some(args) if !args.is_empty() => {
+ if let Ok(v) = serde_json::from_str::(args) {
+ if let Some(cmd) = v.get("command").and_then(|c| c.as_str()) {
+ format!(": `{}`", if cmd.len() > 200 { &cmd[..200] } else { cmd })
+ } else if let Some(q) = v.get("query").and_then(|c| c.as_str()) {
+ format!(": {}", if q.len() > 200 { &q[..200] } else { q })
+ } else if let Some(p) = v.get("path").and_then(|c| c.as_str()) {
+ format!(": {p}")
+ } else if let Some(u) = v.get("url").and_then(|c| c.as_str()) {
+ format!(": {u}")
+ } else {
+ let s = args.to_string();
+ if s.len() > 120 { format!(": {}…", &s[..120]) } else { format!(": {s}") }
+ }
+ } else {
+ let s = args.to_string();
+ if s.len() > 120 { format!(": {}…", &s[..120]) } else { format!(": {s}") }
+ }
+ }
+ _ => String::new(),
+ };
+ let _ = self.tx.send(format!("\u{1F527} `{tool}`{detail}"));
+ }
+ _ => {}
+ }
+ self.inner.record_event(event);
+ }
+ fn record_metric(&self, metric: &ObserverMetric) {
+ self.inner.record_metric(metric);
+ }
+ fn flush(&self) {
+ self.inner.flush();
+ }
+ fn name(&self) -> &str {
+ "channel-notify"
+ }
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+}
+
/// Per-sender conversation history for channel messages.
type ConversationHistoryMap = Arc>>>;
/// Maximum history messages to keep per sender.
@@ -1601,7 +1657,7 @@ async fn process_channel_message(
);
// ── Hook: on_message_received (modifying) ────────────
- let msg = if let Some(hooks) = &ctx.hooks {
+ let mut msg = if let Some(hooks) = &ctx.hooks {
match hooks.run_on_message_received(msg).await {
crate::hooks::HookResult::Cancel(reason) => {
tracing::info!(%reason, "incoming message dropped by hook");
@@ -1783,6 +1839,37 @@ async fn process_channel_message(
_ => None,
};
+ // Wrap observer to forward tool events as live thread messages
+ let (notify_tx, mut notify_rx) = tokio::sync::mpsc::unbounded_channel::();
+ let notify_observer: Arc = Arc::new(ChannelNotifyObserver {
+ inner: Arc::clone(&ctx.observer),
+ tx: notify_tx,
+ tools_used: AtomicBool::new(false),
+ });
+ let notify_observer_flag = Arc::clone(¬ify_observer);
+ let notify_channel = target_channel.clone();
+ let notify_reply_target = msg.reply_target.clone();
+ let notify_thread_root = msg.id.clone();
+ let notify_task = if msg.channel != "cli" {
+ Some(tokio::spawn(async move {
+ let thread_ts = Some(notify_thread_root);
+ while let Some(text) = notify_rx.recv().await {
+ if let Some(ref ch) = notify_channel {
+ let _ = ch
+ .send(
+ &SendMessage::new(&text, ¬ify_reply_target)
+ .in_thread(thread_ts.clone()),
+ )
+ .await;
+ }
+ }
+ }))
+ } else {
+ Some(tokio::spawn(async move {
+ while notify_rx.recv().await.is_some() {}
+ }))
+ };
+
// Record history length before tool loop so we can extract tool context after.
let history_len_before_tools = history.len();
@@ -1801,7 +1888,7 @@ async fn process_channel_message(
active_provider.as_ref(),
&mut history,
ctx.tools_registry.as_ref(),
- ctx.observer.as_ref(),
+ notify_observer.as_ref() as &dyn Observer,
route.provider.as_str(),
route.model.as_str(),
runtime_defaults.temperature,
@@ -1826,6 +1913,17 @@ async fn process_channel_message(
let _ = handle.await;
}
+ // Thread the final reply only if tools were used (multi-message response)
+ if notify_observer_flag.tools_used.load(Ordering::Relaxed) && msg.channel != "cli" {
+ msg.thread_ts = Some(msg.id.clone());
+ }
+ // Drop the notify sender so the forwarder task finishes
+ drop(notify_observer);
+ drop(notify_observer_flag);
+ if let Some(handle) = notify_task {
+ let _ = handle.await;
+ }
+
if let Some(token) = typing_cancellation.as_ref() {
token.cancel();
}
diff --git a/src/gateway/sse.rs b/src/gateway/sse.rs
index e68b81e28..463d20e0b 100644
--- a/src/gateway/sse.rs
+++ b/src/gateway/sse.rs
@@ -98,7 +98,7 @@ impl crate::observability::Observer for BroadcastObserver {
"success": success,
"timestamp": chrono::Utc::now().to_rfc3339(),
}),
- crate::observability::ObserverEvent::ToolCallStart { tool } => serde_json::json!({
+ crate::observability::ObserverEvent::ToolCallStart { tool, .. } => serde_json::json!({
"type": "tool_call_start",
"tool": tool,
"timestamp": chrono::Utc::now().to_rfc3339(),
diff --git a/src/observability/log.rs b/src/observability/log.rs
index e9668679c..e4b4a4ddb 100644
--- a/src/observability/log.rs
+++ b/src/observability/log.rs
@@ -27,7 +27,7 @@ impl Observer for LogObserver {
let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
info!(provider = %provider, model = %model, duration_ms = ms, tokens = ?tokens_used, cost_usd = ?cost_usd, "agent.end");
}
- ObserverEvent::ToolCallStart { tool } => {
+ ObserverEvent::ToolCallStart { tool, .. } => {
info!(tool = %tool, "tool.start");
}
ObserverEvent::ToolCall {
diff --git a/src/observability/otel.rs b/src/observability/otel.rs
index 613232c8a..07faa977b 100644
--- a/src/observability/otel.rs
+++ b/src/observability/otel.rs
@@ -434,6 +434,7 @@ mod tests {
});
obs.record_event(&ObserverEvent::ToolCallStart {
tool: "shell".into(),
+ arguments: None,
});
obs.record_event(&ObserverEvent::ToolCall {
tool: "shell".into(),
diff --git a/src/observability/prometheus.rs b/src/observability/prometheus.rs
index 08cecf320..4fbb1c67a 100644
--- a/src/observability/prometheus.rs
+++ b/src/observability/prometheus.rs
@@ -221,7 +221,7 @@ impl Observer for PrometheusObserver {
.inc_by(*output);
}
}
- ObserverEvent::ToolCallStart { tool: _ }
+ ObserverEvent::ToolCallStart { .. }
| ObserverEvent::TurnComplete
| ObserverEvent::LlmRequest { .. } => {}
ObserverEvent::ToolCall {
diff --git a/src/observability/traits.rs b/src/observability/traits.rs
index 3d4542e66..82235184c 100644
--- a/src/observability/traits.rs
+++ b/src/observability/traits.rs
@@ -40,7 +40,7 @@ pub enum ObserverEvent {
cost_usd: Option,
},
/// A tool call is about to be executed.
- ToolCallStart { tool: String },
+ ToolCallStart { tool: String, arguments: Option },
/// A tool call has completed with a success/failure outcome.
ToolCall {
tool: String,
diff --git a/src/observability/verbose.rs b/src/observability/verbose.rs
index 07fa8545f..12271c0f8 100644
--- a/src/observability/verbose.rs
+++ b/src/observability/verbose.rs
@@ -33,7 +33,7 @@ impl Observer for VerboseObserver {
let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
eprintln!("< Receive (success={success}, duration_ms={ms})");
}
- ObserverEvent::ToolCallStart { tool } => {
+ ObserverEvent::ToolCallStart { tool, .. } => {
eprintln!("> Tool {tool}");
}
ObserverEvent::ToolCall {
@@ -92,6 +92,7 @@ mod tests {
});
obs.record_event(&ObserverEvent::ToolCallStart {
tool: "shell".into(),
+ arguments: None,
});
obs.record_event(&ObserverEvent::ToolCall {
tool: "shell".into(),
From 248348bd80c446006fe8cc404e9e24064fc9aa23 Mon Sep 17 00:00:00 2001
From: Abdullah Imad
Date: Wed, 11 Mar 2026 19:07:49 -0400
Subject: [PATCH 05/15] feat(matrix): reaction and threading support (#3219)
Implement add_reaction/remove_reaction for Matrix channel using
ReactionEventContent and redaction. Add threading support via
Relation::Thread in send() and thread_ts extraction from incoming
messages, enabling threaded conversations.
Co-authored-by: Claude Opus 4.6
Co-authored-by: Argenis
---
src/channels/matrix.rs | 87 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 83 insertions(+), 4 deletions(-)
diff --git a/src/channels/matrix.rs b/src/channels/matrix.rs
index e6d4c3836..42c7e8335 100644
--- a/src/channels/matrix.rs
+++ b/src/channels/matrix.rs
@@ -1,12 +1,17 @@
use crate::channels::traits::{Channel, ChannelMessage, SendMessage};
use async_trait::async_trait;
+use std::collections::HashMap;
use matrix_sdk::{
authentication::matrix::MatrixSession,
config::SyncSettings,
ruma::{
events::room::message::{
- MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
+ MessageType, OriginalSyncRoomMessageEvent, Relation, RoomMessageEventContent,
},
+ events::reaction::ReactionEventContent,
+ events::relation::{Annotation, InReplyTo, Thread},
+ events::room::MediaSource,
+ OwnedEventId,
OwnedRoomId, OwnedUserId,
},
Client as MatrixSdkClient, LoopCtrl, Room, RoomState, SessionMeta, SessionTokens,
@@ -31,6 +36,7 @@ pub struct MatrixChannel {
resolved_room_id_cache: Arc>>,
sdk_client: Arc>,
http_client: Client,
+ reaction_events: Arc>>,
}
impl std::fmt::Debug for MatrixChannel {
@@ -163,6 +169,7 @@ impl MatrixChannel {
resolved_room_id_cache: Arc::new(RwLock::new(None)),
sdk_client: Arc::new(OnceCell::new()),
http_client: Client::new(),
+ reaction_events: Arc::new(RwLock::new(HashMap::new())),
}
}
@@ -547,8 +554,18 @@ impl Channel for MatrixChannel {
anyhow::bail!("Matrix room '{}' is not in joined state", target_room_id);
}
- room.send(RoomMessageEventContent::text_markdown(&message.content))
- .await?;
+ let mut content = RoomMessageEventContent::text_markdown(&message.content);
+
+ if let Some(ref thread_ts) = message.thread_ts {
+ if let Ok(thread_root) = thread_ts.parse::() {
+ content.relates_to = Some(Relation::Thread(Thread::plain(
+ thread_root.clone(),
+ thread_root,
+ )));
+ }
+ }
+
+ room.send(content).await?;
Ok(())
}
@@ -634,6 +651,10 @@ impl Channel for MatrixChannel {
}
}
+ let thread_ts = match &event.content.relates_to {
+ Some(Relation::Thread(thread)) => Some(thread.event_id.to_string()),
+ _ => None,
+ };
let msg = ChannelMessage {
id: event_id,
sender: sender.clone(),
@@ -644,7 +665,7 @@ impl Channel for MatrixChannel {
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
- thread_ts: None,
+ thread_ts,
};
let _ = tx.send(msg).await;
@@ -684,6 +705,64 @@ impl Channel for MatrixChannel {
self.matrix_client().await.is_ok()
}
+
+ async fn add_reaction(
+ &self,
+ _channel_id: &str,
+ message_id: &str,
+ emoji: &str,
+ ) -> anyhow::Result<()> {
+ let client = self.matrix_client().await?;
+ let target_room_id = self.target_room_id().await?;
+ let target_room: OwnedRoomId = target_room_id.parse()?;
+
+ let room = client
+ .get_room(&target_room)
+ .ok_or_else(|| anyhow::anyhow!("Matrix room not found for reaction"))?;
+
+ let event_id: OwnedEventId = message_id
+ .parse()
+ .map_err(|_| anyhow::anyhow!("Invalid event ID for reaction: {}", message_id))?;
+
+ let reaction = ReactionEventContent::new(Annotation::new(event_id, emoji.to_string()));
+ let response = room.send(reaction).await?;
+
+ let key = format!("{}:{}", message_id, emoji);
+ self.reaction_events
+ .write()
+ .await
+ .insert(key, response.event_id.to_string());
+
+ Ok(())
+ }
+
+ async fn remove_reaction(
+ &self,
+ _channel_id: &str,
+ message_id: &str,
+ emoji: &str,
+ ) -> anyhow::Result<()> {
+ let key = format!("{}:{}", message_id, emoji);
+ let reaction_event_id = self.reaction_events.write().await.remove(&key);
+
+ if let Some(reaction_event_id) = reaction_event_id {
+ let client = self.matrix_client().await?;
+ let target_room_id = self.target_room_id().await?;
+ let target_room: OwnedRoomId = target_room_id.parse()?;
+
+ let room = client
+ .get_room(&target_room)
+ .ok_or_else(|| anyhow::anyhow!("Matrix room not found for reaction removal"))?;
+
+ let event_id: OwnedEventId = reaction_event_id.parse().map_err(|_| {
+ anyhow::anyhow!("Invalid reaction event ID: {}", reaction_event_id)
+ })?;
+
+ room.redact(&event_id, None, None).await?;
+ }
+
+ Ok(())
+ }
}
#[cfg(test)]
From bdc0f325bfdc1fab32067300a3ea8f6e1756f565 Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 19:10:08 -0400
Subject: [PATCH 06/15] feat(matrix): add pin and unpin message support (#3220)
Implement pin_message and unpin_message for the Matrix channel using
the m.room.pinned_events state event. Adds default no-op trait methods
to the Channel trait so other channel implementations are unaffected.
Co-authored-by: Claude Opus 4.6
---
src/channels/matrix.rs | 124 +++++++++++++++++++++++++++++++++++++++++
src/channels/traits.rs | 18 ++++++
2 files changed, 142 insertions(+)
diff --git a/src/channels/matrix.rs b/src/channels/matrix.rs
index 42c7e8335..645a47d0a 100644
--- a/src/channels/matrix.rs
+++ b/src/channels/matrix.rs
@@ -763,6 +763,130 @@ impl Channel for MatrixChannel {
Ok(())
}
+
+
+ async fn pin_message(
+ &self,
+ _channel_id: &str,
+ message_id: &str,
+ ) -> anyhow::Result<()> {
+ let room_id = self.target_room_id().await?;
+ let encoded_room = Self::encode_path_segment(&room_id);
+
+ let url = format!(
+ "{}/_matrix/client/v3/rooms/{}/state/m.room.pinned_events",
+ self.homeserver, encoded_room
+ );
+ let resp = self
+ .http_client
+ .get(&url)
+ .header("Authorization", self.auth_header_value())
+ .send()
+ .await?;
+
+ let mut pinned: Vec = if resp.status().is_success() {
+ let body: serde_json::Value = resp.json().await?;
+ body.get("pinned")
+ .and_then(|v| v.as_array())
+ .map(|arr| {
+ arr.iter()
+ .filter_map(|v| v.as_str().map(String::from))
+ .collect()
+ })
+ .unwrap_or_default()
+ } else {
+ Vec::new()
+ };
+
+ let msg_id = message_id.to_string();
+ if pinned.contains(&msg_id) {
+ return Ok(());
+ }
+ pinned.push(msg_id);
+
+ let put_url = format!(
+ "{}/_matrix/client/v3/rooms/{}/state/m.room.pinned_events",
+ self.homeserver, encoded_room
+ );
+ let body = serde_json::json!({ "pinned": pinned });
+ let resp = self
+ .http_client
+ .put(&put_url)
+ .header("Authorization", self.auth_header_value())
+ .json(&body)
+ .send()
+ .await?;
+
+ if !resp.status().is_success() {
+ let err = resp.text().await.unwrap_or_default();
+ anyhow::bail!("Matrix pin_message failed: {err}");
+ }
+
+ Ok(())
+ }
+
+ async fn unpin_message(
+ &self,
+ _channel_id: &str,
+ message_id: &str,
+ ) -> anyhow::Result<()> {
+ let room_id = self.target_room_id().await?;
+ let encoded_room = Self::encode_path_segment(&room_id);
+
+ let url = format!(
+ "{}/_matrix/client/v3/rooms/{}/state/m.room.pinned_events",
+ self.homeserver, encoded_room
+ );
+ let resp = self
+ .http_client
+ .get(&url)
+ .header("Authorization", self.auth_header_value())
+ .send()
+ .await?;
+
+ if !resp.status().is_success() {
+ return Ok(());
+ }
+
+ let body: serde_json::Value = resp.json().await?;
+ let mut pinned: Vec = body
+ .get("pinned")
+ .and_then(|v| v.as_array())
+ .map(|arr| {
+ arr.iter()
+ .filter_map(|v| v.as_str().map(String::from))
+ .collect()
+ })
+ .unwrap_or_default();
+
+ let msg_id = message_id.to_string();
+ let original_len = pinned.len();
+ pinned.retain(|id| id != &msg_id);
+
+ if pinned.len() == original_len {
+ return Ok(());
+ }
+
+ let put_url = format!(
+ "{}/_matrix/client/v3/rooms/{}/state/m.room.pinned_events",
+ self.homeserver, encoded_room
+ );
+ let body = serde_json::json!({ "pinned": pinned });
+ let resp = self
+ .http_client
+ .put(&put_url)
+ .header("Authorization", self.auth_header_value())
+ .json(&body)
+ .send()
+ .await?;
+
+ if !resp.status().is_success() {
+ let err = resp.text().await.unwrap_or_default();
+ anyhow::bail!("Matrix unpin_message failed: {err}");
+ }
+
+ Ok(())
+ }
}
#[cfg(test)]
diff --git a/src/channels/traits.rs b/src/channels/traits.rs
index 2fb90a654..3fc1570d6 100644
--- a/src/channels/traits.rs
+++ b/src/channels/traits.rs
@@ -142,6 +142,24 @@ pub trait Channel: Send + Sync {
) -> anyhow::Result<()> {
Ok(())
}
+
+ /// Pin a message in the channel.
+ async fn pin_message(
+ &self,
+ _channel_id: &str,
+ _message_id: &str,
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+
+ /// Unpin a previously pinned message.
+ async fn unpin_message(
+ &self,
+ _channel_id: &str,
+ _message_id: &str,
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
}
#[cfg(test)]
From 4da85cee6e3f28e7320f83ff43179a9a316261ee Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 19:11:53 -0400
Subject: [PATCH 07/15] chore(github): update review ownership routing (#3216)
- Add @theonlyhennygod as first-listed code owner on all CODEOWNERS paths
- Add SimianAstronaut7 as maintainer with PR approval authority in docs
- Normalize WORKFLOW_OWNER_LOGINS casing to canonical GitHub logins
---
.github/CODEOWNERS | 32 ++++++++++++-------------
.github/workflows/master-branch-flow.md | 4 ++--
docs/contributing/ci-map.md | 2 +-
docs/i18n/vi/ci-map.md | 2 +-
docs/vi/ci-map.md | 2 +-
5 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index e106cc052..2e3322d87 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,5 +1,5 @@
# Default owner for all files
-* @JordanTheJet @SimianAstronaut7
+* @theonlyhennygod @JordanTheJet @SimianAstronaut7
# Important functional modules
/src/agent/** @theonlyhennygod @JordanTheJet @SimianAstronaut7
@@ -13,20 +13,20 @@
/Cargo.lock @theonlyhennygod @JordanTheJet @SimianAstronaut7
# Security / tests / CI-CD ownership
-/src/security/** @JordanTheJet @SimianAstronaut7
-/tests/** @JordanTheJet @SimianAstronaut7
-/.github/** @JordanTheJet @SimianAstronaut7
-/.github/workflows/** @JordanTheJet @SimianAstronaut7
-/.github/codeql/** @JordanTheJet @SimianAstronaut7
-/.github/dependabot.yml @JordanTheJet @SimianAstronaut7
-/SECURITY.md @JordanTheJet @SimianAstronaut7
-/docs/actions-source-policy.md @JordanTheJet @SimianAstronaut7
-/docs/ci-map.md @JordanTheJet @SimianAstronaut7
+/src/security/** @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/tests/** @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/.github/** @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/.github/workflows/** @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/.github/codeql/** @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/.github/dependabot.yml @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/SECURITY.md @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/docs/actions-source-policy.md @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/docs/ci-map.md @theonlyhennygod @JordanTheJet @SimianAstronaut7
# Docs & governance
-/docs/** @JordanTheJet @SimianAstronaut7
-/AGENTS.md @JordanTheJet @SimianAstronaut7
-/CLAUDE.md @JordanTheJet @SimianAstronaut7
-/CONTRIBUTING.md @JordanTheJet @SimianAstronaut7
-/docs/pr-workflow.md @JordanTheJet @SimianAstronaut7
-/docs/reviewer-playbook.md @JordanTheJet @SimianAstronaut7
+/docs/** @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/AGENTS.md @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/CLAUDE.md @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/CONTRIBUTING.md @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/docs/pr-workflow.md @theonlyhennygod @JordanTheJet @SimianAstronaut7
+/docs/reviewer-playbook.md @theonlyhennygod @JordanTheJet @SimianAstronaut7
diff --git a/.github/workflows/master-branch-flow.md b/.github/workflows/master-branch-flow.md
index 8248e979d..518996540 100644
--- a/.github/workflows/master-branch-flow.md
+++ b/.github/workflows/master-branch-flow.md
@@ -12,7 +12,7 @@ Use this with:
ZeroClaw uses a single default branch: `master`. All contributor PRs target `master` directly. There is no `dev` or promotion branch.
-Current maintainers with PR approval authority: `theonlyhennygod` and `jordanthejet`.
+Current maintainers with PR approval authority: `theonlyhennygod`, `JordanTheJet`, and `SimianAstronaut7`.
## Active Workflows
@@ -43,7 +43,7 @@ Current maintainers with PR approval authority: `theonlyhennygod` and `jordanthe
- `security` job: runs `cargo audit` and `cargo deny check licenses sources`.
- Concurrency group cancels in-progress runs for the same PR on new pushes.
3. All jobs must pass before merge.
-4. Maintainer (`theonlyhennygod` or `jordanthejet`) merges PR once checks and review policy are satisfied.
+4. Maintainer (`theonlyhennygod`, `JordanTheJet`, or `SimianAstronaut7`) merges PR once checks and review policy are satisfied.
5. Merge emits a `push` event on `master` (see section 2).
### 2) Push to `master` (including after merge)
diff --git a/docs/contributing/ci-map.md b/docs/contributing/ci-map.md
index 6ee3f2172..e91f15ab0 100644
--- a/docs/contributing/ci-map.md
+++ b/docs/contributing/ci-map.md
@@ -13,7 +13,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
- `.github/workflows/ci-run.yml` (`CI`)
- Purpose: Rust validation (`cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D clippy::correctness`, strict delta lint gate on changed Rust lines, `test`, release build smoke) + docs quality checks when docs change (`markdownlint` blocks only issues on changed lines; link check scans only links added on changed lines)
- Additional behavior: for Rust-impacting PRs and pushes, `CI Required Gate` requires `lint` + `test` + `build` (no PR build-only bypass)
- - Additional behavior: PRs that change `.github/workflows/**` require at least one approving review from a login in `WORKFLOW_OWNER_LOGINS` (repository variable fallback: `theonlyhennygod,jordanthejet`)
+ - Additional behavior: PRs that change `.github/workflows/**` require at least one approving review from a login in `WORKFLOW_OWNER_LOGINS` (repository variable fallback: `theonlyhennygod,JordanTheJet,SimianAstronaut7`)
- Additional behavior: lint gates run before `test`/`build`; when lint/docs gates fail on PRs, CI posts an actionable feedback comment with failing gate names and local fix commands
- Merge gate: `CI Required Gate`
- `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`)
diff --git a/docs/i18n/vi/ci-map.md b/docs/i18n/vi/ci-map.md
index 25da4690e..7a9a86715 100644
--- a/docs/i18n/vi/ci-map.md
+++ b/docs/i18n/vi/ci-map.md
@@ -13,7 +13,7 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
- `.github/workflows/ci-run.yml` (`CI`)
- Mục đích: Rust validation (`cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D clippy::correctness`, strict delta lint gate trên các dòng Rust thay đổi, `test`, kiểm tra smoke release build) + kiểm tra chất lượng tài liệu khi tài liệu thay đổi (`markdownlint` chỉ chặn các vấn đề trên dòng thay đổi; link check chỉ quét các link mới được thêm trên dòng thay đổi)
- Hành vi bổ sung: đối với PR và push ảnh hưởng Rust, `CI Required Gate` yêu cầu `lint` + `test` + `build` (không có shortcut chỉ build trên PR)
- - Hành vi bổ sung: các PR thay đổi `.github/workflows/**` yêu cầu ít nhất một review phê duyệt từ login trong `WORKFLOW_OWNER_LOGINS` (fallback biến repository: `theonlyhennygod,jordanthejet`)
+ - Hành vi bổ sung: các PR thay đổi `.github/workflows/**` yêu cầu ít nhất một review phê duyệt từ login trong `WORKFLOW_OWNER_LOGINS` (fallback biến repository: `theonlyhennygod,JordanTheJet,SimianAstronaut7`)
- Hành vi bổ sung: lint gate chạy trước `test`/`build`; khi lint/docs gate thất bại trên PR, CI đăng comment phản hồi hành động được với tên gate thất bại và các lệnh sửa cục bộ
- Merge gate: `CI Required Gate`
- `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`)
diff --git a/docs/vi/ci-map.md b/docs/vi/ci-map.md
index 834a2c15e..5b9f01a0e 100644
--- a/docs/vi/ci-map.md
+++ b/docs/vi/ci-map.md
@@ -13,7 +13,7 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
- `.github/workflows/ci-run.yml` (`CI`)
- Mục đích: Rust validation (`cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D clippy::correctness`, strict delta lint gate trên các dòng Rust thay đổi, `test`, kiểm tra smoke release build) + kiểm tra chất lượng tài liệu khi tài liệu thay đổi (`markdownlint` chỉ chặn các vấn đề trên dòng thay đổi; link check chỉ quét các link mới được thêm trên dòng thay đổi)
- Hành vi bổ sung: đối với PR và push ảnh hưởng Rust, `CI Required Gate` yêu cầu `lint` + `test` + `build` (không có shortcut chỉ build trên PR)
- - Hành vi bổ sung: các PR thay đổi `.github/workflows/**` yêu cầu ít nhất một review phê duyệt từ login trong `WORKFLOW_OWNER_LOGINS` (fallback biến repository: `theonlyhennygod,jordanthejet`)
+ - Hành vi bổ sung: các PR thay đổi `.github/workflows/**` yêu cầu ít nhất một review phê duyệt từ login trong `WORKFLOW_OWNER_LOGINS` (fallback biến repository: `theonlyhennygod,JordanTheJet,SimianAstronaut7`)
- Hành vi bổ sung: lint gate chạy trước `test`/`build`; khi lint/docs gate thất bại trên PR, CI đăng comment phản hồi hành động được với tên gate thất bại và các lệnh sửa cục bộ
- Merge gate: `CI Required Gate`
- `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`)
From 01bcba83ad626f3661caf879eb5f9b5c1a50de62 Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 19:12:44 -0400
Subject: [PATCH 08/15] feat(matrix): add file upload handling and voice
message support (#3222)
---
src/channels/matrix.rs | 180 ++++++++++++++++++++++++++++++++++++++++-
src/channels/mod.rs | 7 ++
2 files changed, 183 insertions(+), 4 deletions(-)
diff --git a/src/channels/matrix.rs b/src/channels/matrix.rs
index 645a47d0a..f01e0bde2 100644
--- a/src/channels/matrix.rs
+++ b/src/channels/matrix.rs
@@ -1,12 +1,13 @@
use crate::channels::traits::{Channel, ChannelMessage, SendMessage};
use async_trait::async_trait;
use std::collections::HashMap;
+use std::sync::atomic::{AtomicBool, Ordering};
use matrix_sdk::{
authentication::matrix::MatrixSession,
config::SyncSettings,
ruma::{
events::room::message::{
- MessageType, OriginalSyncRoomMessageEvent, Relation, RoomMessageEventContent,
+ MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
},
events::reaction::ReactionEventContent,
events::relation::{Annotation, InReplyTo, Thread},
@@ -37,6 +38,7 @@ pub struct MatrixChannel {
sdk_client: Arc>,
http_client: Client,
reaction_events: Arc>>,
+ voice_mode: Arc,
}
impl std::fmt::Debug for MatrixChannel {
@@ -170,6 +172,7 @@ impl MatrixChannel {
sdk_client: Arc::new(OnceCell::new()),
http_client: Client::new(),
reaction_events: Arc::new(RwLock::new(HashMap::new())),
+ voice_mode: Arc::new(AtomicBool::new(false)),
}
}
@@ -567,6 +570,70 @@ impl Channel for MatrixChannel {
room.send(content).await?;
+ // Voice reply: generate TTS audio and send as m.audio when voice_mode is active
+ if self.voice_mode.load(Ordering::Relaxed) {
+ self.voice_mode.store(false, Ordering::Relaxed);
+ tracing::info!("Voice mode active, generating TTS reply");
+ let voice_work = std::path::PathBuf::from("/tmp/zeroclaw-voice");
+ let _ = tokio::fs::create_dir_all(&voice_work).await;
+ let mp3_path = voice_work.join("reply.mp3");
+
+ let tts_text = message.content
+ .replace("**", "").replace("*", "")
+ .replace("`", "").replace("# ", "");
+
+ let tts_ok = tokio::process::Command::new("edge-tts")
+ .arg("--text").arg(&tts_text)
+ .arg("--write-media").arg(&mp3_path)
+ .output().await
+ .map(|o| o.status.success())
+ .unwrap_or(false);
+
+ if tts_ok && mp3_path.exists() {
+ if let Ok(audio_data) = tokio::fs::read(&mp3_path).await {
+ let upload_url = format!(
+ "{}/_matrix/media/v3/upload?filename=voice-reply.mp3",
+ self.homeserver
+ );
+ if let Ok(resp) = self.http_client
+ .post(&upload_url)
+ .header("Authorization", self.auth_header_value())
+ .header("Content-Type", "audio/mpeg")
+ .body(audio_data)
+ .send().await
+ {
+ if resp.status().is_success() {
+ if let Ok(body) = resp.json::().await {
+ if let Some(content_uri) = body["content_uri"].as_str() {
+ let encoded_room = Self::encode_path_segment(&target_room_id);
+ let txn_id = format!("voice_{}",
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap_or_default().as_millis()
+ );
+ let audio_msg = serde_json::json!({
+ "msgtype": "m.audio",
+ "body": "Voice reply",
+ "url": content_uri,
+ "info": { "mimetype": "audio/mpeg" }
+ });
+ let send_url = format!(
+ "{}/_matrix/client/v3/rooms/{}/send/m.room.message/{}",
+ self.homeserver, encoded_room, txn_id
+ );
+ let _ = self.http_client
+ .put(&send_url)
+ .header("Authorization", self.auth_header_value())
+ .json(&audio_msg)
+ .send().await;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
Ok(())
}
@@ -610,6 +677,9 @@ impl Channel for MatrixChannel {
let my_user_id_for_handler = my_user_id.clone();
let allowed_users_for_handler = self.allowed_users.clone();
let dedupe_for_handler = Arc::clone(&recent_event_cache);
+ let homeserver_for_handler = self.homeserver.clone();
+ let access_token_for_handler = self.access_token.clone();
+ let voice_mode_for_handler = Arc::clone(&self.voice_mode);
client.add_event_handler(move |event: OriginalSyncRoomMessageEvent, room: Room| {
let tx = tx_handler.clone();
@@ -617,6 +687,9 @@ impl Channel for MatrixChannel {
let my_user_id = my_user_id_for_handler.clone();
let allowed_users = allowed_users_for_handler.clone();
let dedupe = Arc::clone(&dedupe_for_handler);
+ let homeserver = homeserver_for_handler.clone();
+ let access_token = access_token_for_handler.clone();
+ let voice_mode = Arc::clone(&voice_mode_for_handler);
async move {
if room.room_id().as_str() != target_room.as_str() {
@@ -632,12 +705,111 @@ impl Channel for MatrixChannel {
return;
}
- let body = match &event.content.msgtype {
- MessageType::Text(content) => content.body.clone(),
- MessageType::Notice(content) => content.body.clone(),
+ // Helper: extract mxc:// download URL and filename for media types
+ let media_info = |source: &MediaSource, name: &str| -> Option<(String, String)> {
+ match source {
+ MediaSource::Plain(mxc) => {
+ let rest = mxc.as_str().strip_prefix("mxc://")?;
+ let url = format!(
+ "{}/_matrix/client/v1/media/download/{}",
+ homeserver, rest
+ );
+ Some((url, name.to_string()))
+ }
+ _ => None,
+ }
+ };
+
+ let (body, media_download) = match &event.content.msgtype {
+ MessageType::Text(content) => (content.body.clone(), None),
+ MessageType::Notice(content) => (content.body.clone(), None),
+ MessageType::Image(content) => {
+ let dl = media_info(&content.source, &content.body);
+ (format!("[image: {}]", content.body), dl)
+ }
+ MessageType::File(content) => {
+ let dl = media_info(&content.source, &content.body);
+ (format!("[file: {}]", content.body), dl)
+ }
+ MessageType::Audio(content) => {
+ let dl = media_info(&content.source, &content.body);
+ (format!("[audio: {}]", content.body), dl)
+ }
+ MessageType::Video(content) => {
+ let dl = media_info(&content.source, &content.body);
+ (format!("[video: {}]", content.body), dl)
+ }
_ => return,
};
+ // Download media to workspace if present
+ let body = if let Some((url, filename)) = media_download {
+ let workspace = std::path::PathBuf::from(
+ std::env::var("ZEROCLAW_WORKSPACE").unwrap_or_else(|_| "/tmp/zeroclaw-uploads".to_string())
+ );
+ let _ = tokio::fs::create_dir_all(&workspace).await;
+ let dest = workspace.join(&filename);
+ let client = reqwest::Client::new();
+ match client.get(&url)
+ .header("Authorization", format!("Bearer {}", access_token))
+ .send()
+ .await
+ {
+ Ok(resp) if resp.status().is_success() => {
+ match resp.bytes().await {
+ Ok(bytes) => {
+ match tokio::fs::write(&dest, &bytes).await {
+ Ok(_) => format!("{} — saved to {}", body, dest.display()),
+ Err(_) => format!("{} — failed to write to disk", body),
+ }
+ }
+ Err(_) => format!("{} — download failed", body),
+ }
+ }
+ _ => format!("{} — download failed (auth error?)", body),
+ }
+ } else {
+ body
+ };
+
+ // Voice transcription: if this was an audio message, transcribe it
+ let body = if body.starts_with("[audio:") {
+ if let Some(path_start) = body.find("saved to ") {
+ let audio_path = body[path_start + 9..].to_string();
+ let wav_path = format!("{}.16k.wav", audio_path);
+ let convert_ok = tokio::process::Command::new("ffmpeg")
+ .args(["-y", "-i", &audio_path, "-ar", "16000", "-ac", "1", "-f", "wav", &wav_path])
+ .stderr(std::process::Stdio::null())
+ .output()
+ .await
+ .map(|o| o.status.success())
+ .unwrap_or(false);
+ if convert_ok {
+ let transcription = tokio::process::Command::new("whisper-cpp")
+ .args(["-m", "/tmp/ggml-base.en.bin",
+ "-f", &wav_path, "--no-timestamps", "-nt"])
+ .output()
+ .await
+ .ok()
+ .filter(|o| o.status.success())
+ .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
+ .filter(|s| !s.is_empty());
+ if let Some(text) = transcription {
+ voice_mode.store(true, Ordering::Relaxed);
+ format!("[Voice message]: {}", text)
+ } else {
+ body
+ }
+ } else {
+ body
+ }
+ } else {
+ body
+ }
+ } else {
+ body
+ };
+
if !MatrixChannel::has_non_empty_body(&body) {
return;
}
diff --git a/src/channels/mod.rs b/src/channels/mod.rs
index 2d999d698..e4eb2e10e 100644
--- a/src/channels/mod.rs
+++ b/src/channels/mod.rs
@@ -457,6 +457,13 @@ fn strip_tool_call_tags(message: &str) -> String {
fn channel_delivery_instructions(channel_name: &str) -> Option<&'static str> {
match channel_name {
+ "matrix" => Some(
+ "When responding on Matrix:\n\
+ - Use Markdown formatting (bold, italic, code blocks)\n\
+ - Be concise and direct\n\
+ - When you receive a [Voice message], the user spoke to you. Respond naturally as in conversation.\n\
+ - Your text reply will automatically be converted to audio and sent back as a voice message.\n",
+ ),
"telegram" => Some(
"When responding on Telegram:\n\
- Include media markers for files or URLs that should be sent as attachments\n\
From f0f0f80895992d55a4be09662084d8ac3a5c9166 Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 19:13:18 -0400
Subject: [PATCH 09/15] feat(matrix): add multi-room support (#3224)
Enable a single Matrix bot instance to respond in multiple rooms:
- Disable the room_id filter so messages from all joined rooms are processed
- Embed room_id in reply_target as "user||room_id" for routing replies
- Include room_id in channel field for per-room conversation isolation
- Extract room_id from recipient in send() for correct message routing
The configured room_id still serves as a fallback for direct sends
without a "||" separator in the recipient.
Co-authored-by: Claude Opus 4.6
---
src/channels/matrix.rs | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/channels/matrix.rs b/src/channels/matrix.rs
index f01e0bde2..525427e1f 100644
--- a/src/channels/matrix.rs
+++ b/src/channels/matrix.rs
@@ -540,7 +540,11 @@ impl Channel for MatrixChannel {
async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
let client = self.matrix_client().await?;
- let target_room_id = self.target_room_id().await?;
+ let target_room_id = if message.recipient.contains("||") {
+ message.recipient.splitn(2, "||").nth(1).unwrap().to_string()
+ } else {
+ self.target_room_id().await?
+ };
let target_room: OwnedRoomId = target_room_id.parse()?;
let mut room = client.get_room(&target_room);
@@ -692,7 +696,7 @@ impl Channel for MatrixChannel {
let voice_mode = Arc::clone(&voice_mode_for_handler);
async move {
- if room.room_id().as_str() != target_room.as_str() {
+ if false /* multi-room: room_id filter disabled */ {
return;
}
@@ -830,9 +834,9 @@ impl Channel for MatrixChannel {
let msg = ChannelMessage {
id: event_id,
sender: sender.clone(),
- reply_target: sender,
+ reply_target: format!("{}||{}", sender, room.room_id()),
content: body,
- channel: "matrix".to_string(),
+ channel: format!("matrix:{}", room.room_id()),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
From 86ca34ac1f2e98b8923384b419bc55022d49f90e Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 19:26:45 -0400
Subject: [PATCH 10/15] fix(tests): update assertions for live tool call
notifications (#3232)
The live tool call notifications feature sends extra messages to the
channel when tools are invoked. Update 5 tests that assumed exactly
1 sent message to instead check the last message in the list.
Co-authored-by: Claude Opus 4.6
---
src/channels/mod.rs | 43 ++++++++++++++++++++++++-------------------
1 file changed, 24 insertions(+), 19 deletions(-)
diff --git a/src/channels/mod.rs b/src/channels/mod.rs
index e4eb2e10e..6903276c5 100644
--- a/src/channels/mod.rs
+++ b/src/channels/mod.rs
@@ -4310,11 +4310,12 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert_eq!(sent_messages.len(), 1);
- assert!(sent_messages[0].starts_with("chat-42:"));
- assert!(sent_messages[0].contains("BTC is currently around"));
- assert!(!sent_messages[0].contains("\"tool_calls\""));
- assert!(!sent_messages[0].contains("mock_price"));
+ assert!(sent_messages.len() >= 1);
+ let reply = sent_messages.last().unwrap();
+ assert!(reply.starts_with("chat-42:"));
+ assert!(reply.contains("BTC is currently around"));
+ assert!(!reply.contains("\"tool_calls\""));
+ assert!(!reply.contains("mock_price"));
}
#[tokio::test]
@@ -4370,8 +4371,9 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert_eq!(sent_messages.len(), 1);
- assert!(sent_messages[0].contains("BTC is currently around"));
+ assert!(sent_messages.len() >= 1);
+ let reply = sent_messages.last().unwrap();
+ assert!(reply.contains("BTC is currently around"));
let histories = runtime_ctx
.conversation_histories
@@ -4504,11 +4506,12 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert_eq!(sent_messages.len(), 1);
- assert!(sent_messages[0].starts_with("chat-84:"));
- assert!(sent_messages[0].contains("alias-tag flow resolved"));
- assert!(!sent_messages[0].contains(""));
- assert!(!sent_messages[0].contains("mock_price"));
+ assert!(sent_messages.len() >= 1);
+ let reply = sent_messages.last().unwrap();
+ assert!(reply.starts_with("chat-84:"));
+ assert!(reply.contains("alias-tag flow resolved"));
+ assert!(!reply.contains(""));
+ assert!(!reply.contains("mock_price"));
}
#[tokio::test]
@@ -4894,10 +4897,11 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert_eq!(sent_messages.len(), 1);
- assert!(sent_messages[0].starts_with("chat-iter-success:"));
- assert!(sent_messages[0].contains("Completed after 11 tool iterations."));
- assert!(!sent_messages[0].contains("⚠️ Error:"));
+ assert!(sent_messages.len() >= 1);
+ let reply = sent_messages.last().unwrap();
+ assert!(reply.starts_with("chat-iter-success:"));
+ assert!(reply.contains("Completed after 11 tool iterations."));
+ assert!(!reply.contains("⚠️ Error:"));
}
#[tokio::test]
@@ -4955,9 +4959,10 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert_eq!(sent_messages.len(), 1);
- assert!(sent_messages[0].starts_with("chat-iter-fail:"));
- assert!(sent_messages[0].contains("⚠️ Error: Agent exceeded maximum tool iterations (3)"));
+ assert!(sent_messages.len() >= 1);
+ let reply = sent_messages.last().unwrap();
+ assert!(reply.starts_with("chat-iter-fail:"));
+ assert!(reply.contains("⚠️ Error: Agent exceeded maximum tool iterations (3)"));
}
struct NoopMemory;
From 74fe29d7727d78372494e5ef7a77a48ae01acc56 Mon Sep 17 00:00:00 2001
From: Alan P John
Date: Thu, 12 Mar 2026 05:05:43 +0530
Subject: [PATCH 11/15] feat: Add Opencode-go provider (#3113)
Adds opencode-go as a first-class provider with dedicated API endpoint,
env var, onboarding wizard wiring, and test coverage.
CI failures are pre-existing on master (Rust 1.94 formatting/lint changes per #3207).
---
.env.example | 1 +
docs/i18n/vi/providers-reference.md | 3 ++-
docs/reference/api/providers-reference.md | 1 +
docs/vi/providers-reference.md | 3 ++-
src/integrations/registry.rs | 12 ++++++++++
src/onboard/wizard.rs | 7 +++++-
src/providers/compatible.rs | 12 +++++++++-
src/providers/mod.rs | 27 +++++++++++++++++++++++
tests/component/provider_resolution.rs | 5 +++++
9 files changed, 67 insertions(+), 4 deletions(-)
diff --git a/.env.example b/.env.example
index a6ba55e28..e8a3a3629 100644
--- a/.env.example
+++ b/.env.example
@@ -59,6 +59,7 @@ PROVIDER=openrouter
# ZAI_API_KEY=...
# SYNTHETIC_API_KEY=...
# OPENCODE_API_KEY=...
+# OPENCODE_GO_API_KEY=...
# VERCEL_API_KEY=...
# CLOUDFLARE_API_KEY=...
diff --git a/docs/i18n/vi/providers-reference.md b/docs/i18n/vi/providers-reference.md
index 00ac11584..313f3b0de 100644
--- a/docs/i18n/vi/providers-reference.md
+++ b/docs/i18n/vi/providers-reference.md
@@ -2,7 +2,7 @@
Tài liệu này liệt kê các provider ID, alias và biến môi trường chứa thông tin xác thực.
-Cập nhật lần cuối: **2026-02-19**.
+Cập nhật lần cuối: **2026-03-10**.
## Cách liệt kê các Provider
@@ -36,6 +36,7 @@ Với chuỗi provider dự phòng (`reliability.fallback_providers`), mỗi pro
| `kimi-code` | `kimi_coding`, `kimi_for_coding` | Không | `KIMI_CODE_API_KEY`, `MOONSHOT_API_KEY` |
| `synthetic` | — | Không | `SYNTHETIC_API_KEY` |
| `opencode` | `opencode-zen` | Không | `OPENCODE_API_KEY` |
+| `opencode-go` | — | Không | `OPENCODE_GO_API_KEY` |
| `zai` | `z.ai` | Không | `ZAI_API_KEY` |
| `glm` | `zhipu` | Không | `GLM_API_KEY` |
| `minimax` | `minimax-intl`, `minimax-io`, `minimax-global`, `minimax-cn`, `minimaxi`, `minimax-oauth`, `minimax-oauth-cn`, `minimax-portal`, `minimax-portal-cn` | Không | `MINIMAX_OAUTH_TOKEN`, `MINIMAX_API_KEY` |
diff --git a/docs/reference/api/providers-reference.md b/docs/reference/api/providers-reference.md
index c94aa8cb6..3748e2e30 100644
--- a/docs/reference/api/providers-reference.md
+++ b/docs/reference/api/providers-reference.md
@@ -38,6 +38,7 @@ credential is not reused for fallback providers.
| `kimi-code` | `kimi_coding`, `kimi_for_coding` | No | `KIMI_CODE_API_KEY`, `MOONSHOT_API_KEY` |
| `synthetic` | — | No | `SYNTHETIC_API_KEY` |
| `opencode` | `opencode-zen` | No | `OPENCODE_API_KEY` |
+| `opencode-go` | — | No | `OPENCODE_GO_API_KEY` |
| `zai` | `z.ai` | No | `ZAI_API_KEY` |
| `glm` | `zhipu` | No | `GLM_API_KEY` |
| `minimax` | `minimax-intl`, `minimax-io`, `minimax-global`, `minimax-cn`, `minimaxi`, `minimax-oauth`, `minimax-oauth-cn`, `minimax-portal`, `minimax-portal-cn` | No | `MINIMAX_OAUTH_TOKEN`, `MINIMAX_API_KEY` |
diff --git a/docs/vi/providers-reference.md b/docs/vi/providers-reference.md
index 00ac11584..313f3b0de 100644
--- a/docs/vi/providers-reference.md
+++ b/docs/vi/providers-reference.md
@@ -2,7 +2,7 @@
Tài liệu này liệt kê các provider ID, alias và biến môi trường chứa thông tin xác thực.
-Cập nhật lần cuối: **2026-02-19**.
+Cập nhật lần cuối: **2026-03-10**.
## Cách liệt kê các Provider
@@ -36,6 +36,7 @@ Với chuỗi provider dự phòng (`reliability.fallback_providers`), mỗi pro
| `kimi-code` | `kimi_coding`, `kimi_for_coding` | Không | `KIMI_CODE_API_KEY`, `MOONSHOT_API_KEY` |
| `synthetic` | — | Không | `SYNTHETIC_API_KEY` |
| `opencode` | `opencode-zen` | Không | `OPENCODE_API_KEY` |
+| `opencode-go` | — | Không | `OPENCODE_GO_API_KEY` |
| `zai` | `z.ai` | Không | `ZAI_API_KEY` |
| `glm` | `zhipu` | Không | `GLM_API_KEY` |
| `minimax` | `minimax-intl`, `minimax-io`, `minimax-global`, `minimax-cn`, `minimaxi`, `minimax-oauth`, `minimax-oauth-cn`, `minimax-portal`, `minimax-portal-cn` | Không | `MINIMAX_OAUTH_TOKEN`, `MINIMAX_API_KEY` |
diff --git a/src/integrations/registry.rs b/src/integrations/registry.rs
index faf3ad28a..7a9d1fa17 100644
--- a/src/integrations/registry.rs
+++ b/src/integrations/registry.rs
@@ -364,6 +364,18 @@ pub fn all_integrations() -> Vec {
}
},
},
+ IntegrationEntry {
+ name: "OpenCode Go",
+ description: "Subsidized Code-focused AI models",
+ category: IntegrationCategory::AiModel,
+ status_fn: |c| {
+ if c.default_provider.as_deref() == Some("opencode-go") {
+ IntegrationStatus::Active
+ } else {
+ IntegrationStatus::Available
+ }
+ },
+ },
IntegrationEntry {
name: "Z.AI",
description: "Z.AI inference",
diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs
index d294485fe..908311d9a 100644
--- a/src/onboard/wizard.rs
+++ b/src/onboard/wizard.rs
@@ -711,7 +711,7 @@ fn default_model_for_provider(provider: &str) -> String {
"qwen-code" => "qwen3-coder-plus".into(),
"ollama" => "llama3.2".into(),
"llamacpp" => "ggml-org/gpt-oss-20b-GGUF".into(),
- "sglang" | "vllm" | "osaurus" => "default".into(),
+ "sglang" | "vllm" | "osaurus" | "opencode-go" => "default".into(),
"gemini" => "gemini-2.5-pro".into(),
"kimi-code" => "kimi-for-coding".into(),
"bedrock" => "anthropic.claude-sonnet-4-5-20250929-v1:0".into(),
@@ -1163,6 +1163,7 @@ fn supports_live_model_fetch(provider_name: &str) -> bool {
| "zai"
| "qwen"
| "nvidia"
+ | "opencode-go"
)
}
@@ -1194,6 +1195,7 @@ fn models_endpoint_for_provider(provider_name: &str) -> Option<&'static str> {
"sglang" => Some("http://localhost:30000/v1/models"),
"vllm" => Some("http://localhost:8000/v1/models"),
"osaurus" => Some("http://localhost:1337/v1/models"),
+ "opencode-go" => Some("https://opencode.ai/zen/go/v1/models"),
_ => None,
},
}
@@ -2195,6 +2197,7 @@ async fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String,
("zai-cn", "Z.AI — China coding endpoint (open.bigmodel.cn)"),
("synthetic", "Synthetic — Synthetic AI models"),
("opencode", "OpenCode Zen — code-focused AI"),
+ ("opencode-go", "OpenCode Go — Subsidized code-focused AI"),
("cohere", "Cohere — Command R+ & embeddings"),
],
4 => local_provider_choices(),
@@ -2879,6 +2882,7 @@ fn provider_env_var(name: &str) -> &'static str {
"zai" => "ZAI_API_KEY",
"synthetic" => "SYNTHETIC_API_KEY",
"opencode" | "opencode-zen" => "OPENCODE_API_KEY",
+ "opencode-go" => "OPENCODE_GO_API_KEY",
"vercel" | "vercel-ai" => "VERCEL_API_KEY",
"cloudflare" | "cloudflare-ai" => "CLOUDFLARE_API_KEY",
"bedrock" | "aws-bedrock" => "AWS_ACCESS_KEY_ID",
@@ -7051,6 +7055,7 @@ mod tests {
assert_eq!(provider_env_var("nvidia-nim"), "NVIDIA_API_KEY"); // alias
assert_eq!(provider_env_var("build.nvidia.com"), "NVIDIA_API_KEY"); // alias
assert_eq!(provider_env_var("astrai"), "ASTRAI_API_KEY");
+ assert_eq!(provider_env_var("opencode-go"), "OPENCODE_GO_API_KEY");
}
#[test]
diff --git a/src/providers/compatible.rs b/src/providers/compatible.rs
index 056f7cfce..b3ec9c962 100644
--- a/src/providers/compatible.rs
+++ b/src/providers/compatible.rs
@@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize};
/// A provider that speaks the OpenAI-compatible chat completions API.
/// Used by: Venice, Vercel AI Gateway, Cloudflare AI Gateway, Moonshot,
-/// Synthetic, `OpenCode` Zen, `Z.AI`, `GLM`, `MiniMax`, Bedrock, Qianfan, Groq, Mistral, `xAI`, etc.
+/// Synthetic, `OpenCode` Zen, `OpenCode` Go, `Z.AI`, `GLM`, `MiniMax`, Bedrock, Qianfan, Groq, Mistral, `xAI`, etc.
#[allow(clippy::struct_excessive_bools)]
pub struct OpenAiCompatibleProvider {
pub(crate) name: String,
@@ -2164,6 +2164,16 @@ mod tests {
);
}
+ #[test]
+ fn chat_completions_url_opencode_go() {
+ // OpenCode Go uses /zen/go/v1 base path
+ let p = make_provider("opencode-go", "https://opencode.ai/zen/go/v1", None);
+ assert_eq!(
+ p.chat_completions_url(),
+ "https://opencode.ai/zen/go/v1/chat/completions"
+ );
+ }
+
#[test]
fn parse_native_response_preserves_tool_call_id() {
let message = ResponseMessage {
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index ec3b928e6..89eca28a2 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -839,6 +839,7 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) ->
"nvidia" | "nvidia-nim" | "build.nvidia.com" => vec!["NVIDIA_API_KEY"],
"synthetic" => vec!["SYNTHETIC_API_KEY"],
"opencode" | "opencode-zen" => vec!["OPENCODE_API_KEY"],
+ "opencode-go" => vec!["OPENCODE_GO_API_KEY"],
"vercel" | "vercel-ai" => vec!["VERCEL_API_KEY"],
"cloudflare" | "cloudflare-ai" => vec!["CLOUDFLARE_API_KEY"],
"ovhcloud" | "ovh" => vec!["OVH_AI_ENDPOINTS_ACCESS_TOKEN"],
@@ -1099,6 +1100,9 @@ fn create_provider_with_url_and_options(
"opencode" | "opencode-zen" => Ok(Box::new(OpenAiCompatibleProvider::new(
"OpenCode Zen", "https://opencode.ai/zen/v1", key, AuthStyle::Bearer,
))),
+ "opencode-go" => Ok(Box::new(OpenAiCompatibleProvider::new(
+ "OpenCode Go", "https://opencode.ai/zen/go/v1", key, AuthStyle::Bearer,
+ ))),
name if zai_base_url(name).is_some() => Ok(Box::new(OpenAiCompatibleProvider::new(
"Z.AI",
zai_base_url(name).expect("checked in guard"),
@@ -1608,6 +1612,12 @@ pub fn list_providers() -> Vec {
aliases: &["opencode-zen"],
local: false,
},
+ ProviderInfo {
+ name: "opencode-go",
+ display_name: "OpenCode Go",
+ aliases: &[],
+ local: false,
+ },
ProviderInfo {
name: "zai",
display_name: "Z.AI",
@@ -2141,6 +2151,22 @@ mod tests {
assert!(create_provider("opencode-zen", Some("key")).is_ok());
}
+ #[test]
+ fn factory_opencode_go() {
+ assert!(create_provider("opencode-go", Some("key")).is_ok());
+ }
+
+ #[test]
+ fn resolve_provider_credential_opencode_go_env() {
+ let _env_lock = env_lock();
+ let _provider_guard = EnvGuard::set("OPENCODE_GO_API_KEY", Some("go-test-key"));
+ let _generic_guard = EnvGuard::set("API_KEY", None);
+ let _zeroclaw_guard = EnvGuard::set("ZEROCLAW_API_KEY", None);
+
+ let resolved = resolve_provider_credential("opencode-go", None);
+ assert_eq!(resolved.as_deref(), Some("go-test-key"));
+ }
+
#[test]
fn factory_zai() {
assert!(create_provider("zai", Some("key")).is_ok());
@@ -2663,6 +2689,7 @@ mod tests {
"kimi-code",
"synthetic",
"opencode",
+ "opencode-go",
"zai",
"zai-cn",
"glm",
diff --git a/tests/component/provider_resolution.rs b/tests/component/provider_resolution.rs
index d057e6718..0b14fec9a 100644
--- a/tests/component/provider_resolution.rs
+++ b/tests/component/provider_resolution.rs
@@ -304,6 +304,11 @@ fn factory_resolves_opencode_provider() {
assert_provider_ok("opencode", Some("test-key"), None);
}
+#[test]
+fn factory_resolves_opencode_go_provider() {
+ assert_provider_ok("opencode-go", Some("test-key"), None);
+}
+
#[test]
fn factory_resolves_astrai_provider() {
assert_provider_ok("astrai", Some("test-key"), None);
From d47f6703d8148b497b70abf5ad4f677c7846723e Mon Sep 17 00:00:00 2001
From: Thomas Tuffin <71447672+ttuffin@users.noreply.github.com>
Date: Thu, 12 Mar 2026 00:38:36 +0100
Subject: [PATCH 12/15] fix: revert Rust bump to 1.93 in Dockerfile (#3208)
Reverts Dockerfile from rust:1.94-slim to rust:1.93-slim.
Rust 1.94 triggers a recursion limit overflow in matrix-sdk 0.16.0
(rust-lang/rust#152942, matrix-org/matrix-rust-sdk#6254). No upstream
fix available yet. CI uses Rust 1.92 and is unaffected.
Fixes #3207
---
Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dockerfile b/Dockerfile
index 4ec1870e3..7c63796fa 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.7
# ── Stage 1: Build ────────────────────────────────────────────
-FROM rust:1.94-slim@sha256:d6782f2b326a10eaf593eb90cafc34a03a287b4a25fe4d0c693c90304b06f6d7 AS builder
+FROM rust:1.93-slim@sha256:9663b80a1621253d30b146454f903de48f0af925c967be48c84745537cd35d8b AS builder
WORKDIR /app
From bb66df5276ea87902ac09809a29f64102a4a9933 Mon Sep 17 00:00:00 2001
From: James Cowan <112015792+jameslcowan@users.noreply.github.com>
Date: Wed, 11 Mar 2026 21:02:20 -0300
Subject: [PATCH 13/15] fix(config): encrypt and decrypt all channel secrets on
save/load (#3217)
Adds symmetric encrypt/decrypt calls for all channel secret fields in
Config::save() and Config::load_or_init(). Previously only nostr.private_key
was handled, leaving all other channel secrets (bot_token, app_token,
access_token, api_token, password, etc.) and gateway.paired_tokens stored
as plaintext when secrets.encrypt = true.
Closes #3175, closes #3173.
Co-authored-by: jameslcowan
---
src/agent/loop_.rs | 6 +-
src/channels/matrix.rs | 130 +++++++------
src/channels/mod.rs | 75 ++++----
src/channels/traits.rs | 12 +-
src/config/schema.rs | 372 ++++++++++++++++++++++++++++++++++++
src/gateway/mod.rs | 26 ++-
src/observability/traits.rs | 5 +-
7 files changed, 516 insertions(+), 110 deletions(-)
diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs
index 3da11d629..3c5172192 100644
--- a/src/agent/loop_.rs
+++ b/src/agent/loop_.rs
@@ -1924,7 +1924,11 @@ async fn execute_one_tool(
) -> Result {
let args_summary = {
let raw = call_arguments.to_string();
- if raw.len() > 300 { format!("{}…", &raw[..300]) } else { raw }
+ if raw.len() > 300 {
+ format!("{}…", &raw[..300])
+ } else {
+ raw
+ }
};
observer.record_event(&ObserverEvent::ToolCallStart {
tool: call_name.to_string(),
diff --git a/src/channels/matrix.rs b/src/channels/matrix.rs
index 525427e1f..38a299716 100644
--- a/src/channels/matrix.rs
+++ b/src/channels/matrix.rs
@@ -1,25 +1,24 @@
use crate::channels::traits::{Channel, ChannelMessage, SendMessage};
use async_trait::async_trait;
-use std::collections::HashMap;
-use std::sync::atomic::{AtomicBool, Ordering};
use matrix_sdk::{
authentication::matrix::MatrixSession,
config::SyncSettings,
ruma::{
+ events::reaction::ReactionEventContent,
+ events::relation::{Annotation, InReplyTo, Thread},
events::room::message::{
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
},
- events::reaction::ReactionEventContent,
- events::relation::{Annotation, InReplyTo, Thread},
events::room::MediaSource,
- OwnedEventId,
- OwnedRoomId, OwnedUserId,
+ OwnedEventId, OwnedRoomId, OwnedUserId,
},
Client as MatrixSdkClient, LoopCtrl, Room, RoomState, SessionMeta, SessionTokens,
};
use reqwest::Client;
use serde::Deserialize;
+use std::collections::HashMap;
use std::path::PathBuf;
+use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex, OnceCell, RwLock};
@@ -541,7 +540,12 @@ impl Channel for MatrixChannel {
async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
let client = self.matrix_client().await?;
let target_room_id = if message.recipient.contains("||") {
- message.recipient.splitn(2, "||").nth(1).unwrap().to_string()
+ message
+ .recipient
+ .splitn(2, "||")
+ .nth(1)
+ .unwrap()
+ .to_string()
} else {
self.target_room_id().await?
};
@@ -582,14 +586,20 @@ impl Channel for MatrixChannel {
let _ = tokio::fs::create_dir_all(&voice_work).await;
let mp3_path = voice_work.join("reply.mp3");
- let tts_text = message.content
- .replace("**", "").replace("*", "")
- .replace("`", "").replace("# ", "");
+ let tts_text = message
+ .content
+ .replace("**", "")
+ .replace("*", "")
+ .replace("`", "")
+ .replace("# ", "");
let tts_ok = tokio::process::Command::new("edge-tts")
- .arg("--text").arg(&tts_text)
- .arg("--write-media").arg(&mp3_path)
- .output().await
+ .arg("--text")
+ .arg(&tts_text)
+ .arg("--write-media")
+ .arg(&mp3_path)
+ .output()
+ .await
.map(|o| o.status.success())
.unwrap_or(false);
@@ -599,21 +609,25 @@ impl Channel for MatrixChannel {
"{}/_matrix/media/v3/upload?filename=voice-reply.mp3",
self.homeserver
);
- if let Ok(resp) = self.http_client
+ if let Ok(resp) = self
+ .http_client
.post(&upload_url)
.header("Authorization", self.auth_header_value())
.header("Content-Type", "audio/mpeg")
.body(audio_data)
- .send().await
+ .send()
+ .await
{
if resp.status().is_success() {
if let Ok(body) = resp.json::().await {
if let Some(content_uri) = body["content_uri"].as_str() {
let encoded_room = Self::encode_path_segment(&target_room_id);
- let txn_id = format!("voice_{}",
+ let txn_id = format!(
+ "voice_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default().as_millis()
+ .unwrap_or_default()
+ .as_millis()
);
let audio_msg = serde_json::json!({
"msgtype": "m.audio",
@@ -625,11 +639,13 @@ impl Channel for MatrixChannel {
"{}/_matrix/client/v3/rooms/{}/send/m.room.message/{}",
self.homeserver, encoded_room, txn_id
);
- let _ = self.http_client
+ let _ = self
+ .http_client
.put(&send_url)
.header("Authorization", self.auth_header_value())
.json(&audio_msg)
- .send().await;
+ .send()
+ .await;
}
}
}
@@ -696,7 +712,9 @@ impl Channel for MatrixChannel {
let voice_mode = Arc::clone(&voice_mode_for_handler);
async move {
- if false /* multi-room: room_id filter disabled */ {
+ if false
+ /* multi-room: room_id filter disabled */
+ {
return;
}
@@ -714,10 +732,8 @@ impl Channel for MatrixChannel {
match source {
MediaSource::Plain(mxc) => {
let rest = mxc.as_str().strip_prefix("mxc://")?;
- let url = format!(
- "{}/_matrix/client/v1/media/download/{}",
- homeserver, rest
- );
+ let url =
+ format!("{}/_matrix/client/v1/media/download/{}", homeserver, rest);
Some((url, name.to_string()))
}
_ => None,
@@ -749,27 +765,25 @@ impl Channel for MatrixChannel {
// Download media to workspace if present
let body = if let Some((url, filename)) = media_download {
let workspace = std::path::PathBuf::from(
- std::env::var("ZEROCLAW_WORKSPACE").unwrap_or_else(|_| "/tmp/zeroclaw-uploads".to_string())
+ std::env::var("ZEROCLAW_WORKSPACE")
+ .unwrap_or_else(|_| "/tmp/zeroclaw-uploads".to_string()),
);
let _ = tokio::fs::create_dir_all(&workspace).await;
let dest = workspace.join(&filename);
let client = reqwest::Client::new();
- match client.get(&url)
+ match client
+ .get(&url)
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await
{
- Ok(resp) if resp.status().is_success() => {
- match resp.bytes().await {
- Ok(bytes) => {
- match tokio::fs::write(&dest, &bytes).await {
- Ok(_) => format!("{} — saved to {}", body, dest.display()),
- Err(_) => format!("{} — failed to write to disk", body),
- }
- }
- Err(_) => format!("{} — download failed", body),
- }
- }
+ Ok(resp) if resp.status().is_success() => match resp.bytes().await {
+ Ok(bytes) => match tokio::fs::write(&dest, &bytes).await {
+ Ok(_) => format!("{} — saved to {}", body, dest.display()),
+ Err(_) => format!("{} — failed to write to disk", body),
+ },
+ Err(_) => format!("{} — download failed", body),
+ },
_ => format!("{} — download failed (auth error?)", body),
}
} else {
@@ -782,7 +796,18 @@ impl Channel for MatrixChannel {
let audio_path = body[path_start + 9..].to_string();
let wav_path = format!("{}.16k.wav", audio_path);
let convert_ok = tokio::process::Command::new("ffmpeg")
- .args(["-y", "-i", &audio_path, "-ar", "16000", "-ac", "1", "-f", "wav", &wav_path])
+ .args([
+ "-y",
+ "-i",
+ &audio_path,
+ "-ar",
+ "16000",
+ "-ac",
+ "1",
+ "-f",
+ "wav",
+ &wav_path,
+ ])
.stderr(std::process::Stdio::null())
.output()
.await
@@ -790,8 +815,14 @@ impl Channel for MatrixChannel {
.unwrap_or(false);
if convert_ok {
let transcription = tokio::process::Command::new("whisper-cpp")
- .args(["-m", "/tmp/ggml-base.en.bin",
- "-f", &wav_path, "--no-timestamps", "-nt"])
+ .args([
+ "-m",
+ "/tmp/ggml-base.en.bin",
+ "-f",
+ &wav_path,
+ "--no-timestamps",
+ "-nt",
+ ])
.output()
.await
.ok()
@@ -930,9 +961,9 @@ impl Channel for MatrixChannel {
.get_room(&target_room)
.ok_or_else(|| anyhow::anyhow!("Matrix room not found for reaction removal"))?;
- let event_id: OwnedEventId = reaction_event_id.parse().map_err(|_| {
- anyhow::anyhow!("Invalid reaction event ID: {}", reaction_event_id)
- })?;
+ let event_id: OwnedEventId = reaction_event_id
+ .parse()
+ .map_err(|_| anyhow::anyhow!("Invalid reaction event ID: {}", reaction_event_id))?;
room.redact(&event_id, None, None).await?;
}
@@ -940,12 +971,7 @@ impl Channel for MatrixChannel {
Ok(())
}
-
- async fn pin_message(
- &self,
- _channel_id: &str,
- message_id: &str,
- ) -> anyhow::Result<()> {
+ async fn pin_message(&self, _channel_id: &str, message_id: &str) -> anyhow::Result<()> {
let room_id = self.target_room_id().await?;
let encoded_room = Self::encode_path_segment(&room_id);
@@ -1001,11 +1027,7 @@ impl Channel for MatrixChannel {
Ok(())
}
- async fn unpin_message(
- &self,
- _channel_id: &str,
- message_id: &str,
- ) -> anyhow::Result<()> {
+ async fn unpin_message(&self, _channel_id: &str, message_id: &str) -> anyhow::Result<()> {
let room_id = self.target_room_id().await?;
let encoded_room = Self::encode_path_segment(&room_id);
diff --git a/src/channels/mod.rs b/src/channels/mod.rs
index 6903276c5..3acdccb5a 100644
--- a/src/channels/mod.rs
+++ b/src/channels/mod.rs
@@ -74,8 +74,8 @@ use crate::agent::loop_::{build_tool_instructions, run_tool_call_loop, scrub_cre
use crate::config::Config;
use crate::identity;
use crate::memory::{self, Memory};
-use crate::observability::{self, runtime_trace, Observer};
use crate::observability::traits::{ObserverEvent, ObserverMetric};
+use crate::observability::{self, runtime_trace, Observer};
use crate::providers::{self, ChatMessage, Provider};
use crate::runtime;
use crate::security::SecurityPolicy;
@@ -102,34 +102,39 @@ struct ChannelNotifyObserver {
impl Observer for ChannelNotifyObserver {
fn record_event(&self, event: &ObserverEvent) {
- match event {
- ObserverEvent::ToolCallStart { tool, arguments } => {
- self.tools_used.store(true, Ordering::Relaxed);
- let detail = match arguments {
- Some(args) if !args.is_empty() => {
- if let Ok(v) = serde_json::from_str::(args) {
- if let Some(cmd) = v.get("command").and_then(|c| c.as_str()) {
- format!(": `{}`", if cmd.len() > 200 { &cmd[..200] } else { cmd })
- } else if let Some(q) = v.get("query").and_then(|c| c.as_str()) {
- format!(": {}", if q.len() > 200 { &q[..200] } else { q })
- } else if let Some(p) = v.get("path").and_then(|c| c.as_str()) {
- format!(": {p}")
- } else if let Some(u) = v.get("url").and_then(|c| c.as_str()) {
- format!(": {u}")
- } else {
- let s = args.to_string();
- if s.len() > 120 { format!(": {}…", &s[..120]) } else { format!(": {s}") }
- }
+ if let ObserverEvent::ToolCallStart { tool, arguments } = event {
+ self.tools_used.store(true, Ordering::Relaxed);
+ let detail = match arguments {
+ Some(args) if !args.is_empty() => {
+ if let Ok(v) = serde_json::from_str::(args) {
+ if let Some(cmd) = v.get("command").and_then(|c| c.as_str()) {
+ format!(": `{}`", if cmd.len() > 200 { &cmd[..200] } else { cmd })
+ } else if let Some(q) = v.get("query").and_then(|c| c.as_str()) {
+ format!(": {}", if q.len() > 200 { &q[..200] } else { q })
+ } else if let Some(p) = v.get("path").and_then(|c| c.as_str()) {
+ format!(": {p}")
+ } else if let Some(u) = v.get("url").and_then(|c| c.as_str()) {
+ format!(": {u}")
} else {
let s = args.to_string();
- if s.len() > 120 { format!(": {}…", &s[..120]) } else { format!(": {s}") }
+ if s.len() > 120 {
+ format!(": {}…", &s[..120])
+ } else {
+ format!(": {s}")
+ }
+ }
+ } else {
+ let s = args.to_string();
+ if s.len() > 120 {
+ format!(": {}…", &s[..120])
+ } else {
+ format!(": {s}")
}
}
- _ => String::new(),
- };
- let _ = self.tx.send(format!("\u{1F527} `{tool}`{detail}"));
- }
- _ => {}
+ }
+ _ => String::new(),
+ };
+ let _ = self.tx.send(format!("\u{1F527} `{tool}`{detail}"));
}
self.inner.record_event(event);
}
@@ -1857,7 +1862,11 @@ async fn process_channel_message(
let notify_channel = target_channel.clone();
let notify_reply_target = msg.reply_target.clone();
let notify_thread_root = msg.id.clone();
- let notify_task = if msg.channel != "cli" {
+ let notify_task = if msg.channel == "cli" {
+ Some(tokio::spawn(async move {
+ while notify_rx.recv().await.is_some() {}
+ }))
+ } else {
Some(tokio::spawn(async move {
let thread_ts = Some(notify_thread_root);
while let Some(text) = notify_rx.recv().await {
@@ -1871,10 +1880,6 @@ async fn process_channel_message(
}
}
}))
- } else {
- Some(tokio::spawn(async move {
- while notify_rx.recv().await.is_some() {}
- }))
};
// Record history length before tool loop so we can extract tool context after.
@@ -4310,7 +4315,7 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert!(sent_messages.len() >= 1);
+ assert!(!sent_messages.is_empty());
let reply = sent_messages.last().unwrap();
assert!(reply.starts_with("chat-42:"));
assert!(reply.contains("BTC is currently around"));
@@ -4371,7 +4376,7 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert!(sent_messages.len() >= 1);
+ assert!(!sent_messages.is_empty());
let reply = sent_messages.last().unwrap();
assert!(reply.contains("BTC is currently around"));
@@ -4506,7 +4511,7 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert!(sent_messages.len() >= 1);
+ assert!(!sent_messages.is_empty());
let reply = sent_messages.last().unwrap();
assert!(reply.starts_with("chat-84:"));
assert!(reply.contains("alias-tag flow resolved"));
@@ -4897,7 +4902,7 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert!(sent_messages.len() >= 1);
+ assert!(!sent_messages.is_empty());
let reply = sent_messages.last().unwrap();
assert!(reply.starts_with("chat-iter-success:"));
assert!(reply.contains("Completed after 11 tool iterations."));
@@ -4959,7 +4964,7 @@ BTC is currently around $65,000 based on latest tool output."#
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
- assert!(sent_messages.len() >= 1);
+ assert!(!sent_messages.is_empty());
let reply = sent_messages.last().unwrap();
assert!(reply.starts_with("chat-iter-fail:"));
assert!(reply.contains("⚠️ Error: Agent exceeded maximum tool iterations (3)"));
diff --git a/src/channels/traits.rs b/src/channels/traits.rs
index 3fc1570d6..501e42735 100644
--- a/src/channels/traits.rs
+++ b/src/channels/traits.rs
@@ -144,20 +144,12 @@ pub trait Channel: Send + Sync {
}
/// Pin a message in the channel.
- async fn pin_message(
- &self,
- _channel_id: &str,
- _message_id: &str,
- ) -> anyhow::Result<()> {
+ async fn pin_message(&self, _channel_id: &str, _message_id: &str) -> anyhow::Result<()> {
Ok(())
}
/// Unpin a previously pinned message.
- async fn unpin_message(
- &self,
- _channel_id: &str,
- _message_id: &str,
- ) -> anyhow::Result<()> {
+ async fn unpin_message(&self, _channel_id: &str, _message_id: &str) -> anyhow::Result<()> {
Ok(())
}
}
diff --git a/src/config/schema.rs b/src/config/schema.rs
index b91716885..2f7695886 100644
--- a/src/config/schema.rs
+++ b/src/config/schema.rs
@@ -4284,6 +4284,192 @@ impl Config {
)?;
}
+ // Decrypt channel secrets
+ if let Some(ref mut tg) = config.channels_config.telegram {
+ decrypt_secret(
+ &store,
+ &mut tg.bot_token,
+ "config.channels_config.telegram.bot_token",
+ )?;
+ }
+ if let Some(ref mut dc) = config.channels_config.discord {
+ decrypt_secret(
+ &store,
+ &mut dc.bot_token,
+ "config.channels_config.discord.bot_token",
+ )?;
+ }
+ if let Some(ref mut sl) = config.channels_config.slack {
+ decrypt_secret(
+ &store,
+ &mut sl.bot_token,
+ "config.channels_config.slack.bot_token",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut sl.app_token,
+ "config.channels_config.slack.app_token",
+ )?;
+ }
+ if let Some(ref mut mm) = config.channels_config.mattermost {
+ decrypt_secret(
+ &store,
+ &mut mm.bot_token,
+ "config.channels_config.mattermost.bot_token",
+ )?;
+ }
+ if let Some(ref mut mx) = config.channels_config.matrix {
+ decrypt_secret(
+ &store,
+ &mut mx.access_token,
+ "config.channels_config.matrix.access_token",
+ )?;
+ }
+ if let Some(ref mut wa) = config.channels_config.whatsapp {
+ decrypt_optional_secret(
+ &store,
+ &mut wa.access_token,
+ "config.channels_config.whatsapp.access_token",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut wa.app_secret,
+ "config.channels_config.whatsapp.app_secret",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut wa.verify_token,
+ "config.channels_config.whatsapp.verify_token",
+ )?;
+ }
+ if let Some(ref mut lq) = config.channels_config.linq {
+ decrypt_secret(
+ &store,
+ &mut lq.api_token,
+ "config.channels_config.linq.api_token",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut lq.signing_secret,
+ "config.channels_config.linq.signing_secret",
+ )?;
+ }
+ if let Some(ref mut wt) = config.channels_config.wati {
+ decrypt_secret(
+ &store,
+ &mut wt.api_token,
+ "config.channels_config.wati.api_token",
+ )?;
+ }
+ if let Some(ref mut nc) = config.channels_config.nextcloud_talk {
+ decrypt_secret(
+ &store,
+ &mut nc.app_token,
+ "config.channels_config.nextcloud_talk.app_token",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut nc.webhook_secret,
+ "config.channels_config.nextcloud_talk.webhook_secret",
+ )?;
+ }
+ if let Some(ref mut em) = config.channels_config.email {
+ decrypt_secret(
+ &store,
+ &mut em.password,
+ "config.channels_config.email.password",
+ )?;
+ }
+ if let Some(ref mut irc) = config.channels_config.irc {
+ decrypt_optional_secret(
+ &store,
+ &mut irc.server_password,
+ "config.channels_config.irc.server_password",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut irc.nickserv_password,
+ "config.channels_config.irc.nickserv_password",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut irc.sasl_password,
+ "config.channels_config.irc.sasl_password",
+ )?;
+ }
+ if let Some(ref mut lk) = config.channels_config.lark {
+ decrypt_secret(
+ &store,
+ &mut lk.app_secret,
+ "config.channels_config.lark.app_secret",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut lk.encrypt_key,
+ "config.channels_config.lark.encrypt_key",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut lk.verification_token,
+ "config.channels_config.lark.verification_token",
+ )?;
+ }
+ if let Some(ref mut fs) = config.channels_config.feishu {
+ decrypt_secret(
+ &store,
+ &mut fs.app_secret,
+ "config.channels_config.feishu.app_secret",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut fs.encrypt_key,
+ "config.channels_config.feishu.encrypt_key",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut fs.verification_token,
+ "config.channels_config.feishu.verification_token",
+ )?;
+ }
+ if let Some(ref mut dt) = config.channels_config.dingtalk {
+ decrypt_secret(
+ &store,
+ &mut dt.client_secret,
+ "config.channels_config.dingtalk.client_secret",
+ )?;
+ }
+ if let Some(ref mut qq) = config.channels_config.qq {
+ decrypt_secret(
+ &store,
+ &mut qq.app_secret,
+ "config.channels_config.qq.app_secret",
+ )?;
+ }
+ if let Some(ref mut wh) = config.channels_config.webhook {
+ decrypt_optional_secret(
+ &store,
+ &mut wh.secret,
+ "config.channels_config.webhook.secret",
+ )?;
+ }
+ if let Some(ref mut ct) = config.channels_config.clawdtalk {
+ decrypt_secret(
+ &store,
+ &mut ct.api_key,
+ "config.channels_config.clawdtalk.api_key",
+ )?;
+ decrypt_optional_secret(
+ &store,
+ &mut ct.webhook_secret,
+ "config.channels_config.clawdtalk.webhook_secret",
+ )?;
+ }
+
+ // Decrypt gateway paired tokens
+ for token in &mut config.gateway.paired_tokens {
+ decrypt_secret(&store, token, "config.gateway.paired_tokens[]")?;
+ }
+
config.apply_env_overrides();
config.validate()?;
tracing::info!(
@@ -4934,6 +5120,192 @@ impl Config {
)?;
}
+ // Encrypt channel secrets
+ if let Some(ref mut tg) = config_to_save.channels_config.telegram {
+ encrypt_secret(
+ &store,
+ &mut tg.bot_token,
+ "config.channels_config.telegram.bot_token",
+ )?;
+ }
+ if let Some(ref mut dc) = config_to_save.channels_config.discord {
+ encrypt_secret(
+ &store,
+ &mut dc.bot_token,
+ "config.channels_config.discord.bot_token",
+ )?;
+ }
+ if let Some(ref mut sl) = config_to_save.channels_config.slack {
+ encrypt_secret(
+ &store,
+ &mut sl.bot_token,
+ "config.channels_config.slack.bot_token",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut sl.app_token,
+ "config.channels_config.slack.app_token",
+ )?;
+ }
+ if let Some(ref mut mm) = config_to_save.channels_config.mattermost {
+ encrypt_secret(
+ &store,
+ &mut mm.bot_token,
+ "config.channels_config.mattermost.bot_token",
+ )?;
+ }
+ if let Some(ref mut mx) = config_to_save.channels_config.matrix {
+ encrypt_secret(
+ &store,
+ &mut mx.access_token,
+ "config.channels_config.matrix.access_token",
+ )?;
+ }
+ if let Some(ref mut wa) = config_to_save.channels_config.whatsapp {
+ encrypt_optional_secret(
+ &store,
+ &mut wa.access_token,
+ "config.channels_config.whatsapp.access_token",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut wa.app_secret,
+ "config.channels_config.whatsapp.app_secret",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut wa.verify_token,
+ "config.channels_config.whatsapp.verify_token",
+ )?;
+ }
+ if let Some(ref mut lq) = config_to_save.channels_config.linq {
+ encrypt_secret(
+ &store,
+ &mut lq.api_token,
+ "config.channels_config.linq.api_token",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut lq.signing_secret,
+ "config.channels_config.linq.signing_secret",
+ )?;
+ }
+ if let Some(ref mut wt) = config_to_save.channels_config.wati {
+ encrypt_secret(
+ &store,
+ &mut wt.api_token,
+ "config.channels_config.wati.api_token",
+ )?;
+ }
+ if let Some(ref mut nc) = config_to_save.channels_config.nextcloud_talk {
+ encrypt_secret(
+ &store,
+ &mut nc.app_token,
+ "config.channels_config.nextcloud_talk.app_token",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut nc.webhook_secret,
+ "config.channels_config.nextcloud_talk.webhook_secret",
+ )?;
+ }
+ if let Some(ref mut em) = config_to_save.channels_config.email {
+ encrypt_secret(
+ &store,
+ &mut em.password,
+ "config.channels_config.email.password",
+ )?;
+ }
+ if let Some(ref mut irc) = config_to_save.channels_config.irc {
+ encrypt_optional_secret(
+ &store,
+ &mut irc.server_password,
+ "config.channels_config.irc.server_password",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut irc.nickserv_password,
+ "config.channels_config.irc.nickserv_password",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut irc.sasl_password,
+ "config.channels_config.irc.sasl_password",
+ )?;
+ }
+ if let Some(ref mut lk) = config_to_save.channels_config.lark {
+ encrypt_secret(
+ &store,
+ &mut lk.app_secret,
+ "config.channels_config.lark.app_secret",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut lk.encrypt_key,
+ "config.channels_config.lark.encrypt_key",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut lk.verification_token,
+ "config.channels_config.lark.verification_token",
+ )?;
+ }
+ if let Some(ref mut fs) = config_to_save.channels_config.feishu {
+ encrypt_secret(
+ &store,
+ &mut fs.app_secret,
+ "config.channels_config.feishu.app_secret",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut fs.encrypt_key,
+ "config.channels_config.feishu.encrypt_key",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut fs.verification_token,
+ "config.channels_config.feishu.verification_token",
+ )?;
+ }
+ if let Some(ref mut dt) = config_to_save.channels_config.dingtalk {
+ encrypt_secret(
+ &store,
+ &mut dt.client_secret,
+ "config.channels_config.dingtalk.client_secret",
+ )?;
+ }
+ if let Some(ref mut qq) = config_to_save.channels_config.qq {
+ encrypt_secret(
+ &store,
+ &mut qq.app_secret,
+ "config.channels_config.qq.app_secret",
+ )?;
+ }
+ if let Some(ref mut wh) = config_to_save.channels_config.webhook {
+ encrypt_optional_secret(
+ &store,
+ &mut wh.secret,
+ "config.channels_config.webhook.secret",
+ )?;
+ }
+ if let Some(ref mut ct) = config_to_save.channels_config.clawdtalk {
+ encrypt_secret(
+ &store,
+ &mut ct.api_key,
+ "config.channels_config.clawdtalk.api_key",
+ )?;
+ encrypt_optional_secret(
+ &store,
+ &mut ct.webhook_secret,
+ "config.channels_config.clawdtalk.webhook_secret",
+ )?;
+ }
+
+ // Encrypt gateway paired tokens
+ for token in &mut config_to_save.gateway.paired_tokens {
+ encrypt_secret(&store, token, "config.gateway.paired_tokens[]")?;
+ }
+
let toml_str =
toml::to_string_pretty(&config_to_save).context("Failed to serialize config")?;
diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs
index d65af32dc..1fbc7e02e 100644
--- a/src/gateway/mod.rs
+++ b/src/gateway/mod.rs
@@ -1933,16 +1933,24 @@ mod tests {
.await
.unwrap();
- let saved = tokio::fs::read_to_string(config_path).await.unwrap();
- let parsed: Config = toml::from_str(&saved).unwrap();
- assert_eq!(parsed.gateway.paired_tokens.len(), 1);
- let persisted = &parsed.gateway.paired_tokens[0];
- assert_eq!(persisted.len(), 64);
- assert!(persisted.chars().all(|c| c.is_ascii_hexdigit()));
+ // In-memory tokens should remain as plaintext 64-char hex hashes.
+ let plaintext = {
+ let in_memory = shared_config.lock();
+ assert_eq!(in_memory.gateway.paired_tokens.len(), 1);
+ in_memory.gateway.paired_tokens[0].clone()
+ };
+ assert_eq!(plaintext.len(), 64);
+ assert!(plaintext.chars().all(|c: char| c.is_ascii_hexdigit()));
- let in_memory = shared_config.lock();
- assert_eq!(in_memory.gateway.paired_tokens.len(), 1);
- assert_eq!(&in_memory.gateway.paired_tokens[0], persisted);
+ // On disk, the token should be encrypted (secrets.encrypt defaults to true).
+ let saved = tokio::fs::read_to_string(config_path).await.unwrap();
+ let raw_parsed: Config = toml::from_str(&saved).unwrap();
+ assert_eq!(raw_parsed.gateway.paired_tokens.len(), 1);
+ let on_disk = &raw_parsed.gateway.paired_tokens[0];
+ assert!(
+ crate::security::SecretStore::is_encrypted(on_disk),
+ "paired_token should be encrypted on disk"
+ );
}
#[test]
diff --git a/src/observability/traits.rs b/src/observability/traits.rs
index 82235184c..c1391aa2e 100644
--- a/src/observability/traits.rs
+++ b/src/observability/traits.rs
@@ -40,7 +40,10 @@ pub enum ObserverEvent {
cost_usd: Option,
},
/// A tool call is about to be executed.
- ToolCallStart { tool: String, arguments: Option },
+ ToolCallStart {
+ tool: String,
+ arguments: Option,
+ },
/// A tool call has completed with a success/failure outcome.
ToolCall {
tool: String,
From 195c7ba919eb8b94aefed115b5664d95177d3584 Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 20:08:16 -0400
Subject: [PATCH 14/15] fix(agent): resolve display text for tool-only turns
(#3054)
---
src/agent/loop_.rs | 45 ++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 40 insertions(+), 5 deletions(-)
diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs
index 3c5172192..4f8e51bf4 100644
--- a/src/agent/loop_.rs
+++ b/src/agent/loop_.rs
@@ -1856,6 +1856,18 @@ fn build_assistant_history_with_tool_calls(text: &str, tool_calls: &[ToolCall])
parts.join("\n")
}
+fn resolve_display_text(response_text: &str, parsed_text: &str, has_tool_calls: bool) -> String {
+ if has_tool_calls {
+ return parsed_text.to_string();
+ }
+
+ if parsed_text.is_empty() {
+ response_text.to_string()
+ } else {
+ parsed_text.to_string()
+ }
+}
+
#[derive(Debug, Clone)]
struct ParsedToolCall {
name: String,
@@ -2344,11 +2356,8 @@ pub(crate) async fn run_tool_call_loop(
}
};
- let display_text = if parsed_text.is_empty() {
- response_text.clone()
- } else {
- parsed_text
- };
+ let display_text =
+ resolve_display_text(&response_text, &parsed_text, !tool_calls.is_empty());
let display_text = strip_tool_result_blocks(&display_text);
// ── Progress: LLM responded ─────────────────────────────
@@ -4088,6 +4097,32 @@ mod tests {
);
}
+ #[test]
+ fn resolve_display_text_hides_raw_payload_for_tool_only_turns() {
+ let display = resolve_display_text(
+ "{\"name\":\"memory_store\"}",
+ "",
+ true,
+ );
+ assert!(display.is_empty());
+ }
+
+ #[test]
+ fn resolve_display_text_keeps_plain_text_for_tool_turns() {
+ let display = resolve_display_text(
+ "{\"name\":\"shell\"}",
+ "Let me check that.",
+ true,
+ );
+ assert_eq!(display, "Let me check that.");
+ }
+
+ #[test]
+ fn resolve_display_text_uses_response_text_for_final_turns() {
+ let display = resolve_display_text("Final answer", "", false);
+ assert_eq!(display, "Final answer");
+ }
+
#[test]
fn parse_tool_calls_extracts_single_call() {
let response = r#"Let me check that.
From 67e581d8ae654d4499909150ad85aec4db567dde Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 20:08:22 -0400
Subject: [PATCH 15/15] fix(gateway): add health and pairing proxy routes to
vite dev server (#3056)
Co-authored-by: Claude Opus 4.6
---
web/vite.config.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/web/vite.config.ts b/web/vite.config.ts
index a0edf3b79..4be51b34d 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -16,6 +16,14 @@ export default defineConfig({
},
server: {
proxy: {
+ "/health": {
+ target: "http://localhost:5555",
+ changeOrigin: true,
+ },
+ "/pair": {
+ target: "http://localhost:5555",
+ changeOrigin: true,
+ },
"/api": {
target: "http://localhost:5555",
changeOrigin: true,