1325 Commits

Author SHA1 Message Date
Henry Park
37669e6ec8 fix(web): prevent browser crash from timer leaks, DOM growth, SSE buffer (#2406) (#2441)
* 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>
2026-04-14 10:25:42 -07:00
Illia Polosukhin
fe2b134fd8 fix(engine): guard consecutive-error checks against None limit (#2460)
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.
2026-04-14 18:30:16 +03:00
Will.hou
b6f5da88a7 perf(tunnel): reuse HTTP client in CustomTunnel health checks (#1201)
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>
2026-04-14 16:52:54 +03:00
Achieve
019c0482cb refactor(llm): promote decorator chain settings from NearAiConfig to top-level LlmConfig (#1749)
* refactor(llm): promote decorator chain settings from NearAiConfig to top-level LlmConfig

* review: add env var override/fallback tests and update module spec

---------

Co-authored-by: Firat Sertgoz <f@nuff.tech>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 16:52:24 +03:00
Guille
ab1f2796b1 fix: more strict check for registry to avoid false positives (#2222)
* 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>
2026-04-14 16:51:17 +03:00
Illia Polosukhin
aea2e87865 fix(ux): actionable auth errors and improved CLI help for new users (#1852) (#2315)
* 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>
2026-04-14 16:49:57 +03:00
Nige
46ff740cc8 docs(setup): warn that Telegram open mode splits history (#2427)
* docs(setup): warn about telegram open mode split identity

* Update src/setup/channels.rs

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix(setup): use idiomatic telegram mode check

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-14 16:48:47 +03:00
Coffee
86a9d0bd12 fix: image generation with nearai models (#1819)
Co-authored-by: Robert Yan <46699230+think-in-universe@users.noreply.github.com>
2026-04-14 19:21:18 +08:00
Zaki Manian
7425dc0f4a fix(security): harden approval thread safety (TOCTOU + error handling) (#2366)
* 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 #1487

https://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>
2026-04-14 12:31:56 +03:00
Coffee
57d7b54193 Fix Feishu webhook auth refresh and extension card overflow (#2443) 2026-04-14 11:09:15 +03:00
Coffee
ba810443d2 fix(mcp): Install NEAR AI MCP server from environment config (#2181) 2026-04-14 15:47:49 +08:00
jinxin
16b7b06abe feat(web): add ironclaw docs link (#2398) 2026-04-14 15:44:35 +08:00
firat.sertgoz
532fc61d25 feat: admin management panel — web UI for users and usage monitoring (#1963)
* 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>
2026-04-14 15:52:52 +09:00
Henry Park
4dbb44cf0f fix(docker): make runtime-staging the default Docker target for Railway (#2244)
* 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>
2026-04-13 14:37:40 -07:00
Zaki Manian
160a75e38c fix(llm): image detail field + /v1 base URL normalization (#2380)
* 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>
2026-04-13 15:40:57 +03:00
Zaki Manian
a9cea6c21d fix(ci): skip NearAI URL DNS validation for non-NearAI backends (#2080)
* 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>
2026-04-13 18:07:43 +09:00
IYEN
625cd85a91 Suppress LLM_BACKEND warning when config.toml and .env values match (#2388)
`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>
2026-04-13 11:08:15 +03:00
firat.sertgoz
3f6149c53e feat(cli): add ironclaw profile list subcommand (#2288)
* 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>
2026-04-13 12:40:39 +09:00
Zaki Manian
50fc2804c5 fix(agent): detect and escalate repeated identical failing tool calls (#2240) (#2338)
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>
2026-04-13 12:39:51 +09:00
Zaki Manian
4529f009d4 fix(engine): track consecutive action errors in orchestrator Tier 0 path (#2325) (#2340)
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>
2026-04-13 10:49:25 +09:00
Illia Polosukhin
66ccafb96b chore(engine): update monty to v0.0.11 (#2364)
* 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>
2026-04-12 18:20:08 -07:00
firat.sertgoz
ed2d6dc3b1 fix web chat refresh active thread (#2330) 2026-04-12 22:44:01 +09:00
Illia Polosukhin
3cb77fe0ed fix: resolve cargo-deny failures (wildcard deps + rand advisory) (#2370)
* 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>
2026-04-12 21:44:21 +09:00
Henry Park
fdb0a13b91 chore: sync staging and main (#2337)
* [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>
2026-04-12 08:15:44 +02:00
Illia Polosukhin
88b87c0ae1 feat: user-facing temperature setting (#2275)
* 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>
2026-04-11 17:35:56 +09:00
Henry Park
cd9b60c64b fix: re-apply Telegram UTF-16 splitting and DB MIGRATION label (#2304)
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>
2026-04-11 17:21:25 +09:00
Robert Yan
207c4d4694 ci: build docker image in release process (#2321) 2026-04-11 17:11:30 +09:00
firat.sertgoz
70862ed57c feat(config): default CLI_MODE to TUI instead of REPL (#2329)
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>
2026-04-11 17:09:56 +09:00
Illia Polosukhin
764e586717 feat(engine): LLM council via per-call model override in CodeAct (#2320)
* 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>
2026-04-11 09:23:05 +03:00
Illia Polosukhin
7d66d83d61 fix(engine): always append ActionResult for every tool call (#2322)
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>
2026-04-11 09:21:56 +03:00
firat.sertgoz
4032f6de23 Fix paired Telegram owner scope routine visibility (#2258)
* Fix paired Telegram owner scope routing

* fix: address review findings (iteration 1)

* fix: address telegram owner routing feedback

* test: isolate telegram routines e2e fixture

---------

Co-authored-by: Guille <gagdiez.c@gmail.com>
2026-04-11 08:36:35 +03:00
firat.sertgoz
a7401eccc4 fix(gateway): scope chat approvals to the active thread (#2267) 2026-04-11 08:32:24 +03:00
github-actions[bot]
72829dbb01 chore: update WASM artifact SHA256 checksums [skip ci] (#2308)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-10 18:49:57 -07:00
Henry Park
be6de43f8e 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>
ironclaw-v0.25.0
2026-04-10 17:59:51 -07:00
ironclaw-ci[bot]
fda376768e chore: release (#2075)
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
ironclaw_common-v0.2.0 ironclaw_safety-v0.2.1
2026-04-10 17:32:43 -07:00
Henry Park
fc0ff2f1aa Merge pull request #2301 from nearai/staging-promote/a53eac5c-24269060157
chore: promote staging to main (2026-04-10 23:37 UTC)
2026-04-10 17:01:21 -07:00
Henry Park
7378da3b95 Merge branch 'main' into staging-promote/a53eac5c-24269060157 2026-04-10 17:00:44 -07:00
Henry Park
a53eac5c2d fix(ci): bump 5 channel versions + fix lifetime desync in panics check (#2300)
Version bumps for channels with source changes:
- discord 0.2.2 -> 0.2.3 (pairing message UX)
- feishu 0.1.4 -> 0.2.0 (pairing flow refactor + multi-tenancy)
- slack 0.2.2 -> 0.3.0 (broadcast feature implementation)
- telegram 0.2.6 -> 0.2.8 (webhook dedup + configurable polling)
- whatsapp 0.2.0 -> 0.2.2 (pairing message UX)

Fix check_no_panics.py: Rust lifetime annotations ('static, 'a) were
parsed as char literal openings, blanking the rest of the line including
any opening brace. This caused the brace-depth tracker to desync in
large test modules, producing false positives (e.g. server.rs:6378).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:37:12 -07:00
Henry Park
ba0f8252ec Merge pull request #1893 from nearai/staging-promote/9c6d8cb3-23875318792
chore: promote staging to staging-promote/42623ed1-23780941831 (2026-04-01 23:10 UTC)
ironclaw_gateway-v0.1.0 ironclaw_skills-v0.1.0 ironclaw_tui-v0.1.0
2026-04-10 16:28:15 -07:00
Henry Park
8c65f3d48f chore: merge main into staging-promote to resolve conflicts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:20:48 -07:00
Henry Park
92388b7a5c 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>
2026-04-10 16:19:17 -07:00
Henry Park
0facf1684f Merge pull request #1953 from nearai/staging-promote/aa59ca09-23935258795
chore: promote staging to staging-promote/4c9a985b-23931806540 (2026-04-03 05:32 UTC)
2026-04-10 16:10:45 -07:00
Henry Park
cc5846ddd4 Merge pull request #2028 from nearai/staging-promote/e1695914-23995878265
chore: promote staging to staging-promote/f3036388-23995135957 (2026-04-05 06:24 UTC)
2026-04-10 16:10:25 -07:00
Henry Park
269b2d3f5c Merge pull request #2032 from nearai/staging-promote/733678dd-23996777140
chore: promote staging to staging-promote/e1695914-23995878265 (2026-04-05 07:24 UTC)
2026-04-10 16:10:14 -07:00
Henry Park
3b558366ca Merge pull request #2044 from nearai/staging-promote/5083aed4-24002418644
chore: promote staging to staging-promote/733678dd-23996777140 (2026-04-05 13:23 UTC)
2026-04-10 16:10:05 -07:00
Henry Park
1e8c160718 Merge pull request #2052 from nearai/staging-promote/13852ff5-24021660555
chore: promote staging to staging-promote/5083aed4-24002418644 (2026-04-06 06:33 UTC)
2026-04-10 16:09:55 -07:00
Henry Park
ea91d9df3f Merge pull request #2053 from nearai/staging-promote/f9ed8152-24023233420
chore: promote staging to staging-promote/13852ff5-24021660555 (2026-04-06 07:31 UTC)
2026-04-10 16:09:44 -07:00
Henry Park
b847855016 Merge pull request #2063 from nearai/staging-promote/d0096dfc-24035427523
chore: promote staging to staging-promote/f9ed8152-24023233420 (2026-04-06 14:19 UTC)
2026-04-10 16:09:32 -07:00
Henry Park
f72bdb241c Merge pull request #2067 from nearai/staging-promote/9cf37364-24039632441
chore: promote staging to staging-promote/d0096dfc-24035427523 (2026-04-06 16:13 UTC)
2026-04-10 16:09:22 -07:00
Henry Park
203a9946cf Merge pull request #2076 from nearai/staging-promote/8b629851-24041939370
chore: promote staging to staging-promote/9cf37364-24039632441 (2026-04-06 17:15 UTC)
2026-04-10 16:09:12 -07:00