🐛 fix: enable executor=client tools for desktop Electron callers#13790
Conversation
Adds a `clientRuntime` signal to execAgent so the server knows the caller itself can execute `executor: 'client'` tools (local-system, stdio MCP) over its Agent Gateway WebSocket. This is the missing server piece for Phase 6.4 (LOBE-7076): previously `local-system` only entered the manifest when a *separately registered* remote device was online & auto-activated, so a desktop Electron caller sitting on the other end of the Gateway WS could never actually be dispatched to via `tool_execute`. The new signal is orthogonal to the legacy device-proxy `deviceContext` — it describes the caller itself, not a third-party device. The enable rule for LocalSystemManifest simply gets one extra OR branch: local && gatewayConfigured && (hasClientExecutor || legacy-device-online-activated) `toolExecutorMap[LocalSystemManifest.identifier] = 'client'` (LOBE-7067) then kicks in as soon as the manifest entry is present, so `RuntimeExecutors.call_tool` (LOBE-7068) will push `tool_execute` over the Agent Gateway WS to this caller. Plumbing: - packages/types: `ExecAgentParams.clientRuntime?: 'desktop' | 'web'` - lambda router: accepts + forwards `clientRuntime` - aiAgent service: forwards to `createServerAgentToolsEngine` - AgentToolsEngine: +1 field, +1 OR branch in LocalSystem enable rule. Zero changes to `runtimeMode` / `platform` / `RemoteDeviceManifest` / `deviceContext` semantics. Tests: 3 new cases in AgentToolsEngine covering desktop / web / gateway-off branches; 3 new cases in execAgent.deviceToolPipeline verifying the `clientRuntime` param is forwarded verbatim. Follow-up (separate PR): frontend receives `tool_execute`, runs the tool via Electron IPC, and sends `tool_result` back over the same WS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f98d04b901
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * 'desktop' enables `executor: 'client'` tools (local-system, stdio MCP) | ||
| * to be dispatched over the Agent Gateway WS. | ||
| */ | ||
| clientRuntime: z.enum(['desktop', 'web']).optional(), |
There was a problem hiding this comment.
Forward clientRuntime in batch execAgent tasks
ExecAgentSchema now accepts clientRuntime, and execAgents reuses that schema for each task, but the batch path never passes clientRuntime to aiAgentService.execAgent (it only forwards a subset of fields). As a result, execAgents requests with clientRuntime: 'desktop' silently lose the flag, so desktop client-executor tools (e.g. local-system) are not enabled in batch runs even though the API input validates and implies support.
Useful? React with 👍 / 👎.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## canary #13790 +/- ##
=========================================
Coverage 66.56% 66.57%
=========================================
Files 2026 2026
Lines 171978 171985 +7
Branches 17519 20805 +3286
=========================================
+ Hits 114484 114493 +9
+ Misses 57370 57368 -2
Partials 124 124
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…tToolsEngine Renames and separates two orthogonal concerns that used to share the misleading `isDesktopClient` name: - `hasClientExecutor` — caller itself can receive `tool_execute` over the Agent Gateway WS (Phase 6.4). Property of the caller. - `hasDeviceProxy` — server has a device-proxy configured that tunnels to a separately registered device (legacy Remote Device). Property of the server. `platform` is now derived from the caller (`clientRuntime`) first, falling back to the device-proxy signal for backwards compat — it was previously derived purely from the server's proxy config, which conflated "server can reach a desktop" with "caller is a desktop". LocalSystem enable rule restructured to read in natural order: runtimeMode === 'local' // user opted in && hasDeviceProxy // server has a Gateway path && (hasClientExecutor || ...) // an execution target exists Behavior is identical to the previous commit; this is a pure rename / regrouping refactor. 38 existing tests still pass without changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… gate
The previous rule required `hasDeviceProxy` as a shared prerequisite for
BOTH enable paths, which is wrong: `hasDeviceProxy` reflects the legacy
device-proxy (`deviceProxy.isConfigured`), while Phase 6.4's
`tool_execute` rides the Agent Gateway WebSocket that this request is
already on. The two systems are orthogonal — a desktop caller on the
Gateway WS can receive `tool_execute` without any device-proxy being
configured server-side.
Correct enable rule:
runtimeMode === 'local'
&& (hasClientExecutor // Phase 6.4, self
|| (hasDeviceProxy && deviceOnline && autoActivated)) // legacy
Updated the `still requires gateway to be configured` test, which was
asserting the incorrect coupling, to instead verify that agent-level
`runtimeMode.desktop === 'none'` opt-out is respected for desktop
callers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend half of LOBE-7076 (Phase 6.4). Pairs with server PR #13790, which adds the `clientRuntime` signal + `hasClientExecutor` gate so `local-system` and stdio MCP can enter the manifest for desktop callers. Data flow, client side: Agent Gateway WS └─ tool_execute event ──► AgentStreamClient └─ 'agent_event' ──► gatewayEventHandler (case 'tool_execute') └─ internal_executeClientTool (fire-and-forget) ├─ parse args → params ├─ mark pendingClientToolExecutions[toolCallId] ├─ dispatch: builtin → invokeExecutor, │ else → mcpService.invokeMcpToolCall ├─ clear pending └─ AgentStreamClient.sendToolResult(...) └─ WS → /api/agent/tool-result → LPUSH → server BLPOP unblocks → loop continues Key guarantees: - `internal_executeClientTool` never throws; ALL error paths (parse failure, no executor match, thrown executor, missing connection, MCP error) still call `sendToolResult({ success: false, error })`. The server's BLPOP must never hang on a silent client. - `case 'tool_execute'` uses `void`, not `await`. A long-running tool must not block subsequent `stream_chunk` / `tool_end` events on the same WebSocket. - UI loading state is kept separate from `toolCallingStreamIds` (the LLM-streaming animation) via a dedicated `pendingClientToolExecutions: Record<toolCallId, true>` map, so a renderer can show a distinct "running on device" indicator without entangling existing selectors. Client → server signal: `executeGatewayAgent` now passes `clientRuntime: isDesktop ? 'desktop' : 'web'` so the server knows this Electron caller can receive `tool_execute`. Tests: 39 new cases across AgentStreamClient / internal_executeClientTool / gatewayEventHandler covering success, error, MCP fallback, pending state lifecycle, and fire-and-forget semantics. 148 total in affected suites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend half of LOBE-7076 (Phase 6.4). Pairs with server PR #13790, which adds the `clientRuntime` signal + `hasClientExecutor` gate so `local-system` and stdio MCP can enter the manifest for desktop callers. Data flow, client side: Agent Gateway WS └─ tool_execute event ──► AgentStreamClient └─ 'agent_event' ──► gatewayEventHandler (case 'tool_execute') └─ internal_executeClientTool (fire-and-forget) ├─ parse args → params ├─ mark pendingClientToolExecutions[toolCallId] ├─ dispatch: builtin → invokeExecutor, │ else → mcpService.invokeMcpToolCall ├─ clear pending └─ AgentStreamClient.sendToolResult(...) └─ WS → /api/agent/tool-result → LPUSH → server BLPOP unblocks → loop continues Key guarantees: - `internal_executeClientTool` never throws; ALL error paths (parse failure, no executor match, thrown executor, missing connection, MCP error) still call `sendToolResult({ success: false, error })`. The server's BLPOP must never hang on a silent client. - `case 'tool_execute'` uses `void`, not `await`. A long-running tool must not block subsequent `stream_chunk` / `tool_end` events on the same WebSocket. - UI loading state is kept separate from `toolCallingStreamIds` (the LLM-streaming animation) via a dedicated `pendingClientToolExecutions: Record<toolCallId, true>` map, so a renderer can show a distinct "running on device" indicator without entangling existing selectors. Client → server signal: `executeGatewayAgent` now passes `clientRuntime: isDesktop ? 'desktop' : 'web'` so the server knows this Electron caller can receive `tool_execute`. Tests: 39 new cases across AgentStreamClient / internal_executeClientTool / gatewayEventHandler covering success, error, MCP fallback, pending state lifecycle, and fire-and-forget semantics. 148 total in affected suites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend half of LOBE-7076 (Phase 6.4). Pairs with server PR #13790, which adds the `clientRuntime` signal + `hasClientExecutor` gate so `local-system` and stdio MCP can enter the manifest for desktop callers. Data flow, client side: Agent Gateway WS └─ tool_execute event ──► AgentStreamClient └─ 'agent_event' ──► gatewayEventHandler (case 'tool_execute') └─ internal_executeClientTool (fire-and-forget) ├─ parse args → params ├─ mark pendingClientToolExecutions[toolCallId] ├─ dispatch: builtin → invokeExecutor, │ else → mcpService.invokeMcpToolCall ├─ clear pending └─ AgentStreamClient.sendToolResult(...) └─ WS → /api/agent/tool-result → LPUSH → server BLPOP unblocks → loop continues Key guarantees: - `internal_executeClientTool` never throws; ALL error paths (parse failure, no executor match, thrown executor, missing connection, MCP error) still call `sendToolResult({ success: false, error })`. The server's BLPOP must never hang on a silent client. - `case 'tool_execute'` uses `void`, not `await`. A long-running tool must not block subsequent `stream_chunk` / `tool_end` events on the same WebSocket. - UI loading state is kept separate from `toolCallingStreamIds` (the LLM-streaming animation) via a dedicated `pendingClientToolExecutions: Record<toolCallId, true>` map, so a renderer can show a distinct "running on device" indicator without entangling existing selectors. Client → server signal: `executeGatewayAgent` now passes `clientRuntime: isDesktop ? 'desktop' : 'web'` so the server knows this Electron caller can receive `tool_execute`. Tests: 39 new cases across AgentStreamClient / internal_executeClientTool / gatewayEventHandler covering success, error, MCP fallback, pending state lifecycle, and fire-and-forget semantics. 148 total in affected suites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: receive and execute executor=client tools on desktop Electron Frontend half of LOBE-7076 (Phase 6.4). Pairs with server PR #13790, which adds the `clientRuntime` signal + `hasClientExecutor` gate so `local-system` and stdio MCP can enter the manifest for desktop callers. Data flow, client side: Agent Gateway WS └─ tool_execute event ──► AgentStreamClient └─ 'agent_event' ──► gatewayEventHandler (case 'tool_execute') └─ internal_executeClientTool (fire-and-forget) ├─ parse args → params ├─ mark pendingClientToolExecutions[toolCallId] ├─ dispatch: builtin → invokeExecutor, │ else → mcpService.invokeMcpToolCall ├─ clear pending └─ AgentStreamClient.sendToolResult(...) └─ WS → /api/agent/tool-result → LPUSH → server BLPOP unblocks → loop continues Key guarantees: - `internal_executeClientTool` never throws; ALL error paths (parse failure, no executor match, thrown executor, missing connection, MCP error) still call `sendToolResult({ success: false, error })`. The server's BLPOP must never hang on a silent client. - `case 'tool_execute'` uses `void`, not `await`. A long-running tool must not block subsequent `stream_chunk` / `tool_end` events on the same WebSocket. - UI loading state is kept separate from `toolCallingStreamIds` (the LLM-streaming animation) via a dedicated `pendingClientToolExecutions: Record<toolCallId, true>` map, so a renderer can show a distinct "running on device" indicator without entangling existing selectors. Client → server signal: `executeGatewayAgent` now passes `clientRuntime: isDesktop ? 'desktop' : 'web'` so the server knows this Electron caller can receive `tool_execute`. Tests: 39 new cases across AgentStreamClient / internal_executeClientTool / gatewayEventHandler covering success, error, MCP fallback, pending state lifecycle, and fire-and-forget semantics. 148 total in affected suites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: pass server operationId to tool_result dispatch (operationId mismatch) The gateway event handler received `tool_execute` events but the resulting `internal_executeClientTool` call looked up `gatewayConnections` by the *local* operation id (e.g. `op_8chrnd`) instead of the *server-side* operation id (e.g. `op_1776171452938_...`) the WS connection is actually keyed on. `conn` was therefore always `undefined`, the early-return in `send(...)` swallowed the response, and the server's BLPOP waiter timed out after 60 s. This was reproducible on canary E2E: server logs showed `dispatching client tool lobe-local-system/readLocalFile` followed by `client tool ... timed out after 60027ms`, with no outbound `tool_result` frame ever reaching the Agent Gateway. Fix: thread a distinct `gatewayOperationId` through `createGatewayEventHandler` and use it for the `case 'tool_execute'` dispatch. The existing `operationId` (used for `dispatchContext` → `internal_dispatchMessage` keying) is untouched. Both `executeGatewayAgent` and `reconnectToGatewayOperation` now pass the server id explicitly; when a caller omits it, it falls back to the local `operationId` for backwards compatibility. Verified live on canary: WS now shows `[in] tool_execute` → `[out] tool_result success=true content=...` and the agent returns the real local-file contents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# 🚀 LobeHub v2.1.50 (20260416) **Release Date:** April 16, 2026\ **Since v2.1.49:** 107 commits · 101 merged PRs · 13 contributors > This weekly release focuses on improving runtime stability and gateway execution consistency, while making Home/Recents workflows faster to navigate and easier to manage in daily use. --- ## ✨ Highlights - **Server-side Human Approval Flow** — Agent runtime now supports more reliable approve/reject/reject-continue handling in gateway mode, reducing stalled execution paths in long-running tasks. (#13829, #13863, #13873) - **Message Gateway End-to-End Hardening** — Gateway message flow, queue handling, tool callback routing, and stop interruption behavior were strengthened for better execution continuity. (#13761, #13816, #13820, #13815) - **Client Tool Execution in Gateway Mode** — Client-executor tools now run more predictably across gateway and desktop callers, with improved executor dispatch behavior. (#13792, #13790) - **Home / Recents / Sidebar Upgrade** — Sidebar layout, custom sort, recents operations, and profile actions were improved to reduce navigation friction in active sessions. (#13719, #13812, #13723, #13739, #13878, #13734) - **Agent Workspace and Documents Expansion** — Working panel and agent document workflows were expanded and polished for better day-to-day agent operations. (#13766, #13857) - **Provider and Model Compatibility Improvements** — Added GLM-5.1 support and refined model/provider edge-case handling, including schema and error-path fixes. (#13757, #13806, #13736, #13740) --- ## 🏗️ Core Agent & Architecture ### Agent runtime and intervention lifecycle - Added server-side human approval and improved runtime coordination across approve/reject decision paths. (#13829, #13863) - Improved interrupted-task handling and operation lifecycle consistency to reduce half-finished runtime states. (#13714) - Refined error classification and payload propagation so downstream surfaces receive clearer actionable errors. (#13736, #13740) ### Execution model and dispatch behavior - Introduced executor-aware runtime behavior to better separate client/server tool execution semantics. (#13758) - Improved tool/plugin resolution and manifest handling to avoid runtime failures on malformed inputs. (#13856, #13840, #13807) --- ## 📱 Gateway & Platform Integrations - Added message gateway support and strengthened queue/error behavior for more stable cross-channel execution. (#13761, #13816, #13820) - Improved gateway callback pipeline with protocol and API additions for `tool_execute` / `tool_result`. (#13762, #13764, #13765) - Improved bot/channel reliability and DM/slash handling in Discord-related paths. (#13805, #13724) --- ## 🖥️ CLI & User Experience - Improved CLI reliability across message/topic operations and build/minify-related paths. (#13731, #13888) - Added image-to-video options and improved command behavior for generation workflows. (#13788) - Improved desktop runtime behavior for remote fetch and Linux notification urgency handling. (#13789, #13782) --- ## 🔧 Tooling - Extracted gateway stream client into `@lobechat/agent-gateway-client` to centralize protocol usage and reduce duplication. (#13866) - Improved built-in tool coverage and runtime support, including GTD server runtime and missing lobe-kb tools. (#13854, #13876) - Updated skill and frontmatter consistency in workflow tooling. (#13730) --- ## 🔒 Security & Reliability - **Security:** Strengthened API key WS auth behavior and safer serverUrl forwarding in gateway-related auth paths. (#13824) - **Reliability:** Reduced runtime stalls by improving gateway stop/interrupt and approval-state routing behavior. (#13815, #13863, #13873) - **Reliability:** Added defensive guards for malformed tool manifests and non-string content edge cases. (#13856, #13753) --- ## 👥 Contributors **101 merged PRs** from **13 contributors** across **107 commits**. ### Community Contributors - @arvinxx - Runtime, gateway, and execution reliability improvements - @Innei - Navigation, workflow UX, and desktop/CLI refinements - @rdmclin2 - Sidebar, recents, and channel behavior updates - @ONLY-yours - Tooling/runtime fixes and model execution compatibility - @tjx666 - Model support and release/tooling maintenance - @nekomeowww - Memory and search-path stability fixes - @cy948 - CLI indexing and command flow fixes - @octo-patch - Local system runtime edge-case fixes - @djthread - Desktop runtime request reliability improvements - @rivertwilight - Documentation and changelog updates - @sudongyuer - Subscription/mobile support improvements - @Zhouguanyang - Provider/model configuration correctness fixes - @lobehubbot - Translation and maintenance automation support --- **Full Changelog**: v2.1.49...v2.1.50
Background
Part of LOBE-7076 (Phase 6.4 — Gateway 客户端 Tool Calling 前端实现). The server side of Phase 6 is already in place:
toolExecutorMapstatically derived from manifestRuntimeExecutors.call_toolroutesexecutor === 'client'→GatewayStreamNotifier.sendToolExecute→ Agent Gateway WS → clientThe one remaining gap:
local-system(and stdio MCP) could only enter the agent's tool manifest when a separately registered remote device was online + auto-activated (legacy device-proxy flow). So a desktop Electron caller sitting on the other end of the Agent Gateway WS — the very target Phase 6.4 wants to dispatch to — never sawlocal-systemin its manifest and never got atool_executepushed to it.What this PR does
Adds a new, orthogonal signal: the caller tells the server what runtime it is.
ExecAgentParams.clientRuntime?: 'desktop' | 'web'— when a caller sends'desktop', the server knows this request is coming in over an Agent Gateway WebSocket from an Electron client that can executeexecutor: 'client'tools locally.AgentToolsEnginethen enableslocal-systemunder one of two independent conditions:Once
local-systemis in the manifest,toolExecutorMap[local-system] = 'client'kicks in (LOBE-7067), andRuntimeExecutors.call_tooldispatches it over the Gateway WS to the caller (LOBE-7068).Two orthogonal capability flags
hasClientExecutortool_executeover the Agent Gateway WSclientRuntime === 'desktop'hasDeviceProxydeviceProxy.isConfiguredThese are independent. The Phase 6.4 WS dispatch has nothing to do with device-proxy — the WS used for
tool_executeis the same one streaming agent events, so as long as the caller is connected, the channel is live.User opt-in is honored
local-systemonly enables whenruntimeMode === 'local', which resolves toagentConfig.chatConfig.runtimeEnv.runtimeMode[platform]— the user's per-agent per-platform runtime-env preference. SettingruntimeMode.desktop = 'none'keepslocal-systemoff even for desktop callers.Changes
packages/types:ExecAgentParams.clientRuntime?: 'desktop' | 'web'src/server/routers/lambda/aiAgent.ts: tRPC schema accepts + forwards the fieldsrc/server/services/aiAgent/index.ts: forwards it tocreateServerAgentToolsEnginesrc/server/modules/Mecha/AgentToolsEngine:clientRuntimeparamplatformnow derived from the caller (clientRuntime) first, falling back tohasDeviceProxyfor backwards compat — previously it was only derived from the server's proxy config, which conflated "server can reach a desktop" with "caller is a desktop"isDesktopClient(which actually meant "server has device-proxy") tohasDeviceProxyhasClientExecutoras a first-class sibling flagOut of scope (follow-up PR)
The frontend side of Phase 6.4, already implemented locally:
AgentStreamClient.sendToolResulton the WebSocket clientinternal_executeClientToolaction: parses args → invokes local executor → always sendstool_resultback (including on every error path, so the server's BLPOP never hangs)gatewayEventHandlerroutestool_executeevents as fire-and-forget to the actionexecuteGatewayAgentsendsclientRuntime: isDesktop ? 'desktop' : 'web'— the signal this PR consumesTest plan
AgentToolsEnginetests pass — includes 3 new cases for the desktop/web/opt-out branchesexecAgent.deviceToolPipelinetests pass — includes 3 new cases verifyingclientRuntimeis forwarded verbatim🤖 Generated with Claude Code