* feat(gateway): full settings page polish with all tiers
- Backend: add ActiveConfigSnapshot to expose resolved LLM backend,
model, and enabled channels via /api/gateway/status
- Add missing Agent settings (daily cost cap, actions/hour, local tools)
- Add Sandbox, Routines, Safety, Skills, and Search setting groups
- Settings import/export (JSON download + file upload)
- Active env defaults shown as placeholders in Inference settings
- Styled confirmation modals replace window.confirm() for remove actions
- Global restart banner persists across settings subtab switches
- Client-side validation with min/max constraints on number inputs
- Accessibility: aria-label on inputs, role=status on save indicators
- Settings search filters rows across current subtab
- Smooth CSS transitions for conditional field visibility (showWhen)
- Tunnel settings in Channels subtab
- Mobile responsive settings layout at 768px breakpoint
- i18n keys for toolbar, search, and import/export in en + zh-CN
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat(gateway): polish settings page and remove registered tools debug section
Remove the "Registered Tools" table from the extensions tab (debug info
not useful to end users), clean up associated CSS/i18n/JS. Additional
settings page UI polish: extension card state styling, layout refinements.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(gateway): address PR review feedback [skip-regression-check]
- Use refreshCurrentSettingsTab() in SSE event handlers to reduce duplication
- Remove unused formatGroupName/formatSettingLabel helpers
- Use i18n keys for MCP Configure/Reconfigure buttons
- Add data-i18n-placeholder to settings search input
- Remove data-i18n from confirm modal button (set dynamically by showConfirmModal)
- Fix cargo fmt in main.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(e2e): update tests for unified settings tab layout [skip-regression-check]
- Update TABS list: replace extensions/skills with settings
- Add settings_subtab/settings_subpanel selectors to helpers
- Update test_connection, test_skills, test_extensions, test_wasm_lifecycle
to navigate via Settings > subtab instead of top-level tabs
- Move MCP card tests to use go_to_mcp() helper (MCP is now a separate subtab)
- Remove tools table tests and mock_ext_apis tools= parameter
- Fix CSP violation: replace inline onclick on confirm modal cancel button
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(gateway): address second round of PR review feedback [skip-regression-check]
- Use I18n.t() for MCP empty state, export/import toasts, confirm modal
- Fix CLI channel card using wrong channel key ('repl' -> 'cli')
- Fix settings search counting hidden rows as visible
- Add aria-label i18n for settings search input
- Add common.loadFailed i18n key (en + zh-CN)
- Update E2E tests: WASM channel tests use Channels subtab,
remove tests use custom confirm modal instead of window.confirm
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(e2e): fix WASM channel card selector and skills remove confirm [skip-regression-check]
- WASM channel tests: filter by display name to avoid matching built-in
channel cards in the Channels subtab
- Skills remove test: click confirm modal button instead of using
window.confirm (skill removal now uses custom confirm modal)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(gateway): address third round of PR review feedback [skip-regression-check]
- approval_needed SSE: refresh any active settings subtab, not just
Extensions — approvals can surface from Channels/MCP setup flows too
- renderCardsSkeleton: remove nested .extensions-list wrapper that
caused skeleton cards to render constrained inside grid cells
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(e2e): fix auth_completed reload test race condition [skip-regression-check]
Use expect_response to deterministically wait for the /api/extensions
reload triggered by handleAuthCompleted → refreshCurrentSettingsTab,
instead of a fixed 600ms sleep that was too short under CI load.
Also remove stale /api/extensions/tools route handler.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(e2e): debug auth_completed reload test with function counter [skip-regression-check]
Inject a counter wrapper around refreshCurrentSettingsTab to verify it's
actually called, and wait for the async fetch to complete before
asserting the reload count.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* feat(gateway): localize all settings labels, descriptions, and channel cards [skip-regression-check]
Move 120+ hardcoded strings in settings definitions (INFERENCE_SETTINGS,
AGENT_SETTINGS, NETWORKING_SETTINGS) and channel card labels to i18n
keys. Render functions now resolve labels via I18n.t() so the settings
page translates when switching locales.
Covers: group titles, setting labels/descriptions, built-in channel
names/descriptions, and the "No settings found" empty state.
Both en.js and zh-CN.js updated with all new cfg.* and channels.* keys.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(gateway): localize remaining hardcoded UI strings [skip-regression-check]
- Fix export error toast using wrong i18n key (importFailed → exportFailed)
- Replace "Failed to load settings:" with I18n.t('common.loadFailed')
- Localize renderBuiltinChannelCard: "Built-in", "Active", "Inactive"
- Localize settings placeholders: "env: ", "env default", "use env default"
- Localize "✓ Saved" indicator
- Add new i18n keys to both en.js and zh-CN.js
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(gateway): confirm modal a11y, Esc/click-outside, search guard [skip-regression-check]
- Add role="dialog", aria-modal="true", aria-labelledby to confirm modal
- Focus confirm button when modal opens
- Close modal on Escape key or overlay click
- Skip settings search on non-settings panels (Extensions/MCP/Skills/Channels)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(gateway): boolean tri-state, search reset on subtab switch, stale model suggestions [skip-regression-check]
Address PR review feedback:
- Boolean settings now use a tri-state select (env default / On / Off)
instead of a checkbox, matching the pattern used by other select settings
and allowing users to revert to the env default
- Clear search input when switching settings subtabs so stale filters
don't carry over to the new panel
- Always assign model suggestions (even empty array) so stale IDs from a
previous successful /v1/models fetch don't persist when the endpoint
later returns empty
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(gateway): auth_completed handler, bedrock_cross_region select, integer-only number inputs [skip-regression-check]
Address PR review feedback:
- auth_completed SSE listener now delegates to handleAuthCompleted(data)
instead of inlining logic with a bare closeConfigureModal() call, so
only the matching extension's modal is dismissed
- bedrock_cross_region changed from free text to select with the four
valid values (us/eu/apac/global), matching backend validation
- Number settings now use step=1 and parseInt() instead of parseFloat(),
preventing fractional values that the backend (u32/u64) would reject
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: full_job routine runs stay running until linked job completion (#1317)
Previously, execute_full_job() returned RunStatus::Ok immediately after
dispatching the job, causing routine runs to be marked as completed before
the linked worker job had actually finished. This meant failure notifications
were never sent and max_concurrent guardrails stopped applying once the run
was prematurely finalized.
Changes:
- execute_full_job() now returns RunStatus::Running instead of Ok
- execute_routine() skips finalization for Running status (leaves run open)
- New sync_dispatched_runs() polls on each cron tick, checks linked job
state, and finalizes runs when jobs reach terminal states
- New list_dispatched_routine_runs() DB method on both backends
- Deferred notifications are sent when the run is actually finalized
- consecutive_failures is preserved (not reset) while outcome is unknown
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review feedback (watcher predicate, running_count safety)
- FullJobWatcher: use is_parallel_blocking() instead of is_active() so
the watcher exits when a job reaches Completed (not terminal but
finished executing). Fixes infinite-poll for routine jobs.
- Remove running_count decrement from sync_dispatched_runs() — in normal
flow execute_routine() handles it; sync only runs for crash recovery
where the counter is already 0.
- Update PR description to match actual FullJobWatcher behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: sync only at startup to prevent double-completion race
- Move sync_dispatched_runs() out of cron loop into startup-only path.
During normal operation FullJobWatcher handles finalization inline;
running sync on every tick would race with the watcher.
- Update complete_dispatched_run() to properly advance runtime fields
(last_run_at, next_fire_at, run_count) for crash recovery — in that
scenario execute_routine() never reached its runtime update.
- Fix stale doc comment on complete_dispatched_run().
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: use boot_time filter for safe periodic sync of orphaned runs
- Add boot_time field to RoutineEngine, set to Utc::now() at creation.
- sync_dispatched_runs() now filters runs by started_at < boot_time,
so it only processes orphans from a previous process — never races
with FullJobWatcher instances from the current process.
- Move sync back into the cron loop (safe with boot_time filter) and
run it BEFORE check_cron_triggers to avoid picking up freshly
dispatched runs.
- Fix doc comments to match actual behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: add FullJobWatcher to track full_job lifecycle for concurrency (#1318)
full_job routines previously bypassed max_concurrent and global concurrency
limits because execute_full_job() returned RunStatus::Ok immediately after
dispatch. This meant running_count was decremented and the routine_run row
was finalized before the actual job completed.
Introduce FullJobWatcher struct that polls store.get_job() every 5s until
the linked job reaches a non-active state, then maps the final JobState to
RunStatus. execute_full_job now creates and awaits the watcher, keeping both
the DB-level running row and the in-memory running_count elevated for the
full job duration.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* test: full_job concurrency regression tests (issue #1318)
Add two integration tests verifying full_job routine concurrency:
1. full_job_max_concurrent_blocks_second_fire_while_first_active:
Inserts a Running routine_run (simulating an in-flight full_job) and
verifies fire_manual returns MaxConcurrent error for max_concurrent=1.
2. global_concurrency_counts_live_full_job_runs:
Elevates running_count to simulate a live full_job holding the global
slot, verifies check_cron_triggers skips due routines, then releases
the slot and verifies the routine fires.
Also makes running_count_for_test() unconditionally public so integration
tests (separate crate) can access it.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: fmt and clippy fixes for full_job concurrency tests
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review feedback on FullJobWatcher
- Add #[doc(hidden)] to running_count_for_test() to hide from public API
- Derive MAX_POLLS from POLL_INTERVAL to keep constants coupled
- Check job state before first sleep to finalize promptly for fast jobs
- Update execute_full_job doc comment to reflect blocking behavior
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
One flaky test (test_builtin_echo_tool timeout) was stopping the entire
e2e coverage suite via -x, preventing 118+ remaining tests from running
and generating coverage data.
Tests are independent (each gets a fresh browser context via the
function-scoped page fixture), so removing -x is safe.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: consume matched event routine messages
* style: run rustfmt for event routine fix
* fix: preserve preprocessing for routine-triggered messages
* fix: match routines against rewritten input
* refactor: narrow check_event_triggers API and simplify routine_engine_slot
Address Copilot review feedback:
- Change check_event_triggers to accept (user_id, channel, content) instead
of &IncomingMessage, eliminating the need to clone the full message
(including attachments) when hooks rewrite content.
- Remove routine_trigger_message and the Cow<IncomingMessage> indirection;
the event-trigger check now inlines the is_internal + UserInput guard and
passes the post-hook content string directly.
- Make routine_engine_slot non-optional since Agent::new() always
initializes it. Removes the redundant Option wrapper and simplifies
accessor/setter methods.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: add debug_assert invariant guards to critical code paths (closes#1215)
Add three debug_assert! calls to catch impossible-in-correct-code states
early in debug builds without affecting release performance:
- execute_tool_with_safety: assert tool_name is non-empty at entry
- JobContext::transition_to: assert state machine transition is valid
- CircuitBreakerProvider::record_success: assert circuit is not Open
(check_allowed() must gate all calls before record_success())
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* test: add regression test for empty tool name invariant guard
Covers the debug_assert!(!tool_name.is_empty()) added in execute_tool_with_safety.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* feat: upgrade MiniMax default model to M2.7
- Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to model list
- Set MiniMax-M2.7 as default model
- Keep all previous models as alternatives
- Update related tests
* fix: use canonical model name in test per review
Use MiniMax-M2.7-highspeed (canonical casing) in the reasoning
models test for consistency with the documentation and provider
configuration.
[skip-regression-check]
These tests guard against catastrophic regex backtracking (seconds/minutes),
not 12ms differences. CI runners with coverage instrumentation (cargo-llvm-cov)
consistently exceed the 100ms threshold due to overhead, causing flaky failures.
500ms still catches real regressions while tolerating CI variability.
[skip-regression-check]
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: Rate limiter returns retry after None instead of a duration
linter fix
* review fixes
* fix: rate limiter returns None for retry_after duration
Add regression test to src/llm/retry.rs that verifies RateLimited errors
always have a fallback duration (never None) due to the 60-second fallback
applied in all rate limit error creation sites (nearai_chat.rs,
anthropic_oauth.rs, embeddings.rs).
The production code fix adds `.or(Some(Duration::from_secs(60)))` to ensure
the error message never displays "retry after None" to the user.
[skip-regression-check]
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
---------
Co-authored-by: Claude Haiku 4.5 <[email protected]>
- Remove duplicate build_nearai_model_fetch_config() definition from setup/wizard.rs
(function already exists in llm/models.rs and is imported)
- Add missing cheap_model and smart_routing_cascade fields to LlmConfig
initializer in build_nearai_model_fetch_config() (llm/models.rs)
- Pass request_timeout_secs to create_registry_provider() call
(llm/mod.rs:432)
All clippy checks pass with zero warnings (--no-default-features --features libsql).
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
Resolved merge conflicts in 5 files:
1. src/agent/job_monitor.rs - Used is_internal flag approach (HEAD) for safe internal message marking. Removed metadata-based approach which could be spoofed by external channels.
2. src/agent/agent_loop.rs - Used is_internal check (HEAD) for routing internal messages, consistent with security model where is_internal field cannot be spoofed.
3. src/agent/dispatcher.rs - Included notify_metadata in job context (main), needed for job routing through JobMonitorRoute.
4. src/setup/wizard.rs - Added build_nearai_model_fetch_config() function (main) for model selection during setup.
5. src/tools/builtin/job.rs - Used both comments from HEAD (clarifying notify_channel and notify_user logic) while removing metadata field from JobMonitorRoute (consistent with job_monitor.rs).
All conflicts resolved with security-first approach: use is_internal boolean field for internal message marking (cannot be spoofed), while passing routing metadata through context.
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* feat: add LLM_CHEAP_MODEL for generic smart routing across all backends
Add generic cheap model support that works with any LLM backend, not just
NearAI. New env vars: LLM_CHEAP_MODEL (cheap model for any backend) and
SMART_ROUTING_CASCADE (top-level cascade flag).
Resolution order: LLM_CHEAP_MODEL > NEARAI_CHEAP_MODEL (backward compat).
Registry-based providers (OpenAI, Anthropic, Groq, etc.) clone their
RegistryProviderConfig with the cheap model swapped in. Bedrock returns
an explicit error (not yet supported). All error paths use ok_or_else
with proper LlmError variants -- no unwrap/expect in production code.
* refactor: address Gemini review — remove unnecessary async, extract cheap_model_name()
- Remove async from create_cheap_provider_for_backend() and
create_cheap_llm_provider() — neither contains .await calls
- Extract duplicated cheap model resolution logic into
LlmConfig::cheap_model_name() helper method (DRY)
- Revert tests from tokio::test async back to sync #[test]
- Add test_cheap_model_name_resolution() unit test for the helper
---------
Co-authored-by: SMKRV <[email protected]>
Route messages and replies to the correct Telegram forum topic via
message_thread_id. Key behaviors:
- Parse message_thread_id, is_topic_message, is_forum from incoming updates
- Thread agent sessions by "chat_id:topic_id" for forum groups only
(non-forum reply threads are excluded via is_forum guard)
- Pass message_thread_id through all send methods (text, photo, document)
- Normalize thread_id=1 (General topic) to None for sendMessage/sendPhoto/
sendDocument since Telegram rejects it, but preserve it for sendChatAction
where Telegram requires it for typing indicators
- Hoist bot_username workspace read to avoid duplicate WASM host call per
group message
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(orchestrator): read ORCHESTRATOR_PORT env var for configurable API port
The orchestrator internal API port was hardcoded to 50051 in two places
(ContainerJobConfig and OrchestratorApi::start call), making it impossible
to run multiple IronClaw instances on the same host — the second instance
fails with "Address already in use".
NETWORK_SECURITY.md already documents ORCHESTRATOR_PORT as configurable,
and ContainerJobConfig.orchestrator_port is propagated to worker containers
via IRONCLAW_ORCHESTRATOR_URL, but the env var was never actually read.
Extract resolve_orchestrator_port() that reads ORCHESTRATOR_PORT and falls
back to 50051. Includes tests for valid, invalid, and out-of-range values.
* test: add ENV_LOCK mutex for env-var test serialization
Address Gemini review: add std::sync::Mutex to serialize env var access
across test threads. Keep unsafe blocks — required in Rust edition 2024
where std::env::set_var/remove_var are unsafe functions.
---------
Co-authored-by: SMKRV <[email protected]>
* feat(transcription): add Chat Completions API provider for audio transcription
The existing transcription pipeline only supports the OpenAI Whisper API
(/v1/audio/transcriptions with multipart upload). Providers like OpenRouter
expose audio transcription through the Chat Completions API instead, using
base64-encoded audio in the `input_audio` content type.
Add `ChatCompletionsTranscriptionProvider` that sends audio as base64 in
a chat completion request and extracts the transcript from the response.
Compatible with OpenRouter, OpenAI GPT-4o-audio, and any provider that
supports audio input via Chat Completions.
Config changes:
- TRANSCRIPTION_PROVIDER=chat_completions selects the new provider
- TRANSCRIPTION_API_KEY overrides provider-specific keys
- LLM_API_KEY used as fallback for chat_completions provider
- Default model per provider (whisper-1 for openai, gemini-2.0-flash for
chat_completions)
* style: address review feedback — formatting, idiomatic patterns
- Fix rustfmt formatting for provider constructor chain
- Use or_else for resolve_api_key priority chain (Gemini review)
- Use trim_end_matches('/') instead of while loop (Gemini review)
---------
Co-authored-by: SMKRV <[email protected]>
* fix(jobs): make completed->completed transition idempotent to prevent race errors
Both execution_loop and the worker wrapper in execute() can race to call
mark_completed(). Previously the second call hit "Cannot transition from
completed to completed" and errored the job despite successful completion.
This narrowly allows only the Completed->Completed self-transition as
idempotent (early return with debug log, no duplicate history entry).
All other self-transitions remain rejected to preserve state machine
strictness.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix assert! formatting in idempotent completion test
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>