From 7dc3c6d0670e316145f38ea5d79eefd17e4c978c Mon Sep 17 00:00:00 2001 From: "ironclaw-ci[bot]" <266877842+ironclaw-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:20:16 -0700 Subject: [PATCH 01/13] chore: release v0.20.0 (#1310) Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aad4993..7bf7c55d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.20.0](https://github.com/nearai/ironclaw/compare/v0.19.0...v0.20.0) - 2026-03-19 + +### Added + +- *(self-repair)* wire stuck_threshold, store, and builder ([#712](https://github.com/nearai/ironclaw/pull/712)) +- *(testing)* add FaultInjector framework for StubLlm ([#1233](https://github.com/nearai/ironclaw/pull/1233)) +- *(gateway)* unified settings page with subtabs ([#1191](https://github.com/nearai/ironclaw/pull/1191)) +- upgrade MiniMax default model to M2.7 ([#1357](https://github.com/nearai/ironclaw/pull/1357)) + +### Fixed + +- navigate telegram E2E tests to channels subtab ([#1408](https://github.com/nearai/ironclaw/pull/1408)) +- add missing `builder` field and update E2E extensions tab navigation ([#1400](https://github.com/nearai/ironclaw/pull/1400)) +- remove debug_assert guards that panic on valid error paths ([#1385](https://github.com/nearai/ironclaw/pull/1385)) +- address valid review comments from PR #1359 ([#1380](https://github.com/nearai/ironclaw/pull/1380)) +- full_job routine runs stay running until linked job completion ([#1374](https://github.com/nearai/ironclaw/pull/1374)) +- full_job routine concurrency tracks linked job lifetime ([#1372](https://github.com/nearai/ironclaw/pull/1372)) +- remove -x from coverage pytest to prevent suite-blocking failures ([#1360](https://github.com/nearai/ironclaw/pull/1360)) +- add debug_assert invariant guards to critical code paths ([#1312](https://github.com/nearai/ironclaw/pull/1312)) +- *(mcp)* retry after missing session id errors ([#1355](https://github.com/nearai/ironclaw/pull/1355)) +- *(telegram)* preserve polling after secret-blocked updates ([#1353](https://github.com/nearai/ironclaw/pull/1353)) +- *(llm)* cap retry-after delays ([#1351](https://github.com/nearai/ironclaw/pull/1351)) +- *(setup)* remove nonexistent webhook secret command hint ([#1349](https://github.com/nearai/ironclaw/pull/1349)) +- Rate limiter returns retry after None instead of a duration ([#1269](https://github.com/nearai/ironclaw/pull/1269)) + +### Other + +- bump telegram channel version to 0.2.5 ([#1410](https://github.com/nearai/ironclaw/pull/1410)) +- *(ci)* enforce test requirement for state machine and resilience changes ([#1230](https://github.com/nearai/ironclaw/pull/1230)) ([#1304](https://github.com/nearai/ironclaw/pull/1304)) +- Fix duplicate LLM responses for matched event routines ([#1275](https://github.com/nearai/ironclaw/pull/1275)) +- add Japanese README ([#1306](https://github.com/nearai/ironclaw/pull/1306)) +- *(ci)* add coverage gates via codecov.yml ([#1228](https://github.com/nearai/ironclaw/pull/1228)) ([#1291](https://github.com/nearai/ironclaw/pull/1291)) +- Redesign routine create requests for LLMs ([#1147](https://github.com/nearai/ironclaw/pull/1147)) + ## [0.19.0](https://github.com/nearai/ironclaw/compare/v0.18.0...v0.19.0) - 2026-03-17 ### Added diff --git a/Cargo.lock b/Cargo.lock index 2c5547e0..ec10ff96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3436,7 +3436,7 @@ dependencies = [ [[package]] name = "ironclaw" -version = "0.19.0" +version = "0.20.0" dependencies = [ "aes-gcm", "aho-corasick", diff --git a/Cargo.toml b/Cargo.toml index 5b452651..1447c66e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ exclude = [ [package] name = "ironclaw" -version = "0.19.0" +version = "0.20.0" edition = "2024" rust-version = "1.92" description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly" From e4d3200d808737293337220403e21cf4a364e85b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:04:07 -0700 Subject: [PATCH 02/13] chore: update WASM artifact SHA256 checksums [skip ci] (#1424) Co-authored-by: github-actions[bot] --- registry/channels/telegram.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/channels/telegram.json b/registry/channels/telegram.json index 85d793ed..52f66ce3 100644 --- a/registry/channels/telegram.json +++ b/registry/channels/telegram.json @@ -18,8 +18,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-telegram-0.2.4-wasm32-wasip2.tar.gz", - "sha256": "a7cb300ec1c946831cfceaa95c1dc8f30d0f42a3924f3cb5de8098821573f4b8" + "url": "https://github.com/nearai/ironclaw/releases/download/v0.20.0/channel-telegram-0.2.5-wasm32-wasip2.tar.gz", + "sha256": "1ef20a538f55b379e049356e4d6758006251846bc3365ceaa1c87eba8379a329" } }, "auth_summary": { From e077e1277d730601785738d9b8d4ba9aadaa8f29 Mon Sep 17 00:00:00 2001 From: Henry Park Date: Fri, 20 Mar 2026 10:33:57 -0700 Subject: [PATCH 03/13] fix: bump Feishu channel version for promotion --- registry/channels/feishu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/channels/feishu.json b/registry/channels/feishu.json index 66cecf1d..9cac2562 100644 --- a/registry/channels/feishu.json +++ b/registry/channels/feishu.json @@ -2,7 +2,7 @@ "name": "feishu", "display_name": "Feishu / Lark Channel", "kind": "channel", - "version": "0.1.1", + "version": "0.1.2", "wit_version": "0.3.0", "description": "Talk to your agent through a Feishu or Lark bot", "keywords": [ From 91a241a3c7fccb4d17d61c5d011d103968163b68 Mon Sep 17 00:00:00 2001 From: "ironclaw-ci[bot]" <266877842+ironclaw-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:23:39 -0700 Subject: [PATCH 04/13] chore: release v0.21.0 (#1472) Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 18 ++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bf7c55d..5ef9f5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.21.0](https://github.com/nearai/ironclaw/compare/v0.20.0...v0.21.0) - 2026-03-20 + +### Added + +- structured fallback deliverables for failed/stuck jobs ([#236](https://github.com/nearai/ironclaw/pull/236)) +- LRU embedding cache for workspace search ([#1423](https://github.com/nearai/ironclaw/pull/1423)) +- receive relay events via webhook callbacks ([#1254](https://github.com/nearai/ironclaw/pull/1254)) + +### Fixed + +- bump Feishu channel version for promotion +- *(approval)* make "always" auto-approve work for credentialed HTTP requests ([#1257](https://github.com/nearai/ironclaw/pull/1257)) +- skip NEAR AI session check when backend is not nearai ([#1413](https://github.com/nearai/ironclaw/pull/1413)) + +### Other + +- Make hosted OAuth and MCP auth generic ([#1375](https://github.com/nearai/ironclaw/pull/1375)) + ## [0.20.0](https://github.com/nearai/ironclaw/compare/v0.19.0...v0.20.0) - 2026-03-19 ### Added diff --git a/Cargo.lock b/Cargo.lock index ec10ff96..df7c54ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3436,7 +3436,7 @@ dependencies = [ [[package]] name = "ironclaw" -version = "0.20.0" +version = "0.21.0" dependencies = [ "aes-gcm", "aho-corasick", diff --git a/Cargo.toml b/Cargo.toml index 1447c66e..ab003e64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ exclude = [ [package] name = "ironclaw" -version = "0.20.0" +version = "0.21.0" edition = "2024" rust-version = "1.92" description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly" From d47b4b0346537497b47bad0488f3611f82c514ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:24:47 -0700 Subject: [PATCH 05/13] chore: update WASM artifact SHA256 checksums [skip ci] (#1481) Co-authored-by: github-actions[bot] --- registry/channels/feishu.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/channels/feishu.json b/registry/channels/feishu.json index 9cac2562..dc02b2c7 100644 --- a/registry/channels/feishu.json +++ b/registry/channels/feishu.json @@ -19,8 +19,8 @@ }, "artifacts": { "wasm32-wasip2": { - "sha256": "5fca74022264d1c8e78a0853766276f7ffa3cf0d8065b2f51ca10985acad4714", - "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-feishu-0.1.1-wasm32-wasip2.tar.gz" + "sha256": "52def36121a93cf0b06dd6d594dc9528745fae56ded7d632998700cc595be762", + "url": "https://github.com/nearai/ironclaw/releases/download/v0.21.0/channel-feishu-0.1.2-wasm32-wasip2.tar.gz" } }, "auth_summary": { From 67a025e2faf73c9f970129523c7bc18b5d3c3c9e Mon Sep 17 00:00:00 2001 From: serrrfirat Date: Wed, 25 Mar 2026 13:59:50 +0300 Subject: [PATCH 06/13] fix(deps): unblock promotion PR #1451 cargo-deny --- Cargo.lock | 18 +++++++++--------- deny.toml | 2 ++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c5547e0..0a6b5797 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2339,7 +2339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5575,7 +5575,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5624,7 +5624,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -5696,9 +5696,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -6457,9 +6457,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -6479,10 +6479,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/deny.toml b/deny.toml index 80aa2215..fddb3d43 100644 --- a/deny.toml +++ b/deny.toml @@ -15,6 +15,8 @@ ignore = [ "RUSTSEC-2026-0020", # wasmtime wasi:http/types.fields panic — mitigated by fuel limits "RUSTSEC-2026-0021", + # rustls-webpki CRL distributionPoint matching — 0.102.8 pinned by libsql transitive dep + "RUSTSEC-2026-0049", ] [licenses] From ef37d705a16a3ef91bac753ea0c344c998d55a19 Mon Sep 17 00:00:00 2001 From: Henry Park Date: Wed, 25 Mar 2026 15:56:49 -0700 Subject: [PATCH 07/13] Merge pull request #1655 from nearai/codex/fix-staging-promotion-1451-version-bumps fix: bump registry versions for staging promotion 1451 --- registry/channels/feishu.json | 2 +- registry/tools/github.json | 2 +- registry/tools/gmail.json | 2 +- registry/tools/google-calendar.json | 2 +- registry/tools/google-docs.json | 2 +- registry/tools/google-drive.json | 2 +- registry/tools/google-sheets.json | 2 +- registry/tools/google-slides.json | 2 +- registry/tools/llm-context.json | 2 +- registry/tools/slack.json | 2 +- registry/tools/telegram.json | 2 +- registry/tools/web-search.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/registry/channels/feishu.json b/registry/channels/feishu.json index 66cecf1d..e666fcd9 100644 --- a/registry/channels/feishu.json +++ b/registry/channels/feishu.json @@ -2,7 +2,7 @@ "name": "feishu", "display_name": "Feishu / Lark Channel", "kind": "channel", - "version": "0.1.1", + "version": "0.1.3", "wit_version": "0.3.0", "description": "Talk to your agent through a Feishu or Lark bot", "keywords": [ diff --git a/registry/tools/github.json b/registry/tools/github.json index e760c4df..aa89794e 100644 --- a/registry/tools/github.json +++ b/registry/tools/github.json @@ -2,7 +2,7 @@ "name": "github", "display_name": "GitHub", "kind": "tool", - "version": "0.2.1", + "version": "0.2.2", "wit_version": "0.3.0", "description": "GitHub integration for issues, PRs, repos, and code search", "keywords": [ diff --git a/registry/tools/gmail.json b/registry/tools/gmail.json index 08913ce6..90573115 100644 --- a/registry/tools/gmail.json +++ b/registry/tools/gmail.json @@ -2,7 +2,7 @@ "name": "gmail", "display_name": "Gmail", "kind": "tool", - "version": "0.2.0", + "version": "0.2.1", "wit_version": "0.3.0", "description": "Read, send, and manage Gmail messages and threads", "keywords": [ diff --git a/registry/tools/google-calendar.json b/registry/tools/google-calendar.json index c43112d3..a9cdc539 100644 --- a/registry/tools/google-calendar.json +++ b/registry/tools/google-calendar.json @@ -2,7 +2,7 @@ "name": "google-calendar", "display_name": "Google Calendar", "kind": "tool", - "version": "0.2.0", + "version": "0.2.1", "wit_version": "0.3.0", "description": "Create, read, update, and delete Google Calendar events", "keywords": [ diff --git a/registry/tools/google-docs.json b/registry/tools/google-docs.json index 9f1ab133..e3206b5e 100644 --- a/registry/tools/google-docs.json +++ b/registry/tools/google-docs.json @@ -2,7 +2,7 @@ "name": "google-docs", "display_name": "Google Docs", "kind": "tool", - "version": "0.2.0", + "version": "0.2.1", "wit_version": "0.3.0", "description": "Create and edit Google Docs documents", "keywords": [ diff --git a/registry/tools/google-drive.json b/registry/tools/google-drive.json index 9766e555..3de90ed1 100644 --- a/registry/tools/google-drive.json +++ b/registry/tools/google-drive.json @@ -2,7 +2,7 @@ "name": "google-drive", "display_name": "Google Drive", "kind": "tool", - "version": "0.2.0", + "version": "0.2.1", "wit_version": "0.3.0", "description": "Upload, download, search, and manage Google Drive files and folders", "keywords": [ diff --git a/registry/tools/google-sheets.json b/registry/tools/google-sheets.json index b63265e1..4e5722e9 100644 --- a/registry/tools/google-sheets.json +++ b/registry/tools/google-sheets.json @@ -2,7 +2,7 @@ "name": "google-sheets", "display_name": "Google Sheets", "kind": "tool", - "version": "0.2.0", + "version": "0.2.1", "wit_version": "0.3.0", "description": "Read and write Google Sheets spreadsheet data", "keywords": [ diff --git a/registry/tools/google-slides.json b/registry/tools/google-slides.json index 54187531..bbd4f8c0 100644 --- a/registry/tools/google-slides.json +++ b/registry/tools/google-slides.json @@ -2,7 +2,7 @@ "name": "google-slides", "display_name": "Google Slides", "kind": "tool", - "version": "0.2.0", + "version": "0.2.1", "wit_version": "0.3.0", "description": "Create and edit Google Slides presentations", "keywords": [ diff --git a/registry/tools/llm-context.json b/registry/tools/llm-context.json index e4e9808c..acea3301 100644 --- a/registry/tools/llm-context.json +++ b/registry/tools/llm-context.json @@ -2,7 +2,7 @@ "name": "llm-context", "display_name": "LLM Context", "kind": "tool", - "version": "0.1.0", + "version": "0.1.1", "wit_version": "0.3.0", "description": "Fetch pre-extracted web content from Brave Search for grounding LLM answers (RAG, fact-checking)", "keywords": [ diff --git a/registry/tools/slack.json b/registry/tools/slack.json index 8e1df989..27bce7c4 100644 --- a/registry/tools/slack.json +++ b/registry/tools/slack.json @@ -2,7 +2,7 @@ "name": "slack-tool", "display_name": "Slack Tool", "kind": "tool", - "version": "0.2.0", + "version": "0.2.1", "wit_version": "0.3.0", "description": "Your agent uses Slack to post and read messages in your workspace", "keywords": [ diff --git a/registry/tools/telegram.json b/registry/tools/telegram.json index 12e58c68..d3370940 100644 --- a/registry/tools/telegram.json +++ b/registry/tools/telegram.json @@ -2,7 +2,7 @@ "name": "telegram-mtproto", "display_name": "Telegram Tool", "kind": "tool", - "version": "0.2.0", + "version": "0.2.1", "wit_version": "0.3.0", "description": "Your agent uses your Telegram account to read and send messages", "keywords": [ diff --git a/registry/tools/web-search.json b/registry/tools/web-search.json index 5c1dedef..47f4d699 100644 --- a/registry/tools/web-search.json +++ b/registry/tools/web-search.json @@ -2,7 +2,7 @@ "name": "web-search", "display_name": "Web Search", "kind": "tool", - "version": "0.2.1", + "version": "0.2.2", "wit_version": "0.3.0", "description": "Search the web using Brave Search API", "keywords": [ From 0b4e7c761b3e12853d743ab5d454255334e2c1c6 Mon Sep 17 00:00:00 2001 From: "ironclaw-ci[bot]" <266877842+ironclaw-ci[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:44:53 -0700 Subject: [PATCH 08/13] chore: release v0.22.0 (#1601) Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef9f5f9..9acc56ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.22.0](https://github.com/nearai/ironclaw/compare/ironclaw-v0.21.0...ironclaw-v0.22.0) - 2026-03-25 + +### Added + +- *(agent)* thread per-tool reasoning through provider, session, and all surfaces ([#1513](https://github.com/nearai/ironclaw/pull/1513)) +- *(cli)* show credential auth status in tool info ([#1572](https://github.com/nearai/ironclaw/pull/1572)) +- multi-tenant auth with per-user workspace isolation ([#1118](https://github.com/nearai/ironclaw/pull/1118)) +- *(cli)* add ironclaw models subcommands (list/status/set/set-provider) ([#1043](https://github.com/nearai/ironclaw/pull/1043)) +- *(workspace)* multi-scope workspace reads ([#1117](https://github.com/nearai/ironclaw/pull/1117)) +- *(ux)* complete UX overhaul — design system, onboarding, web polish ([#1277](https://github.com/nearai/ironclaw/pull/1277)) +- *(gemini_oauth)* full Gemini CLI OAuth integration with Cloud Code API ([#1356](https://github.com/nearai/ironclaw/pull/1356)) +- *(shell)* add Low/Medium/High risk levels for graduated command approval (closes #172) ([#368](https://github.com/nearai/ironclaw/pull/368)) +- *(agent)* queue and merge messages during active turns ([#1412](https://github.com/nearai/ironclaw/pull/1412)) +- *(cli)* add `ironclaw hooks list` subcommand ([#1023](https://github.com/nearai/ironclaw/pull/1023)) +- *(extensions)* support text setup fields in web configure modal ([#496](https://github.com/nearai/ironclaw/pull/496)) +- *(llm)* add GitHub Copilot as LLM provider ([#1512](https://github.com/nearai/ironclaw/pull/1512)) +- *(workspace)* layered memory with sensitivity-based privacy redirect ([#1112](https://github.com/nearai/ironclaw/pull/1112)) +- *(webhooks)* add public webhook trigger endpoint for routines ([#736](https://github.com/nearai/ironclaw/pull/736)) +- *(llm)* Add OpenAI Codex (ChatGPT subscription) as LLM provider ([#1461](https://github.com/nearai/ironclaw/pull/1461)) +- *(web)* add light theme with dark/light/system toggle ([#1457](https://github.com/nearai/ironclaw/pull/1457)) +- *(agent)* activate stuck_threshold for time-based stuck job detection ([#1234](https://github.com/nearai/ironclaw/pull/1234)) +- chat onboarding and routine advisor ([#927](https://github.com/nearai/ironclaw/pull/927)) + +### Fixed + +- ensure LLM calls always end with user message (closes #763) ([#1259](https://github.com/nearai/ironclaw/pull/1259)) +- restore owner-scoped gateway startup ([#1625](https://github.com/nearai/ironclaw/pull/1625)) +- remove stale stream_token gate from channel-relay activation ([#1623](https://github.com/nearai/ironclaw/pull/1623)) +- *(agent)* case-insensitive channel match and user_id filter for event triggers ([#1211](https://github.com/nearai/ironclaw/pull/1211)) +- *(routines)* normalize status display across web and CLI ([#1469](https://github.com/nearai/ironclaw/pull/1469)) +- *(tunnel)* managed tunnels target wrong port and die from SIGPIPE ([#1093](https://github.com/nearai/ironclaw/pull/1093)) +- *(agent)* persist /model selection to .env, TOML, and DB ([#1581](https://github.com/nearai/ironclaw/pull/1581)) +- post-merge review sweep — 8 fixes across security, perf, and correctness ([#1550](https://github.com/nearai/ironclaw/pull/1550)) +- generate Mistral-compatible 9-char alphanumeric tool call IDs ([#1242](https://github.com/nearai/ironclaw/pull/1242)) +- *(mcp)* handle empty 202 notification acknowledgements ([#1539](https://github.com/nearai/ironclaw/pull/1539)) +- *(tests)* eliminate env mutex poison cascade ([#1558](https://github.com/nearai/ironclaw/pull/1558)) +- *(safety)* escape tool output XML content and remove misleading sanitized attr ([#1067](https://github.com/nearai/ironclaw/pull/1067)) +- *(oauth)* reject malformed ic2.* states in decode_hosted_oauth_state ([#1441](https://github.com/nearai/ironclaw/pull/1441)) ([#1454](https://github.com/nearai/ironclaw/pull/1454)) +- parameter coercion and validation for oneOf/anyOf/allOf schemas ([#1397](https://github.com/nearai/ironclaw/pull/1397)) +- persist startup-loaded MCP clients in ExtensionManager ([#1509](https://github.com/nearai/ironclaw/pull/1509)) +- *(deps)* patch rustls-webpki vulnerability (RUSTSEC-2026-0049) +- *(routines)* add missing extension_manager field in trigger_manual EngineContext +- *(ci)* serialize env-mutating OAuth wildcard tests with ENV_MUTEX ([#1280](https://github.com/nearai/ironclaw/pull/1280)) ([#1468](https://github.com/nearai/ironclaw/pull/1468)) +- *(setup)* remove redundant LLM config and API keys from bootstrap .env ([#1448](https://github.com/nearai/ironclaw/pull/1448)) +- resolve wasm broadcast merge conflicts with staging ([#395](https://github.com/nearai/ironclaw/pull/395)) ([#1460](https://github.com/nearai/ironclaw/pull/1460)) +- skip credential validation for Bedrock backend ([#1011](https://github.com/nearai/ironclaw/pull/1011)) +- register sandbox jobs in ContextManager for query tool visibility ([#1426](https://github.com/nearai/ironclaw/pull/1426)) +- prefer execution-local message routing metadata ([#1449](https://github.com/nearai/ironclaw/pull/1449)) +- *(security)* validate embedding base URLs to prevent SSRF ([#1221](https://github.com/nearai/ironclaw/pull/1221)) +- f32→f64 precision artifact in temperature causes provider 400 errors ([#1450](https://github.com/nearai/ironclaw/pull/1450)) +- *(routines)* surface errors when sandbox unavailable for full_job routines ([#769](https://github.com/nearai/ironclaw/pull/769)) +- restore libSQL vector search with dynamic dimensions ([#1393](https://github.com/nearai/ironclaw/pull/1393)) +- staging CI triage — consolidate retry parsing, fix flaky tests, add docs ([#1427](https://github.com/nearai/ironclaw/pull/1427)) + +### Other + +- Merge branch 'main' into staging-promote/455f543b-23329172268 +- Merge pull request #1655 from nearai/codex/fix-staging-promotion-1451-version-bumps +- Merge pull request #1499 from nearai/staging-promote/9603fefd-23364438978 +- Fix libsql prompt scope regressions ([#1651](https://github.com/nearai/ironclaw/pull/1651)) +- Normalize cron schedules on routine create ([#1648](https://github.com/nearai/ironclaw/pull/1648)) +- Fix MCP lifecycle trace user scope ([#1646](https://github.com/nearai/ironclaw/pull/1646)) +- Fix REPL single-message hang and cap CI test duration ([#1643](https://github.com/nearai/ironclaw/pull/1643)) +- extract AppEvent to crates/ironclaw_common ([#1615](https://github.com/nearai/ironclaw/pull/1615)) +- Fix hosted OAuth refresh via proxy ([#1602](https://github.com/nearai/ironclaw/pull/1602)) +- *(agent)* optimize approval thread resolution (UUID parsing + lock contention) ([#1592](https://github.com/nearai/ironclaw/pull/1592)) +- *(tools)* auto-compact WASM tool schemas, add descriptions, improve credential prompts ([#1525](https://github.com/nearai/ironclaw/pull/1525)) +- Default new lightweight routines to tools-enabled ([#1573](https://github.com/nearai/ironclaw/pull/1573)) +- Google OAuth URL broken when initiated from Telegram channel ([#1165](https://github.com/nearai/ironclaw/pull/1165)) +- add gitcgr code graph badge ([#1563](https://github.com/nearai/ironclaw/pull/1563)) +- Fix owner-scoped message routing fallbacks ([#1574](https://github.com/nearai/ironclaw/pull/1574)) +- *(tools)* remove unconditional params clone in shared execution (fix #893) ([#926](https://github.com/nearai/ironclaw/pull/926)) +- *(llm)* move transcription module into src/llm/ ([#1559](https://github.com/nearai/ironclaw/pull/1559)) +- *(agent)* avoid preview allocations for non-truncated strings (fix #894) ([#924](https://github.com/nearai/ironclaw/pull/924)) +- Expand AGENTS.md with coding agents guidance ([#1392](https://github.com/nearai/ironclaw/pull/1392)) +- Fix CI approval flows and stale fixtures ([#1478](https://github.com/nearai/ironclaw/pull/1478)) +- Use live owner tool scope for autonomous routines and jobs ([#1453](https://github.com/nearai/ironclaw/pull/1453)) +- use Arc in embedding cache to avoid clones on miss path ([#1438](https://github.com/nearai/ironclaw/pull/1438)) +- Add owner-scoped permissions for full-job routines ([#1440](https://github.com/nearai/ironclaw/pull/1440)) + ## [0.21.0](https://github.com/nearai/ironclaw/compare/v0.20.0...v0.21.0) - 2026-03-20 ### Added diff --git a/Cargo.lock b/Cargo.lock index 678054e4..581c75bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3390,7 +3390,7 @@ dependencies = [ [[package]] name = "ironclaw" -version = "0.21.0" +version = "0.22.0" dependencies = [ "aes-gcm", "aho-corasick", diff --git a/Cargo.toml b/Cargo.toml index e4f14108..8a77f5b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ exclude = [ [package] name = "ironclaw" -version = "0.21.0" +version = "0.22.0" edition = "2024" rust-version = "1.92" description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly" From f02345fd1f9140573d341cf9b4028d55eb021a1d Mon Sep 17 00:00:00 2001 From: Henry Park Date: Wed, 25 Mar 2026 17:58:51 -0700 Subject: [PATCH 09/13] fix: allow publishing ironclaw_common (#1657) --- crates/ironclaw_common/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ironclaw_common/Cargo.toml b/crates/ironclaw_common/Cargo.toml index 353ab747..6e7db5a4 100644 --- a/crates/ironclaw_common/Cargo.toml +++ b/crates/ironclaw_common/Cargo.toml @@ -8,7 +8,6 @@ authors = ["NEAR AI "] license = "MIT OR Apache-2.0" homepage = "https://github.com/nearai/ironclaw" repository = "https://github.com/nearai/ironclaw" -publish = false [package.metadata.dist] dist = false From ab67f028860094dc8086f4e9866ed0e8ff44b3cd Mon Sep 17 00:00:00 2001 From: Henry Park Date: Wed, 25 Mar 2026 18:21:17 -0700 Subject: [PATCH 10/13] fix: publish ironclaw_safety 0.2.0 (#1659) --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/ironclaw_safety/Cargo.toml | 3 +-- release-plz.toml | 5 ----- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 581c75bb..c3747590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3496,7 +3496,7 @@ dependencies = [ [[package]] name = "ironclaw_safety" -version = "0.1.0" +version = "0.2.0" dependencies = [ "aho-corasick", "regex", diff --git a/Cargo.toml b/Cargo.toml index 8a77f5b4..41895b16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ cron = "0.13" ironclaw_common = { path = "crates/ironclaw_common", version = "0.1.0" } # Safety/sanitization -ironclaw_safety = { path = "crates/ironclaw_safety", version = "0.1.0" } +ironclaw_safety = { path = "crates/ironclaw_safety", version = "0.2.0" } regex = "1" aho-corasick = "1" diff --git a/crates/ironclaw_safety/Cargo.toml b/crates/ironclaw_safety/Cargo.toml index d12aa909..38b8718a 100644 --- a/crates/ironclaw_safety/Cargo.toml +++ b/crates/ironclaw_safety/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironclaw_safety" -version = "0.1.0" +version = "0.2.0" edition = "2024" rust-version = "1.92" description = "Prompt injection defense, input validation, secret leak detection, and safety policy enforcement" @@ -8,7 +8,6 @@ authors = ["NEAR AI "] license = "MIT OR Apache-2.0" homepage = "https://github.com/nearai/ironclaw" repository = "https://github.com/nearai/ironclaw" -publish = false [package.metadata.dist] dist = false diff --git a/release-plz.toml b/release-plz.toml index b003952d..e8e0670f 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -1,7 +1,2 @@ [workspace] git_release_enable = false - -[[package]] -name = "ironclaw_safety" -publish = false -release = false From 6b8a38e147885aae6f8cfeb0627a79bec6eebb7c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:40:48 -0700 Subject: [PATCH 11/13] chore: update WASM artifact SHA256 checksums [skip ci] (#1663) Co-authored-by: github-actions[bot] --- registry/channels/feishu.json | 4 ++-- registry/tools/github.json | 4 ++-- registry/tools/gmail.json | 4 ++-- registry/tools/google-calendar.json | 4 ++-- registry/tools/google-docs.json | 4 ++-- registry/tools/google-drive.json | 4 ++-- registry/tools/google-sheets.json | 4 ++-- registry/tools/google-slides.json | 4 ++-- registry/tools/llm-context.json | 4 ++-- registry/tools/slack.json | 4 ++-- registry/tools/telegram.json | 4 ++-- registry/tools/web-search.json | 4 ++-- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/registry/channels/feishu.json b/registry/channels/feishu.json index 90619b9c..a7530943 100644 --- a/registry/channels/feishu.json +++ b/registry/channels/feishu.json @@ -19,8 +19,8 @@ }, "artifacts": { "wasm32-wasip2": { - "sha256": "52def36121a93cf0b06dd6d594dc9528745fae56ded7d632998700cc595be762", - "url": "https://github.com/nearai/ironclaw/releases/download/v0.21.0/channel-feishu-0.1.2-wasm32-wasip2.tar.gz" + "sha256": "a66ff0dafb67d2216d8161bb7e96e724a94acb0ab993b85d2782d30412f8fe94", + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/channel-feishu-0.1.3-wasm32-wasip2.tar.gz" } }, "auth_summary": { diff --git a/registry/tools/github.json b/registry/tools/github.json index aa89794e..bb351259 100644 --- a/registry/tools/github.json +++ b/registry/tools/github.json @@ -19,8 +19,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-github-0.2.1-wasm32-wasip2.tar.gz", - "sha256": "92c530b3ad172e2372d819744b5233f1d8f65768e26eb5a6c213eba3ce1de758" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-github-0.2.2-wasm32-wasip2.tar.gz", + "sha256": "70b55af593193d8fa495c0f702ea23284d83a624124f8a5f7564916ec5032c3f" } }, "auth_summary": { diff --git a/registry/tools/gmail.json b/registry/tools/gmail.json index 90573115..c4772129 100644 --- a/registry/tools/gmail.json +++ b/registry/tools/gmail.json @@ -18,8 +18,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/gmail-0.2.0-wasm32-wasip2.tar.gz", - "sha256": "ee9574e02e92bc1d481f1310eb88afd99ee52bf6971074ab33bd76bf99b34b1d" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-gmail-0.2.1-wasm32-wasip2.tar.gz", + "sha256": "79025b40ee70ce1120acc4320bae50da095d7afb0ef67bd56d99b064b72ea779" } }, "auth_summary": { diff --git a/registry/tools/google-calendar.json b/registry/tools/google-calendar.json index a9cdc539..73065a67 100644 --- a/registry/tools/google-calendar.json +++ b/registry/tools/google-calendar.json @@ -18,8 +18,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-calendar-0.2.0-wasm32-wasip2.tar.gz", - "sha256": "2fa47150ea222e787c122182ad6f4dfa2ffaf5fe490d05e8de887a76445f8d2d" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-google-calendar-0.2.1-wasm32-wasip2.tar.gz", + "sha256": "86bcc075010b08f5ab2f98f504cec1c6c9e0ca144857d185cbecf72a11f504bf" } }, "auth_summary": { diff --git a/registry/tools/google-docs.json b/registry/tools/google-docs.json index e3206b5e..02cc94fe 100644 --- a/registry/tools/google-docs.json +++ b/registry/tools/google-docs.json @@ -18,8 +18,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-docs-0.2.0-wasm32-wasip2.tar.gz", - "sha256": "40e134a1c1564f832ca861c3396895d4e33ec67b99313fc1f97baf8d971423a9" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-google-docs-0.2.1-wasm32-wasip2.tar.gz", + "sha256": "39d476029764949498a53a6a223f9952b5f4df151be7b8b19bf3fe4d401a57cd" } }, "auth_summary": { diff --git a/registry/tools/google-drive.json b/registry/tools/google-drive.json index 3de90ed1..719690f7 100644 --- a/registry/tools/google-drive.json +++ b/registry/tools/google-drive.json @@ -18,8 +18,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-drive-0.2.0-wasm32-wasip2.tar.gz", - "sha256": "002a341a1d58125563a7c69561b26fbc2629b04ea723cade744102bdc0fbb71f" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-google-drive-0.2.1-wasm32-wasip2.tar.gz", + "sha256": "6e9a700fab93865c852af718666af64c5b534ad6a419fb4b736e07740188f494" } }, "auth_summary": { diff --git a/registry/tools/google-sheets.json b/registry/tools/google-sheets.json index 4e5722e9..09aae574 100644 --- a/registry/tools/google-sheets.json +++ b/registry/tools/google-sheets.json @@ -18,8 +18,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-sheets-0.2.0-wasm32-wasip2.tar.gz", - "sha256": "8aa2c9d52f033edea3a6c2311b0ec694ccb6d0a54ef07e94d72bf8be1ce8009a" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-google-sheets-0.2.1-wasm32-wasip2.tar.gz", + "sha256": "1f8c381799a916be83263cac9d497d52946e21b1b588592a3a42ca94a73b7051" } }, "auth_summary": { diff --git a/registry/tools/google-slides.json b/registry/tools/google-slides.json index bbd4f8c0..64bc0e45 100644 --- a/registry/tools/google-slides.json +++ b/registry/tools/google-slides.json @@ -17,8 +17,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-slides-0.2.0-wasm32-wasip2.tar.gz", - "sha256": "e931a97d4fd0b0b938e464dc7c7f2be6ea6b4d1508f5ea3cd931d44db23f05f5" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-google-slides-0.2.1-wasm32-wasip2.tar.gz", + "sha256": "e2528be5da02f1b8cfc8ee9b0cdd849516c53d412e2f75c6175b3bded7f512cb" } }, "auth_summary": { diff --git a/registry/tools/llm-context.json b/registry/tools/llm-context.json index acea3301..422f2e18 100644 --- a/registry/tools/llm-context.json +++ b/registry/tools/llm-context.json @@ -21,8 +21,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-llm-context-0.1.0-wasm32-wasip2.tar.gz", - "sha256": "d9ced2b1226b879135891e0ee40e072c7c95412e1b2462925a23853e1f92497e" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-llm-context-0.1.1-wasm32-wasip2.tar.gz", + "sha256": "9b19e2fd05dbbbe3c8bd55309a91db09124e8415eb0f767828b6e10b55771e63" } }, "auth_summary": { diff --git a/registry/tools/slack.json b/registry/tools/slack.json index 27bce7c4..236062a4 100644 --- a/registry/tools/slack.json +++ b/registry/tools/slack.json @@ -17,8 +17,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-slack-0.2.0-wasm32-wasip2.tar.gz", - "sha256": "ccfb0415d7a04f9497726c712d15216de36e86f498b849101283c017f5ab4efb" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-slack-0.2.1-wasm32-wasip2.tar.gz", + "sha256": "927519e5b7734beeb022d3b8bbd152e0e6b9f67c9452a8ad47809d3c4221a137" } }, "auth_summary": { diff --git a/registry/tools/telegram.json b/registry/tools/telegram.json index d3370940..e684ca94 100644 --- a/registry/tools/telegram.json +++ b/registry/tools/telegram.json @@ -18,8 +18,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-telegram-0.2.0-wasm32-wasip2.tar.gz", - "sha256": "c17065ca41fae5f2a7c43b36144686718cd310a2f22442313bb1aa82bbad0ae4" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-telegram-0.2.1-wasm32-wasip2.tar.gz", + "sha256": "1e57d0755fc9c7b3ec013d079f30168898b484a6919f9edd105f0cd80131c1cd" } }, "auth_summary": { diff --git a/registry/tools/web-search.json b/registry/tools/web-search.json index 47f4d699..014466af 100644 --- a/registry/tools/web-search.json +++ b/registry/tools/web-search.json @@ -18,8 +18,8 @@ }, "artifacts": { "wasm32-wasip2": { - "url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-web-search-0.2.1-wasm32-wasip2.tar.gz", - "sha256": "bad275ca4ec314adea5241d6b92c44ccf9cebcbca8e30ba2493cc0bcb4b57218" + "url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.22.0/tool-web-search-0.2.2-wasm32-wasip2.tar.gz", + "sha256": "47382b50c1ea7525b20d59dc02fab04e336d018665826c2f24710bdf460779ae" } }, "auth_summary": { From b3fbef5287c84d0388fcdab1713c69d5ef62104a Mon Sep 17 00:00:00 2001 From: "firat.sertgoz" Date: Thu, 26 Mar 2026 09:37:59 +0300 Subject: [PATCH 12/13] fix(llm): filter XML tool-call recovery by context (#1641) * fix(llm): filter XML tool-call recovery by context * fix: address review comments on PR #1641 --- src/llm/reasoning.rs | 98 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/src/llm/reasoning.rs b/src/llm/reasoning.rs index cf3692e9..473eb16d 100644 --- a/src/llm/reasoning.rs +++ b/src/llm/reasoning.rs @@ -1345,6 +1345,49 @@ fn is_inside_code(pos: usize, regions: &[CodeRegion]) -> bool { regions.iter().any(|r| pos >= r.start && pos < r.end) } +/// Check whether a byte range overlaps any code region. +fn overlaps_code_region(start: usize, end: usize, regions: &[CodeRegion]) -> bool { + regions.iter().any(|r| start < r.end && end > r.start) +} + +/// Return the byte bounds of the line containing `pos`, excluding the trailing newline. +fn line_bounds(text: &str, pos: usize) -> (usize, usize) { + let start = text[..pos].rfind('\n').map_or(0, |idx| idx + 1); + let end = text[pos..].find('\n').map_or(text.len(), |idx| pos + idx); + (start, end) +} + +/// Only recover XML-style tool calls when they are isolated content outside +/// markdown code and quote contexts. This avoids converting code examples or +/// quoted snippets into executable tool calls. +fn is_recoverable_tool_call_segment( + text: &str, + start: usize, + end: usize, + code_regions: &[CodeRegion], +) -> bool { + if overlaps_code_region(start, end, code_regions) { + return false; + } + + let (first_line_start, first_line_end) = line_bounds(text, start); + let first_line = &text[first_line_start..first_line_end]; + + if first_line.trim_start().starts_with('>') { + return false; + } + + let (_, last_line_end) = line_bounds(text, end.saturating_sub(1)); + let first_line_prefix = &text[first_line_start..start]; + let last_line_suffix = &text[end..last_line_end]; + + if !first_line_prefix.trim().is_empty() || !last_line_suffix.trim().is_empty() { + return false; + } + + true +} + /// Clean up LLM response by stripping model-internal tags and reasoning patterns. /// /// Some models (GLM-4.7, etc.) emit XML-tagged internal state like @@ -1364,6 +1407,7 @@ fn recover_tool_calls_from_content( ) -> Vec { let tool_names: std::collections::HashSet<&str> = available_tools.iter().map(|t| t.name.as_str()).collect(); + let code_regions = find_code_regions(content); let mut calls = Vec::new(); for (open, close) in &[ @@ -1372,15 +1416,23 @@ fn recover_tool_calls_from_content( ("", ""), ("<|function_call|>", "<|/function_call|>"), ] { - let mut remaining = content; - while let Some(start) = remaining.find(open) { + let mut search_from = 0; + while let Some(offset) = content[search_from..].find(open) { + let start = search_from + offset; let inner_start = start + open.len(); - let after = &remaining[inner_start..]; - let Some(end) = after.find(close) else { + let after = &content[inner_start..]; + let Some(end_offset) = after.find(close) else { break; }; - let inner = after[..end].trim(); - remaining = &after[end + close.len()..]; + let end = inner_start + end_offset; + let segment_end = end + close.len(); + search_from = segment_end; + + if !is_recoverable_tool_call_segment(content, start, segment_end, &code_regions) { + continue; + } + + let inner = content[inner_start..end].trim(); if inner.is_empty() { continue; @@ -2313,6 +2365,40 @@ That's my plan."#; assert_eq!(calls[0].name, "tool_list"); } + #[test] + fn test_recover_tool_call_in_fenced_code_block_ignored() { + let tools = make_tools(&["tool_list"]); + let content = "Here is the XML format:\n\n```xml\ntool_list\n```"; + let calls = recover_tool_calls_from_content(content, &tools); + assert!(calls.is_empty()); + } + + #[test] + fn test_recover_tool_call_in_inline_code_ignored() { + let tools = make_tools(&["tool_list"]); + let content = "Use `tool_list` to illustrate the syntax."; + let calls = recover_tool_calls_from_content(content, &tools); + assert!(calls.is_empty()); + } + + #[test] + fn test_recover_tool_call_in_blockquote_ignored() { + let tools = make_tools(&["tool_list"]); + let content = "The page replied:\n> tool_list"; + let calls = recover_tool_calls_from_content(content, &tools); + assert!(calls.is_empty()); + } + + #[test] + fn test_recover_multiline_json_tool_call_on_own_line() { + let tools = make_tools(&["memory_search"]); + let content = "Let me check.\n\n\n{\"name\": \"memory_search\", \"arguments\": {\"query\": \"test\"}}\n\n\nDone."; + let calls = recover_tool_calls_from_content(content, &tools); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].name, "memory_search"); + assert_eq!(calls[0].arguments, serde_json::json!({"query": "test"})); + } + // ---- System prompt building tests (issue #565) ---- fn make_test_reasoning() -> Reasoning { From ed4d92932ac5d2d9123a8448aac4627bb8bb2d7c Mon Sep 17 00:00:00 2001 From: rajulbhatnagar Date: Thu, 26 Mar 2026 00:02:41 -0700 Subject: [PATCH 13/13] fix(agent): discard truncated tool calls when finish_reason == Length (#1631) (#1632) --- src/agent/agentic_loop.rs | 126 +++++++++++++++++++++++++++++++++++++- src/agent/dispatcher.rs | 2 + src/llm/mod.rs | 3 +- src/llm/reasoning.rs | 110 ++++++++++++++++++++++++++++++++- src/worker/job.rs | 2 + 5 files changed, 239 insertions(+), 4 deletions(-) diff --git a/src/agent/agentic_loop.rs b/src/agent/agentic_loop.rs index e61856dc..27c2ab72 100644 --- a/src/agent/agentic_loop.rs +++ b/src/agent/agentic_loop.rs @@ -10,7 +10,7 @@ use std::borrow::Cow; use crate::agent::session::PendingApproval; use crate::error::Error; -use crate::llm::{ChatMessage, Reasoning, ReasoningContext, RespondResult}; +use crate::llm::{ChatMessage, FinishReason, Reasoning, ReasoningContext, RespondResult}; /// Signal from the delegate indicating how the loop should proceed. pub enum LoopSignal { @@ -134,6 +134,9 @@ pub async fn run_agentic_loop( config: &AgenticLoopConfig, ) -> Result { let mut consecutive_tool_intent_nudges: u32 = 0; + // Accumulates across all iterations (not reset by text responses) so + // non-consecutive truncations still escalate to force_text. + let mut truncation_count: u32 = 0; for iteration in 1..=config.max_iterations { // Check for external signals (stop, cancellation, user messages) @@ -215,7 +218,35 @@ pub async fn run_agentic_loop( tool_calls, content, } => { + // If the response was truncated, tool call parameters are likely + // incomplete. Discard them and tell the LLM to try a different + // approach rather than executing malformed tool calls. + if output.finish_reason == FinishReason::Length { + truncation_count += 1; + let names: Vec<&str> = tool_calls.iter().map(|tc| tc.name.as_str()).collect(); + tracing::warn!( + iteration, + tools = ?names, + truncation_count, + "Discarding truncated tool calls (finish_reason=Length)" + ); + if let Some(ref text) = content { + reason_ctx.messages.push(ChatMessage::assistant(text)); + } + reason_ctx + .messages + .push(ChatMessage::user(crate::llm::TRUNCATED_TOOL_CALL_NOTICE)); + // After repeated truncations, force text-only mode so the LLM + // stops attempting tool calls it can't fit in the output budget. + if truncation_count >= 3 { + reason_ctx.force_text = true; + } + delegate.after_iteration(iteration).await; + continue; + } + consecutive_tool_intent_nudges = 0; + truncation_count = 0; if let Some(outcome) = delegate .execute_tool_calls(tool_calls, content, reason_ctx) @@ -271,6 +302,7 @@ mod tests { RespondOutput { result: RespondResult::Text(text.to_string()), usage: zero_usage(), + finish_reason: FinishReason::Stop, } } @@ -281,6 +313,7 @@ mod tests { content: None, }, usage: zero_usage(), + finish_reason: FinishReason::ToolUse, } } @@ -622,4 +655,95 @@ mod tests { let result = truncate_for_preview("café", 4); assert_eq!(result, "caf..."); } + + #[tokio::test] + async fn test_truncated_tool_calls_discarded_on_length() { + let truncated_tool_call = ToolCall { + id: "call_1".to_string(), + name: "memory_write".to_string(), + arguments: serde_json::json!({}), // empty — truncated + reasoning: None, + }; + let truncated_output = RespondOutput { + result: RespondResult::ToolCalls { + tool_calls: vec![truncated_tool_call], + content: Some("I'll write the report.".to_string()), + }, + usage: zero_usage(), + finish_reason: FinishReason::Length, // response was truncated + }; + let delegate = MockDelegate::new(vec![truncated_output, text_output("Summarized it.")]); + let reasoning = stub_reasoning(); + let mut ctx = ReasoningContext::new(); + let config = AgenticLoopConfig { + max_iterations: 5, + ..Default::default() + }; + + let outcome = run_agentic_loop(&delegate, &reasoning, &mut ctx, &config) + .await + .unwrap(); + + // Tool calls should NOT have been executed + assert_eq!(delegate.tool_exec_count.load(Ordering::SeqCst), 0); + // The loop should have continued and returned the text response + assert!(matches!(outcome, LoopOutcome::Response(ref t) if t == "Summarized it.")); + // A truncation notice should have been injected into context + assert!( + ctx.messages + .iter() + .any(|m| m.role == crate::llm::Role::User && m.content.contains("truncated")), + "Should inject truncation notice into context" + ); + // The partial assistant content should have been preserved + assert!( + ctx.messages + .iter() + .any(|m| m.role == crate::llm::Role::Assistant + && m.content.contains("write the report")), + "Should preserve partial assistant content" + ); + } + + #[tokio::test] + async fn test_repeated_truncations_force_text_mode() { + let make_truncated = || RespondOutput { + result: RespondResult::ToolCalls { + tool_calls: vec![ToolCall { + id: "call_1".to_string(), + name: "memory_write".to_string(), + arguments: serde_json::json!({}), + reasoning: None, + }], + content: None, + }, + usage: zero_usage(), + finish_reason: FinishReason::Length, + }; + // Three truncated responses, then a text response + let delegate = MockDelegate::new(vec![ + make_truncated(), + make_truncated(), + make_truncated(), + text_output("Gave up on tool calls."), + ]); + let reasoning = stub_reasoning(); + let mut ctx = ReasoningContext::new(); + let config = AgenticLoopConfig { + max_iterations: 5, + ..Default::default() + }; + + let outcome = run_agentic_loop(&delegate, &reasoning, &mut ctx, &config) + .await + .unwrap(); + + assert!(matches!(outcome, LoopOutcome::Response(_))); + assert_eq!(delegate.tool_exec_count.load(Ordering::SeqCst), 0); + // After 3 truncations, force_text should be set + assert!( + ctx.force_text, + "Should escalate to force_text after repeated truncations" + ); + } } diff --git a/src/agent/dispatcher.rs b/src/agent/dispatcher.rs index 96bca197..a5f9cd6f 100644 --- a/src/agent/dispatcher.rs +++ b/src/agent/dispatcher.rs @@ -306,6 +306,8 @@ impl<'a> LoopDelegate for ChatDelegate<'a> { // Update context for this iteration reason_ctx.available_tools = tool_defs; + // Preserve force_text if already set (e.g. by truncation escalation). + let force_text = force_text || reason_ctx.force_text; reason_ctx.system_prompt = Some(if force_text { self.cached_prompt_no_tools.clone() } else { diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 308b3983..d681547d 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -63,7 +63,8 @@ pub use provider::{ }; pub use reasoning::{ ActionPlan, Reasoning, ReasoningContext, RespondOutput, RespondResult, SILENT_REPLY_TOKEN, - TOOL_INTENT_NUDGE, TokenUsage, ToolSelection, is_silent_reply, llm_signals_tool_intent, + TOOL_INTENT_NUDGE, TRUNCATED_TOOL_CALL_NOTICE, TokenUsage, ToolSelection, is_silent_reply, + llm_signals_tool_intent, }; pub use recording::RecordingLlm; pub use registry::{ProviderDefinition, ProviderProtocol, ProviderRegistry}; diff --git a/src/llm/reasoning.rs b/src/llm/reasoning.rs index 473eb16d..6e078ac7 100644 --- a/src/llm/reasoning.rs +++ b/src/llm/reasoning.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; use crate::llm::error::LlmError; use crate::llm::{ - ChatMessage, CompletionRequest, LlmProvider, Role, ToolCall, ToolCompletionRequest, - ToolDefinition, + ChatMessage, CompletionRequest, FinishReason, LlmProvider, Role, ToolCall, + ToolCompletionRequest, ToolDefinition, }; /// Token the agent returns when it has nothing to say (e.g. in group chats). @@ -23,6 +23,13 @@ You said you would perform an action, but you did not include any tool calls.\n\ Do NOT describe what you intend to do — actually call the tool now.\n\ Use the tool_calls mechanism to invoke the appropriate tool."; +/// Notice injected when the LLM's response was truncated mid-tool-call, +/// causing incomplete parameters. Tells the LLM to try a different approach. +pub const TRUNCATED_TOOL_CALL_NOTICE: &str = "\ +Your previous response was truncated while generating tool call parameters. \ +The tool calls were discarded. Please try a different approach — \ +summarize or transform the data instead of echoing it verbatim in a tool call."; + /// Seed value used as the second argument to `generate_tool_call_id` when /// recovering tool calls from malformed LLM text responses. This must differ /// from the `0` seed used in `rig_adapter::normalized_tool_call_id` to avoid @@ -194,6 +201,8 @@ pub struct ReasoningContext { pub metadata: std::collections::HashMap, /// When true, force a text-only response (ignore available tools). /// Used by the agentic loop to guarantee termination near the iteration limit. + /// Sticky: once set, never cleared within a loop invocation. Callers must + /// create a fresh `ReasoningContext` per `run_agentic_loop()` call. pub force_text: bool, /// Pre-built system prompt. When set, `respond_with_tools` uses this directly /// instead of calling `build_system_prompt_with_tools`. Allows callers to build @@ -349,6 +358,7 @@ pub enum RespondResult { pub struct RespondOutput { pub result: RespondResult, pub usage: TokenUsage, + pub finish_reason: FinishReason, } /// Reasoning engine for the agent. @@ -530,6 +540,17 @@ impl Reasoning { let response = self.llm.complete_with_tools(request).await?; + // If the response was truncated, tool call parameters are likely incomplete. + // Return empty so the caller can fall through to respond_with_tools() which + // has a larger output token budget. + if response.finish_reason == FinishReason::Length { + tracing::warn!( + "select_tools response truncated (finish_reason=Length), \ + discarding potentially incomplete tool selections" + ); + return Ok(vec![]); + } + let shared_reasoning = response .content .map(|c| { @@ -722,6 +743,7 @@ Respond in JSON format: content: narrative, }, usage, + finish_reason: response.finish_reason, }); } @@ -749,6 +771,7 @@ Respond in JSON format: }, }, usage, + finish_reason: response.finish_reason, }); } @@ -774,6 +797,7 @@ Respond in JSON format: Ok(RespondOutput { result: RespondResult::Text(final_text), usage, + finish_reason: response.finish_reason, }) } else { // No tools, use simple completion @@ -805,6 +829,7 @@ Respond in JSON format: cache_read_input_tokens: response.cache_read_input_tokens, cache_creation_input_tokens: response.cache_creation_input_tokens, }, + finish_reason: response.finish_reason, }) } } @@ -3315,4 +3340,85 @@ That's my plan."#; let cleaned = clean_response(&pre_truncated); assert!(cleaned.trim().is_empty()); } + + // ---- select_tools truncation guard ---- + + /// Mock provider that returns tool calls with a configurable finish_reason. + struct TruncatingLlm { + finish_reason: crate::llm::FinishReason, + } + + #[async_trait::async_trait] + impl crate::llm::LlmProvider for TruncatingLlm { + fn model_name(&self) -> &str { + "truncating-stub" + } + fn cost_per_token(&self) -> (rust_decimal::Decimal, rust_decimal::Decimal) { + (rust_decimal::Decimal::ZERO, rust_decimal::Decimal::ZERO) + } + async fn complete( + &self, + _request: crate::llm::CompletionRequest, + ) -> Result { + unimplemented!() + } + async fn complete_with_tools( + &self, + _request: crate::llm::ToolCompletionRequest, + ) -> Result { + Ok(crate::llm::ToolCompletionResponse { + content: Some("I'll write the report.".to_string()), + tool_calls: vec![ToolCall { + id: "call_1".to_string(), + name: "memory_write".to_string(), + arguments: serde_json::json!({}), + reasoning: None, + }], + input_tokens: 5000, + output_tokens: 1024, + finish_reason: self.finish_reason, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }) + } + } + + #[tokio::test] + async fn test_select_tools_returns_empty_on_truncation() { + let llm = Arc::new(TruncatingLlm { + finish_reason: FinishReason::Length, + }); + let reasoning = Reasoning::new(llm); + let mut ctx = ReasoningContext::new().with_message(ChatMessage::user("Write a report")); + ctx.available_tools.push(ToolDefinition { + name: "memory_write".to_string(), + description: "Write to memory".to_string(), + parameters: serde_json::json!({"type": "object"}), + }); + + let selections = reasoning.select_tools(&ctx).await.unwrap(); + assert!( + selections.is_empty(), + "Truncated tool selections should be discarded (got {} selections)", + selections.len() + ); + } + + #[tokio::test] + async fn test_select_tools_returns_selections_when_not_truncated() { + let llm = Arc::new(TruncatingLlm { + finish_reason: FinishReason::ToolUse, + }); + let reasoning = Reasoning::new(llm); + let mut ctx = ReasoningContext::new().with_message(ChatMessage::user("Write a report")); + ctx.available_tools.push(ToolDefinition { + name: "memory_write".to_string(), + description: "Write to memory".to_string(), + parameters: serde_json::json!({"type": "object"}), + }); + + let selections = reasoning.select_tools(&ctx).await.unwrap(); + assert_eq!(selections.len(), 1); + assert_eq!(selections[0].tool_name, "memory_write"); + } } diff --git a/src/worker/job.rs b/src/worker/job.rs index 671b8864..f74d4ec8 100644 --- a/src/worker/job.rs +++ b/src/worker/job.rs @@ -1158,6 +1158,7 @@ impl<'a> JobDelegate<'a> { Ok(crate::llm::RespondOutput { result: RespondResult::Text(String::new()), usage: crate::llm::TokenUsage::default(), + finish_reason: crate::llm::FinishReason::Stop, }) } } @@ -1283,6 +1284,7 @@ impl<'a> LoopDelegate for JobDelegate<'a> { content: reasoning_text, }, usage: crate::llm::TokenUsage::default(), + finish_reason: crate::llm::FinishReason::ToolUse, }); } Ok(_) => {} // empty selections, fall through