* fix(web): prevent browser crash from timer leaks, DOM growth, SSE buffer (#2406)
Extended sessions with heavy bot interactions caused Chrome's "Pages
Unresponsive" dialog due to accumulated browser resources that were
never cleaned up.
Fixes:
- Add cleanupConnectionState() to clear leaked setInterval/setTimeout
timers on SSE reconnect, tab visibility change, and page unload
- Cap DOM at 200 message nodes via pruneOldMessages() with streaming-
aware pruning (skips data-streaming elements, called at turn
boundaries and after loadHistory)
- Cap jobEvents Map at 50 entries with LRU eviction (excludes current
job from eviction scan)
- Increase SSE broadcast buffer from 256 to 1024 (configurable via
SSE_BROADCAST_BUFFER env var, with zero-guard to prevent panic)
Closes#2406
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): address PR #2433 review — move SSE buffer to GatewayConfig, fix E2E timer test
Move SSE_BROADCAST_BUFFER env var from direct std::env::var() in sse.rs
to GatewayConfig in config/channels.rs, following the convention that all
gateway env vars flow through structured config. Add MAX_BROADCAST_BUFFER
(65,536) clamp to prevent OOM from misconfiguration.
Fix E2E timer leak test to install setInterval monkey-patch via
page.add_init_script() before navigation so initialization timers are
tracked. Add test_dom_resource_limits.py to E2E CLAUDE.md scenario table.
Add unit test for buffer config parsing, zero-rejection, and clamp.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): address review — clean gatewayStatusInterval, prune user msgs, use constants
- Add gatewayStatusInterval to cleanupConnectionState() so it is cleared
on reconnect/tab-hide/unload; add guard in startGatewayStatusPolling()
to prevent double-start; restart polling on tab visibility restore
- Call pruneOldMessages() after addMessage('user', ...) in sendMessage()
so DOM stays bounded even during rapid user input
- Replace hardcoded broadcast_buffer: 1024 with DEFAULT_BROADCAST_BUFFER
in all test construction sites (5 occurrences across 4 files)
- Document in from_sender() doc comment why broadcast_buffer is absent
- Tighten E2E timer leak assertion from baseline+1 to baseline
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): address PR #2441 review — prune/timer/assert/doc fixes
- Remove pruneOldMessages() from loadHistory() pagination path to avoid
immediately evicting just-prepended older messages
- Move MAX_DOM_MESSAGES constant to top-level constants block
- Add _loadThreadsTimer to cleanupConnectionState() for consistency
- Add assert!(broadcast_buffer > 0) to SseManager constructor with
panic doc (tokio broadcast channel requires capacity > 0)
- Use Set-based interval tracking in E2E test to prevent counter
underflow from double-clear
- Update CLAUDE.md broadcast buffer docs (256 → 1024, SSE_BROADCAST_BUFFER)
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): remove assert! from SseManager to pass no-panics CI check
Replace assert!(broadcast_buffer > 0) with a doc comment noting the
precondition. GatewayConfig already rejects 0 at the config layer.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): address ilblackdragon review — correctness, e2e tests, docs (#2406)
Correctness:
- pruneOldMessages: clean up orphaned leading time-separators after pruning
- jobEvents LRU: replace O(n) scan with O(1) Map insertion-order eviction
- Document degenerate all-streaming under-prune case
Playwright e2e tests:
- Tab hide/restore: no duplicate gateway status polling intervals
- DOM cap + streaming: 260 elements prune to ≤200, streaming preserved, no orphan separators
- jobEvents bounded: 60 jobs stay capped at ≤50 via LRU eviction
- Fix assertion selector to match pruneOldMessages superset, tighten lower bound
Rust:
- Unit test: SseManager buffer size parameter actually controls lag behavior
- Document MAX_BROADCAST_BUFFER memory impact (65K×100×200B ≈ 1.3 GB)
- Move "capacity baked into tx" comment from from_sender to rebuild_state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): protect currentJobId from LRU eviction in jobEvents map (#2441)
The O(1) LRU eviction skips the job that just received an event (moved
to end via delete+set), but did not protect the job the user is actively
viewing in the detail panel (currentJobId). If the user views a quiet
job while 50+ other jobs fire events, the viewed job's events would be
evicted and the activity tab would appear empty.
Add a currentJobId guard to the eviction loop and a Playwright e2e test
that verifies the actively-viewed job survives LRU pressure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(e2e): add real-flow Playwright tests for DOM resource limits (#2406)
Add 4 E2E tests that exercise pruning and timer cleanup through actual
UI interactions (mock LLM round-trips, real SSE reconnects) instead of
page.evaluate() injection. Also fix the existing timer leak test which
failed due to execution context destruction from add_init_script.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: serrrfirat <f@nuff.tech>
ThreadConfig::default() had max_consecutive_errors: None, which
serializes to null. The Python orchestrator's config.get(..., 5)
returns None (not 5) when the key is present with a null value, so
the guard `consecutive_action_errors >= max_consecutive_errors + 2`
crashed with TypeError on the first action error.
Fix both sides: default to Some(5) in Rust so the happy path sends
a real int, and treat None as "no limit" in Python so callers can
explicitly disable the guard without blowing up arithmetic.
Store a reqwest::Client in CustomTunnel and reuse it across health
check calls instead of creating a new client on every invocation.
This avoids repeated TLS/connection-pool setup overhead.
CustomTunnel::new() now returns Result<Self> so the client builder
error is propagated rather than silently falling back.
Co-authored-by: willamhou <willamhou@ceresman.com>
* fix: more strict check for registry to avoid false positives
By default ironclaw gets installed in ~/.cargo/bin/ironclaw, if
the user happens to try to compile anything (e.g. their own tool)
then the ~/.cargo/registry folder gets created, which makes
ironclaw think that it found an (empty) registry, and thus stops
being able to do list or install tools from its internal registry
* fix: comment
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Guillermo Alejandro Gallardo Diez <gagdiez@iR2.local>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fix(ux): actionable error messages and improved CLI help for issue #1852
AuthFailed errors now include provider-specific guidance (which env var
to set, relevant URLs, and how to run `ironclaw onboard --step provider`).
CLI help text improved across top-level, onboard, models, config, and
doctor commands to help new users discover the setup wizard and provider
configuration commands. `models set-provider` now warns when an API key
is missing after switching providers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(llm): snapshot coverage for rendered AuthFailed messages per provider
Addresses PR #2315 review note: auth error text is now policy-bearing
product guidance and warrants explicit coverage so future edits are
deliberate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): use secrets-aware optional_env for API key check
Addresses PR #2315 review: API key warning now checks the secrets
store overlay via optional_env() instead of raw std::env::var(),
preventing false "API key required" warnings for users who stored
keys via `ironclaw secrets`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(security): harden approval thread safety (TOCTOU + error handling)
Consolidates two security fixes for the approval processing flow in
thread_ops.rs:
**TOCTOU race (#1486):** Hold session lock for the entire take-verify
sequence in process_approval() so pending approval cannot be lost if a
concurrent operation modifies the thread between take and restore.
Previously, the lock was dropped after take_pending_approval() and
re-acquired for request_id verification, creating a window where the
approval could be permanently lost.
**Silent error fallback (#1487):** Replace 10 silent `if let Some(thread)`
patterns with explicit `match` arms. Critical paths (state transitions,
deferred approval setup) return errors when threads disappear. Non-critical
paths (tool result recording, auth mode, rejection) log debug messages but
continue.
Regression tests:
- test_approval_request_id_mismatch_restores_pending
- test_approval_on_missing_thread_should_error
Supersedes #1591 (branch had no merge base with current staging).
Closes#1486, Closes#1487https://claude.ai/code/session_01X86EZxqXEFiU9VetyhPKjM
* fix(security): prevent orphaned SSE events for dead threads
Address review feedback:
- handle_auth_intercept: return early when thread is gone instead
of emitting auth-required SSE to a dead thread
- process_auth_token: skip emit_auth_required_status when thread
disappeared (both Ok retry and Err retry paths)
Clients will no longer see auth prompts that can never resolve
when the underlying thread has been deleted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(web): add admin management panel
* fix(web): address admin panel review findings
* fix(web): address remaining admin review feedback
* refactor(web): type admin api responses
* fix(db): aggregate admin usage summary in sql
* Add audit logging for admin privileged state-changes
Add structured tracing (warn-level) to suspend, activate, delete, and
update handlers so that privileged admin actions are recorded with the
acting admin's user_id, the action performed, and the target user.
Addresses security assessment item #1 from PR review.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix PairingStore::new() call in test after staging merge
Use PairingStore::new_noop() since the test doesn't need a real DB.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(admin): address PR #1963 review feedback
- Fix total_jobs semantics: query agent_jobs directly instead of
counting via LEFT JOIN on llm_calls (which missed jobs without
LLM calls). Fixed in both libSQL and PostgreSQL backends.
- Fix showConfirmModal XSS: escape message parameter internally
instead of relying on callers to sanitize.
- Add explicit ::numeric cast to PG COALESCE(SUM(cost), 0) to
prevent integer type inference.
- Use info! instead of warn! for successful admin audit events
(update, suspend, activate, delete) — warn implies anomaly.
- Add missing index on llm_calls.created_at for both PG (V21
migration) and libSQL (incremental migration 21).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address remaining admin panel review follow-ups
* fix: address review comments — query consolidation, CSP, docs, security notes
- Collapse 4 redundant llm_calls subqueries into single subquery (libsql + pg)
- Add WARNING to V21 migration about table lock risk with CONCURRENTLY note
- Add performance doc comments on admin_usage_summary full-table scan
- Add CSP and noindex meta tags to admin.html
- Add JSDoc for showConfirmModal documenting auto-escaping
- Add sessionStorage threat model security comment
- Add serde(flatten) collision risk doc on AdminUserDetailResponse
- Add TODO(#1968) for inline styles migration to CSS custom properties
- Add PG parity test stub for admin_usage_summary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: renumber migration from V21/23 to V24 to avoid conflicts with staging
Staging added V21 (backfill_conversation_source_channel), V22
(sandbox_restart_params), and V23 (list_workspace_files_escape_like).
Renumber our llm_calls_created_at_index migration to V24 in both PG
and libSQL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback — CSP, logging, validation, dispatch-exempt, tests
- Remove 'unsafe-inline' from script-src CSP; move CSP to HTTP response header
- Change audit tracing::info! to tracing::debug! (TUI corruption)
- Add dispatch-exempt annotation on usage_summary_handler
- Add server-side input validation on users_create_handler (name length, email, role)
- Rename detailRowHtml to detailRowRawHtml with XSS safety comment
- Add real PG integration test for admin_usage_summary with non-zero data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): skip test-only directories in no-panics check
Files under `src/**/tests/*.rs` are Rust test sub-modules included
behind `#[cfg(test)]` — they are never compiled in production builds.
The no-panics checker was flagging `.unwrap()` and `assert!()` in
helper functions at module level in these files as production code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(admin): scope cost aggregates to 30d, drop external fonts, flatten detail response
Addresses review feedback on #1963:
- Scope all llm_calls aggregates to the 30d `since` window so the admin
dashboard query is served by `idx_llm_calls_created_at` rather than a
full table scan. Drops the unused all-time `total_cost` subquery from
both libsql and postgres backends.
- Self-contain the admin SPA — remove `fonts.googleapis.com` /
`fonts.gstatic.com` link tags from admin.html and tighten the admin
CSP to fully same-origin. Typography degrades to the system-font
fallback already listed in `font-family`.
- Fold `metadata` into `AdminUserInfo` (optional, skip-if-none) and
remove the `#[serde(flatten)]` wrapper, eliminating the
documented collision risk.
- Add regression test asserting `since` actually bounds the LLM
aggregates (future `since` should yield zero LLM counts without
affecting non-windowed counts).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: ilblackdragon@gmail.com <ilblackdragon@gmail.com>
* fix(docker): make runtime-staging the default Docker target for Railway
Railway's railway.toml doesn't support a `target` field — it was being
silently ignored, so the wasm-builder stage never ran and WASM extensions
were never pre-bundled.
Fix by reordering Dockerfile stages so runtime-staging is last (= default
target). Railway builds the default target, so it now gets WASM extensions.
CI is unaffected — it uses explicit --target flags in docker.yml.
Also reverts the CACHE_BUST arg added in efdb738a (no longer needed)
and removes the unsupported `target` field from railway.toml.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): add --target runtime to CI and local docker build commands
Copilot correctly flagged that test.yml's `docker build .` would now
build the wasm-builder stage (slow, flaky) since runtime-staging is
the default target. Add explicit --target runtime to CI and update
the Dockerfile header comment for local builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* perf(docker): decouple wasm-builder from builder stage
wasm-builder now inherits from chef instead of builder, so WASM
extensions only rebuild when tools-src/, channels-src/, registry/,
or wit/ change — not on every src/ edit. The extensions are standalone
crates with their own lockfiles and don't depend on the main workspace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(llm): add image detail field, auto-append /v1 to base URL (#2378, #1934)
Set detail: "auto" on ImageUrl construction so providers requiring the
field (e.g. MiniMax) no longer reject vision requests. Normalize
OpenAI-compatible base URLs by appending /v1 when missing, fixing 404s
for local model servers (MLX, vLLM, llama.cpp) using bare URLs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(llm): scope /v1 normalization to bare host-only URLs
Address review feedback:
- Only append /v1 when the URL has no path component (bare
scheme://host[:port]). URLs with existing paths like Zai's
/api/paas/v4 or Gemini's /v1beta/openai are now left unchanged.
- Use case-insensitive check for /v1 suffix to prevent double-suffixing
URLs like http://localhost:8080/V1.
- Document why Ollama is intentionally excluded from normalization
(uses /api/chat, not /v1/chat/completions).
- Add test cases for real provider URLs from providers.json (Zai,
Gemini) and case-insensitive /V1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: update gimli 0.33.1 -> 0.33.0 (yanked crate)
gimli v0.33.1 was yanked on crates.io, causing cargo-deny to fail.
Downgrade to v0.33.0 which is the latest non-yanked release compatible
with wasmtime 43.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: serrrfirat <f@nuff.tech>
* fix(ci): skip NearAI URL DNS validation when NearAI is not the active backend
LlmConfig::resolve() unconditionally called validate_base_url() on
default NearAI URLs (private.near.ai), which performs synchronous DNS
resolution. In environments without external DNS access (CI runners,
containers), this blocks startup then fails — breaking all E2E tests
when a different LLM backend is configured.
Conditionally skip validation when NearAI is not the active backend
and the user hasn't explicitly set the URL. Also removes redundant
@pytest.mark.asyncio decorators from test_webhook.py (asyncio_mode =
"auto" handles this automatically per project convention).
https://claude.ai/code/session_01FybyQXiX2HDhaGizxr2PFC
* fix(ci): also validate NearAI URLs when DB override or NearAI embeddings are active
The validation gate for NEARAI_BASE_URL and NEARAI_AUTH_URL previously
only checked whether NearAI was the primary chat backend or the URL was
explicitly set via env var. This allowed a base_url supplied through
settings.llm_builtin_overrides (DB override) or used by NearAI
embeddings (embeddings.provider=nearai) to bypass the SSRF guard.
Widen both validation gates to also fire when:
- nearai_override provides a base_url (DB builtin override)
- NearAI embeddings are enabled (embeddings enabled + provider=nearai)
https://claude.ai/code/session_01YSmxv6gT4d9kJu5vsjxpCz
* ci: retrigger CI checks
https://claude.ai/code/session_01AQ4iNcEfFeniBA1iMvFTuN
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ilblackdragon@gmail.com <ilblackdragon@gmail.com>
`models set-provider` intentionally writes to both config.toml and .env
for immediate effect. This caused the config resolver to always emit a
warning about DB (config.toml) overriding the env var, even though both
values are identical. Skip the warning when the values match, since there
is no silent override happening.
Co-authored-by: iyen <iyen@iyens-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): add `ironclaw profile list` subcommand
Wire the existing `list_profiles()` function from the deployment profile
system (#2203) into a new CLI subcommand so users can discover available
profiles, see which one is active, and read descriptions.
Closes#2271
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): address review feedback on profile list command
- Fix import ordering to satisfy rustfmt (BUILTIN_PROFILES, ProfileInfo, list_profiles)
- Log warning instead of silently ignoring errors when reading user profile files
- Replace manual serde_json::Value construction with typed ProfileEntry struct
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a tool call fails, the LLM often retries the exact same call with
identical args, repeating up to max_iterations (50 for chat, 10 for jobs)
with no mechanism to break the loop.
Add a DuplicateToolCallTracker to the agentic loop that fingerprints each
batch of tool calls by hashing (tool_name, canonicalized_args). When the
same fingerprint appears in consecutive iterations and all tools fail:
- After 3 consecutive duplicates: inject a warning message telling the
LLM to try a different approach
- After 5 consecutive duplicates: set force_text = true to disable tool
calls entirely
The tracker resets when the LLM calls different tools, any tool succeeds,
or a text response is produced. This follows the same counter + threshold
+ escalation pattern as the existing tool intent nudge and truncation
detection mechanisms.
Also extracts canonicalize_json_value from agent/routine.rs to util.rs
for reuse across modules.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Python orchestrator had no error counting for structured action calls
(Tier 0), allowing threads to loop indefinitely on failing tool calls and
complete "successfully" even when every tool call failed. This adds a
consecutive_action_errors counter that increments when all actions in a
batch fail, resets when any succeeds, injects a nudge at the threshold,
and transitions to failed at threshold + 2. Also prefixes error outputs
with [ACTION FAILED] for visibility and persists the counter in
checkpoints.
Closes#2325
Related: #2279, #2240
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore(engine): update monty to v0.0.11
Bump the embedded Python interpreter (pydantic/monty) from rev 7a0d4b7
to the v0.0.11 release. Notable upstream changes: ~2x faster JSON
loads/~1.6x faster dumps, filesystem mounting, Rust-side async API
additions, and mount edge case fixes. No Python-level syntax changes,
so the CodeAct preamble is unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): correct async/await limitation in MONTY.md
`await` and `asyncio.gather()` work for tool calls and llm_query()
via Monty's ExternalFuture/ResolveFutures mechanism. Only `async def`
(defining custom coroutines) is unsupported. The previous wording
incorrectly said async/await was not available at all.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): correct stale limitations in MONTY.md and CodeAct preamble
Remove features that actually work in Monty v0.0.11 from the
"not supported" lists:
- async def / await / asyncio.gather() — fully supported
- *expr star unpacking in assignments — fully supported
- generator expressions — work (yield statements still don't)
Clarify:
- class: host-provided dataclasses work, user-defined classes don't
- yield: generator expressions work, yield statements don't
- os module: available (os.getenv, os.path), not just os.path
- asyncio module: available (asyncio.gather)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): clarify os module is blocked, not available
import os succeeds in Monty but the executor blocks all OsFunction
calls (os.getenv, Path.*, os.environ) with OSError. Document this
explicitly and remove os from the available modules list. Agents must
use injected tools (shell, read_file, etc.) for OS operations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ci: retrigger with regression check skip
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: fix cargo-deny failures (wildcard deps + new rand advisory)
Add version constraints to ironclaw_engine, ironclaw_gateway, and
ironclaw_tui path dependencies so cargo-deny's wildcard check passes
for public crates. Ignore RUSTSEC-2026-0097 (rand unsoundness with
custom logger calling rand::rng() during reseed) — we don't use that
pattern.
[skip-regression-check]
https://claude.ai/code/session_01X86EZxqXEFiU9VetyhPKjM
* chore: add revisit-by date to rand advisory ignore
Address PR review feedback: add a concrete expiry date and upgrade
target so the RUSTSEC-2026-0097 ignore doesn't become a permanent
blind spot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [codex] Label migration PRs with DB MIGRATION (#1967)
* Add DB MIGRATION PR label
* Broaden DB MIGRATION label coverage
* chore(ci): address DB MIGRATION label review feedback
* Fix Telegram UTF-16 message splitting (#1961)
* Fix Telegram UTF-16 message splitting
* fix: bump telegram channel registry version
* chore: bump registry versions for github tool, whatsapp and telegram channels
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* revert: undo 2 main-only commits to unblock staging-promote merge (#2297)
Reverts:
- 6f7575de Fix Telegram UTF-16 message splitting (#1961)
- 7be3b910 [codex] Label migration PRs with DB MIGRATION (#1967)
Keeps f0db0a3d (registry version bumps) intact.
These changes were made directly on main and conflict with staging-promote.
Both already exist in staging and will flow back to main via the promote merge.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: release (#2075)
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
* fix(ci): unblock v0.25.0 release — fix tag filter and publish config (#2306)
The release pipeline broke when ironclaw_engine was added (Apr 2) with
a monty git dependency that blocks crates.io publishing. Additionally,
sub-crate tags (ironclaw_tui-v0.1.0) were triggering cargo-dist builds
and stealing the "Latest" badge from the main release.
- Narrow release.yml tag pattern to `ironclaw-v*` so only the main
binary release tags trigger cargo-dist (not sub-crate tags)
- Configure release-plz to skip crates.io publish for ironclaw
(publish = false) while still creating git tags for cargo-dist
- Mark ironclaw_engine, ironclaw_tui, ironclaw_gateway as
non-publishable (release = false) in release-plz.toml
- Add publish = false to tui and gateway Cargo.toml
- Remove version fields from non-publishable path deps in root
Cargo.toml
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: update WASM artifact SHA256 checksums [skip ci] (#2308)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
---------
Co-authored-by: firat.sertgoz <firat.sertgoz@near.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat: user-facing temperature setting for LLM requests
Add a configurable default sampling temperature (0.0–2.0) that users
can set via the web settings UI or API. The setting flows into the
main conversational agent loop via ReasoningContext, replacing the
hardcoded 0.7 default. Per-request temperature (e.g. from the
OpenAI-compatible endpoint) still takes precedence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: admin-scoped settings fallback for multi-tenant
Admin-set defaults now propagate to all members who haven't overridden
the value themselves. Three layers of change:
1. TenantScope::get_setting_with_admin_fallback() — checks user scope
first, then falls back to __admin__ scope. Used by the dispatcher
for temperature and selected_model.
2. Config::from_db_with_toml() — layers admin-scope settings between
TOML and per-user DB settings during resolution. Priority:
TOML < admin DB < per-user DB.
3. Settings API — GET/PUT/DELETE /api/settings/{key}?scope=admin lets
admins read/write to the shared default scope. Non-admins get 403.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clamp temperature to 0.0-2.0 before reaching provider
Address review comment on #2275: the backend must guard against
bad DB values and per-request overrides that bypass the frontend
range enforcement. Some providers reject out-of-range temperatures
outright.
Clamped at both the read site (dispatcher reading DB settings) and
the use site (reasoning.rs respond_with_tools) for defense in depth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR #2275 review feedback
- Strip admin-only LLM keys (ollama_base_url, openai_compatible_base_url,
llm_builtin_overrides, llm_custom_providers) from the admin-scope merge
in `Config::from_db_with_toml` and `Config::re_resolve_llm_with_secrets`
when the resolving user is not an operator. Defense-in-depth so a
non-admin member never inherits private/loopback provider endpoints
from admin defaults.
- Preserve per-request `reason_ctx.temperature` precedence in the
dispatcher: settings-derived temperature only applies when no value
was already set by the API caller. Extract `resolve_settings_temperature`
helper for direct unit testing.
- Add regression tests covering admin-scope strip behavior for both
operator and non-operator paths, plus the temperature precedence rule
including range clamping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Re-applies two changes that were reverted on main (92388b7a) to unblock
the staging-promote merge. Neither existed on staging:
- Fix Telegram message splitting to count UTF-16 code units instead of
Unicode scalar values (emoji like 😀 are 2 UTF-16 units, not 1)
- Add "DB MIGRATION" auto-label for PRs touching migration files
Original commits: 6f7575de (#1961), 7be3b910 (#1967)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The TUI (Ratatui-based terminal UI) is the richer, more polished
interactive experience with sidebar, log broadcaster, and context
display. The REPL is a bare-bones fallback. Since the `tui` cargo
feature is already compiled in by default, the runtime should match.
Users can still opt out with CLI_MODE=repl in env or DB settings.
[skip-regression-check]
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(engine): LLM council via per-call model override in CodeAct
Extend `llm_query()` and `llm_query_batched()` with a `model=` (and
`models=` for the batched variant) keyword so CodeAct can route
individual sub-queries to specific LLMs. The "LLM council" pattern
becomes a skill — the agent broadcasts the same prompt across a
parallel array of models and synthesizes the responses — with no new
tool, dispatch path, or capability boundary.
- Add `model: Option<String>` to `LlmCallConfig`; thread it through
`LlmBridgeAdapter` onto `CompletionRequest.model` /
`ToolCompletionRequest.model` so providers that honor per-request
overrides (NEAR AI, Anthropic OAuth, GitHub Copilot, Bedrock) pick
it up. Other providers fall back to their configured model.
- `handle_llm_query` extracts a `model` arg; `handle_llm_query_batched`
accepts either `model="..."` (broadcast) or `models=[...]` (parallel
array, length-validated against `prompts`).
- `__llm_complete__` host fn extracts `model` from explicit_config so
the Python orchestrator can also forward it.
- Update CodeAct preamble docs so the agent sees the new parameters.
- Add `skills/llm-council/SKILL.md` with the council pattern,
recommended NEAR AI model line-ups, and a synthesis example.
Tests: 5 new scripting tests (model kwarg forwarding, default `None`,
`models=` broadcast, single-`model=` broadcast, length-mismatch error)
and 2 new bridge tests (config.model → CompletionRequest.model on both
the no-tools and with-tools paths).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): address llm-council review feedback
Address three review comments on the LLM council PR:
1. Test coverage for the orchestrator entry point. Add two tests
driving `handle_llm_complete` directly with explicit_config
containing `model` (and a control case without it). Closes the
"test through the caller, not just the helper" gap — the previous
tests only exercised `handle_llm_query`, leaving the parallel
`__llm_complete__` host fn path unverified.
2. Loud failure on non-string entries in `models=[...]`. Previously
`models=[1, 2]` was silently coerced via `monty_to_string` to
`["1", "2"]`. Now returns `TypeError` with the offending value,
matching the existing length-mismatch handling style.
3. `None` slots in `models=[...]` are no longer backfilled by the
singular `model=` kwarg. Each slot is authoritative: a `None`
means "no override for this prompt" (use the configured default).
Mixing the two would have been surprising — the docs now spell
out the contract explicitly. Add a regression test that passes
both `models=[None, "gpt-4o"]` and `model="claude-..."` and
asserts the None slot stays None.
Also update `skills/llm-council/SKILL.md` to default to a 4-model
council of `anthropic/claude-opus-4-6`, `google/gemini-3-pro`,
`zai-org/GLM-latest`, `openai/gpt-5.4`. Per-call provider errors
already flow through the existing `Ok(Err(e))` arm as
`"Error: ..."` strings — the batch never fails as a whole, so
unavailable models just surface in their own slot.
Tests: 4 new (2 orchestrator, 2 scripting), all 346 engine unit
tests pass, zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): strict optional-string parsing for llm_query model= kwarg
Address Copilot review on PR #2320. Four comments, all valid:
1 & 2. `model` and `single_model` were extracted via `extract_string_arg`,
which calls `monty_to_string` — that coerces `MontyObject::None` to the
literal string "None" and stringifies non-string values (ints become
"1", etc.). So `llm_query(prompt="hi", model=None)` would silently
route every call to a bogus model ID called "None".
Add a strict `extract_optional_string_kwarg` helper that returns
`Ok(None)` for missing/`None`, `Ok(Some(s))` for strings, and a
`TypeError` for anything else. Use it in both `handle_llm_query` and
`handle_llm_query_batched` for the `model=` kwarg. Regression tests
cover: `model=None` → no override, `model=<int>` → TypeError, and the
same two cases on the batched path.
3. The `models=` list-type error message said "list of strings" but we
accept `None` entries. Updated to "list of str or None".
4. SKILL.md claimed the batched call "never raises". It does — for
argument validation errors (wrong types, length mismatch). Clarified
that per-model failures return as `"Error: ..."` strings, but
argument validation still raises.
Tests: 4 new regression tests, all 4687 main-crate and 358 engine tests
pass, zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): positional args for llm_query_batched + correct provider docs
Address two review comments from serrrfirat on PR #2320.
1. `llm_query_batched` silently dropped positional `context`/`model`/
`models` args. The documented signature is
`llm_query_batched(prompts, context=None, model=None, models=None)`,
but the extractors were hardcoded to kwargs only (`&[]` for args).
A call like `llm_query_batched(prompts, None, "gpt-4o")` routed to
the default model — silent contract violation.
Thread the real `args` slice into each extractor with the documented
positional indices: context=1, model=2, models=3. Positional
`MontyObject::None` at any of those slots now correctly means "no
override". Added 3 regression tests:
- `llm_query_batched_honors_positional_context_and_model`
- `llm_query_batched_honors_positional_models_list`
- `llm_query_batched_positional_none_for_models_is_no_override`
2. SKILL.md claimed Bedrock honors per-request model overrides, but
`bedrock.rs::complete()` unconditionally uses
`self.current_model_id()` and ignores `request.model`. Also, the
default 4-model prefixed lineup (`anthropic/...`, `google/...`,
`openai/...`) only works on aggregator backends like NEAR AI — a
direct Anthropic OAuth or Copilot provider honors `set_model` but
can only switch between models within its own vendor.
Rewrite the SKILL.md preamble with a provider capability table
(dropping Bedrock from "honors it" and adding cross-vendor routing
as a separate column), and add per-backend default lineups:
NEAR AI (prefixed cross-vendor), Anthropic OAuth (Anthropic tiers
only), Copilot (Copilot-exposed models). For backends that don't
honor `model=` at all (Bedrock, raw OpenAI/Ollama/Tinfoil), the
skill now instructs the agent to tell the user and fall back to a
single-model answer.
Tests: 3 new regression tests, all 4687 main-crate and 361 engine tests
pass, zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Python orchestrator only appended ActionResult messages when
`output is not None`, which left dangling tool_calls in working_messages
whenever:
1. A tool returned null output (JSON null -> Python None)
2. A result was gate_paused (the result JSON has no `output` field)
3. RequireApproval preflight returned fewer results than calls
OpenAI's Responses API requires 1:1 correspondence between
function_call and function_call_output items, so the gap surfaced as
HTTP 400 "No tool output found for function call <id>" on the next
LLM turn.
- Iterate over executable_calls (not results) and emit an ActionResult
for every call, falling back to "[no output]" / "[execution skipped]"
placeholders when the real output is missing.
- Pad the RequireApproval early-return results to match parsed.len()
so the Python side sees a null slot (and emits the placeholder)
instead of a shorter list.
- Regression tests at both layers: json_to_thread_messages parsing
and the full ThreadMessage -> ChatMessage -> sanitize_tool_messages
pipeline, both asserting that every assistant tool_call has a
matching tool result.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The release pipeline broke when ironclaw_engine was added (Apr 2) with
a monty git dependency that blocks crates.io publishing. Additionally,
sub-crate tags (ironclaw_tui-v0.1.0) were triggering cargo-dist builds
and stealing the "Latest" badge from the main release.
- Narrow release.yml tag pattern to `ironclaw-v*` so only the main
binary release tags trigger cargo-dist (not sub-crate tags)
- Configure release-plz to skip crates.io publish for ironclaw
(publish = false) while still creating git tags for cargo-dist
- Mark ironclaw_engine, ironclaw_tui, ironclaw_gateway as
non-publishable (release = false) in release-plz.toml
- Add publish = false to tui and gateway Cargo.toml
- Remove version fields from non-publishable path deps in root
Cargo.toml
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts:
- 6f7575de Fix Telegram UTF-16 message splitting (#1961)
- 7be3b910 [codex] Label migration PRs with DB MIGRATION (#1967)
Keeps f0db0a3d (registry version bumps) intact.
These changes were made directly on main and conflict with staging-promote.
Both already exist in staging and will flow back to main via the promote merge.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>