Commit graph

10007 commits

Author SHA1 Message Date
Arvin Xu
4203e32dc7
♻️ refactor: createAgent uses agentModel.create directly (#13871)
* ♻️ refactor: createAgent uses agentModel.create directly

The createAgent router was still going through sessionModel.create,
which is a legacy path that doesn't pass all agent fields (like
agencyConfig) to the agents table. Switch to agentModel.create
which directly inserts into the agents table with full field support.

- Add CreateAgentSchema in types package for proper input validation
- Remove dependency on insertAgentSchema from database package
- Remove sessionId from CreateAgentResult

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🏷️ chore: mark session-based agent creation as deprecated

Add @deprecated JSDoc tags to the legacy session-based agent creation
path (session router, SessionService, SessionModel.create, session store,
insertAgentSchema). New code should use agent.createAgent / agentModel.create
directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: honor groupId when creating agents

Pass input.groupId as sessionGroupId to agentModel.create so that
agents created from a sidebar folder are correctly assigned to that group.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve type errors from createAgent refactor

- Remove sessionId fallback in AddAgent.tsx and ForkAndChat.tsx
- Use z.custom<T>() for agencyConfig and tts in CreateAgentSchema
  to match agentModel.create parameter types

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-16 13:10:13 +08:00
LobeHub Bot
9583de88e3
🌐 chore: translate non-English comments to English in desktop-controller-tests (#13867)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:02:18 +08:00
LiJian
0699a0b5ce
🐛 fix: improve the skill execution error body back (#13868)
fix: improve the skill execution error body back
2026-04-16 11:43:01 +08:00
Arvin Xu
dd81642d83
♻️ refactor: extract agent-stream into @lobechat/agent-gateway-client package (#13866)
* ♻️ refactor: extract agent-stream into @lobechat/agent-gateway-client package

Move the Agent Gateway WebSocket client from src/libs/agent-stream/ into
a standalone workspace package at packages/agent-gateway-client/. This
eliminates the duplicate AgentStreamEvent type in apps/cli and provides
a single source of truth for the Gateway WS protocol types shared by
SPA, server, and CLI consumers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* add agent-gateway-client

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:25:32 +08:00
Arvin Xu
f6c70210f2
♻️ refactor(chat): remove reject-only button, unify to rejected_continue (#13865)
* ♻️ refactor(chat): remove reject-only button, unify to rejected_continue

Server-side `decision='rejected'` and `decision='rejected_continue'`
share the exact same code path — both surface the rejection to the
LLM as user feedback. Having a separate "reject only" button added UI
complexity without behavioural difference.

- Remove the "仅拒绝" button from InterventionBar popover; the single
  "拒绝" button now calls `rejectAndContinueToolCall` directly
- `rejectToolCalling` Gateway branch sends `rejected_continue` instead
  of `rejected` so all rejection paths use one decision value

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update ApprovalActions.tsx

*  feat(tool): add executors field to BuiltinToolManifest and dispatch page-agent to client

Add `executors?: ('client' | 'server')[]` to `BuiltinToolManifest` so
each builtin tool declares where it can run. The server-side dispatch
logic in `aiAgent/index.ts` now reads this field instead of hardcoding
per-identifier checks.

- `lobe-local-system`: `executors: ['client', 'server']` — runs on
  client via Electron IPC or server via Remote Device proxy
- `lobe-page-agent`: `executors: ['client']` — requires EditorRuntime,
  client-only
- Stdio MCP plugins still use the `customParams.mcp.type` heuristic
  (not manifest-driven)

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-16 10:45:17 +08:00
Arvin Xu
8109bbbbc3
🐛 fix(gateway): route approve/reject via lab flag (#13863)
🐛 fix(gateway): route approve/reject via lab flag, not transient server op state

After the coordinator fix for `waiting_for_human` (#13860) the paused
`execServerAgentRuntime` op is marked `completed` client-side as soon
as the server emits `agent_runtime_end`. `startOperation` then runs
`cleanupCompletedOperations(30_000)`, which deletes any op completed
more than 30 seconds ago — so by the time the user sees the
InterventionBar and clicks approve/reject, the running (or recently
completed) server op is gone.

The previous `#hasRunningServerOp` check therefore kept returning
false against a live Gateway backend, flipping approve/reject into
the client-mode `internal_execAgentRuntime` branch and stranding the
server-side paused conversation.

Switch the helper to `#shouldUseGatewayResume`, which checks the same
`isGatewayModeEnabled()` lab flag used to route the initial send. The
signal now mirrors how the conversation was dispatched and survives
the op-cleanup window.

New regression test exercises the post-coordinator-fix state: the
paused `execServerAgentRuntime` op is explicitly `completed` before
the approve call runs, and we still expect the Gateway branch to
fire with `decision='approved'`.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:26:22 +08:00
Arvin Xu
1005f442d6
🐛 fix(gateway): clean up paused server op after human approve/reject (#13860)
* 🐛 fix(gateway): clean up paused server op after human approve/reject

In Gateway mode with userInterventionConfig.approvalMode='ask', the
paused execServerAgentRuntime op was never released — the loading
spinner kept spinning after the user approved, rejected, or
reject-and-continued, and reject-only silently did nothing on the
server.

- ToolAction.rejectToolCall now delegates to chatStore.rejectToolCalling
  so the Gateway resume op actually fires with decision='rejected';
  previously it only mutated local intervention state and the server's
  paused op waited forever.
- AgentRuntimeCoordinator treats waiting_for_human as end-of-stream so
  the coordinator emits agent_runtime_end when request_human_approve
  flips state, letting the client close the paused op via the normal
  terminal-event path.
- conversationControl adds #completeRunningServerOps as a fallback
  guard in the approve/reject/reject-continue Gateway branches — if
  the server-side signal is delayed or missing, the client still clears
  the orphan op before starting the resume op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(gateway): defer paused-op cleanup until resume starts successfully

If `executeGatewayAgent` failed (transient network/auth/server error),
the paused `execServerAgentRuntime` op was already marked completed
locally by the pre-call `#completeRunningServerOps`. Retries would
then see no running server op, miss `#hasRunningServerOp`, and fall
through to the non-Gateway client-mode path — while the backend was
still paused awaiting human input.

Snapshot the paused op IDs before the resume call and retire them
only inside the try block after `executeGatewayAgent` resolves. On
failure the running marker stays intact so a retry still lands on
the Gateway branch and can re-issue the resume.

The helper was renamed from `#completeRunningServerOps(context)` to
`#completeOpsById(ids)` to reflect the new contract: callers must
snapshot beforehand, not re-query at completion time (which would
incorrectly match the new resume op too).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(gateway): avoid double reject dispatch in reject-and-continue

Now that `rejectToolCall` delegates to `chatStore.rejectToolCalling`,
the chained `await get().rejectToolCall(...)` inside
`rejectAndContinueToolCall` fired a full halting reject before the
continue call. In Gateway mode that meant two resume ops on the same
tool_call_id (`decision='rejected'` followed by
`decision='rejected_continue'`) racing server-side; in client mode it
duplicated reject bookkeeping that `chatStore.rejectAndContinueToolCalling`
already handles internally.

Drop the chained call and fire `onToolRejected` inline so hook
semantics are preserved. `chatStore.rejectAndContinueToolCalling` is
now the single entry point for both the rejection persist and the
continue dispatch.

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-16 01:43:00 +08:00
Arvin Xu
2cf65e9fb3
💄 style: agent documents (#13857)
* improve style

* improve style
2026-04-16 01:05:27 +08:00
Arvin Xu
6636b35188
🐛 fix: drop manifests missing api before feeding ToolsEngine (#13856)
🐛 fix(toolEngineering): drop manifests missing `api` before feeding ToolsEngine

`ToolsEngine.convertManifestsToTools` calls `manifest.api.map(...)`
without a null check, so any manifest that is truthy but lacks a valid
`api` array crashes the entire tools build with "Cannot read properties
of undefined (reading 'map')". This takes down anything that touches
the tools pipeline on that agent — including TokenTag in ChatInput,
which is why users see the crash on the chat page load path.

Manifests are merged from 5 sources (installed plugins, builtin tools,
Klavis, LobeHub skills, caller-supplied extras), only some of which
filter falsy entries, and none validate `api`. Guard defensively at
the merge point and log the offending source + identifier so the
underlying bad data can be traced.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:04:55 +08:00
Arvin Xu
8475bc11fc
🐛 fix(builtin-tool-gtd): add server runtime for GTD tool (#13854)
*  feat(builtin-tool-gtd): add server runtime for GTD tool

Implement server-side execution runtime so the GTD tool works when
agents run in a pure server context (bot platforms, async task workers,
QStash workflows). Previously only the client executor existed, which
relied on `useNotebookStore` and `notebookService` and would break on
the server.

- `packages/builtin-tool-gtd/src/ExecutionRuntime/index.ts`: pure
  `GTDExecutionRuntime` class with an injected service interface,
  covering createPlan/updatePlan/createTodos/updateTodos/clearTodos
  and execTask/execTasks. Since server runtime has no stepContext,
  todo state is read from / written back to the Plan document's
  `metadata.todos` field.
- `src/server/services/toolExecution/serverRuntimes/gtd.ts`: factory
  wiring `DocumentModel` + `TopicDocumentModel` into the runtime and
  registering under `GTDIdentifier`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor(builtin-tool-gtd): share runtime logic between executor and server

Make the client executor a thin adapter over `GTDExecutionRuntime` so
all processing logic (todo reducer, plan CRUD flow, execTask state
builder, output formatting) lives in one place. Previously the server
runtime was a near-duplicate of the client executor.

- Expand `GTDRuntimeContext` with `currentTodos`, `messageId`, `signal`
  so both callers can thread their environment through:
  - client supplies `currentTodos` from stepContext / pluginState via
    `getTodosFromContext`, and `messageId` for execTask parentMessageId
  - server lets the runtime resolve todos from the plan document's
    metadata when `currentTodos` is not supplied
- Split service surface into `updatePlan` (user-facing: goal / desc /
  context — client routes through `useNotebookStore` to refresh SWR)
  vs `updatePlanMetadata` (silent todos sync — client stays on the
  raw `notebookService`)
- Runtime methods now return `BuiltinToolResult` (superset of
  `BuiltinServerRuntimeOutput`), so `stop: true` on execTask /
  execTasks is typed cleanly without `@ts-expect-error`

Net effect: `executor/index.ts` shrinks from 510 → 134 lines; the
server factory just maps models to the service interface.

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-16 00:04:48 +08:00
LobeHub Bot
3bb4fd6046
🌐 chore: translate non-English comments to English in lambda-router-tests (#13838)
🌐 chore: translate non-English comments to English in lambda router tests

Translated all Chinese/CJK comments to English in 6 test files under
src/server/routers/lambda/__tests__/. Code logic and string literals
are unchanged; only explanatory comments were translated.

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 23:59:08 +08:00
Arvin Xu
9608494b0a
💄 style(chat): tighten execServerAgentRuntime loading copy (#13855)
💄 style(chat): tighten `execServerAgentRuntime` loading copy

Current text was trying to do too much in one line — status + two
separate user affordances — and read as an explanation, not a status.
Replaces it with a status-first line that mentions where the work is
happening and the single reassurance users actually need.

- EN: "Task is running in the server. You are safe to leave this page."
- zh-CN: "任务正在服务器运行,您可以放心离开此页面。"

Only en-US and zh-CN are edited; CI translates the rest from the
default file.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:42:37 +08:00
Innei
bc7b798dbb
🐛 fix(conversation): improve workflow display when user intervention is pending (#13847)
* 🐛 fix(conversation): improve workflow display when user intervention is pending

Made-with: Cursor

* 🐛 fix(builtin-tool-activator): add ActivatedToolInfo typing to requestedTools for tsgo compat

requestedTools was inferred as `{ identifier, name }[]` which lacks the
`avatar` property required by `ActivatedToolInfo`, causing tsgo errors.
2026-04-15 23:30:34 +08:00
Arvin Xu
986bd2f7ec
🐛 fix(agent-runtime): fetch tool plugin from message_plugins for resumeApproval (#13852)
`messageModel.findById(parentMessageId)` only returns the row from the
`messages` table — the tool-call metadata (identifier / apiName /
arguments / type / toolCallId) lives in the separate `message_plugins`
table. The resumeApproval path was reading `(resumeParentMessage as any).plugin`
and `(resumeParentMessage as any).tool_call_id`, both always undefined,
which meant:

- Approved tool calls were dispatched with `identifier: undefined`,
  causing the server-side tool executor to throw
  `Builtin tool "undefined" is not implemented`. The follow-up LLM
  step could still describe success (it sees the user prompt + picks
  plausible output) but the tool message content is permanently the
  error string.
- The toolCallId mismatch guard was silently disabled because the
  stored value was always null → validation always passed regardless
  of what the client sent.

Fix: query `messagePlugins.findFirst` by message id, use the fetched
row for both the toolCallId equality check and the approvedToolCall
payload that the runtime dispatches.

Tests:
- Mock `db.query.messagePlugins.findFirst` with the plugin fields so
  existing asserts on `approvedToolCall.identifier`/`apiName` pass
  against real values.
- Move `tool_call_id` / identifier / apiName / arguments / type out of
  the mock `messages` row fixture into a separate `pendingToolPlugin`
  fixture that mirrors the actual DB layout.
- Flip the "toolCallId mismatch" guard test to mutate the plugin mock
  (not the message mock) — this is exactly the class of bug the fetch
  guards against, so the test would have masked it before.
- New guard test: throw when `messagePlugins.findFirst` returns
  undefined (stale message id, wrong user, etc.).

Discovered during E2E verification of LOBE-7152 approve flow — the
approve decision was flipping to the new op correctly but every tool
execution was failing with the "undefined" error.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:53:50 +08:00
Arvin Xu
843cb8f30b
🔨 chore: wire server-mode human approval through conversationControl (#13830)
 feat(chat): server-mode human approval via new Gateway op + resumeApproval

When the current agent runtime is Gateway-mode (execServerAgentRuntime),
approve / reject / reject_continue now start a **new** Gateway op carrying
a `resumeApproval` decision instead of resuming the paused op in place
over tRPC — mirroring the "interrupt + new op" pattern from LOBE-7142
(stop/interrupt). This sidesteps the stepIndex / executeStep early-exit
race that was blocking the in-place resume path and matches the Linear
spec for LOBE-7152. Client mode is unchanged.

### Client

- `conversationControl.ts`
  - `approveToolCalling` / `rejectToolCalling` / `rejectAndContinueToolCalling`:
    server-mode branch calls `executeGatewayAgent({ message: '',
    parentMessageId: toolMessageId, resumeApproval: { decision, ... } })`.
    The local runtime never spins up; the new op's `agent_runtime_end`
    clears loading.
  - `#hasRunningServerOp` replaces the old `#getServerOperationId` helper
    (we no longer need the paused op's id). Forwards scope/groupId/
    subAgentId from `ConversationContext` into the operation lookup so
    group/thread conversations correctly resolve their running server op
    — `operationsByContext` is keyed on the full `messageMapKey`.
- `gateway.ts` — `executeGatewayAgent` takes an optional `resumeApproval`
  and forwards it to `aiAgentService.execAgentTask`.
- `services/aiAgent.ts` — `ExecAgentTaskParams.resumeApproval` with new
  `ResumeApprovalParam` shape (decision + parentMessageId + toolCallId
  + optional rejectionReason).
- `gatewayEventHandler.ts` — kept the `toolMessageIds` branch that fetches
  pending tool messages on `tools_calling`.
- `services/agentRuntime/{type,index}.ts` — removed the short-lived
  `toolMessageId` / `reject_continue` additions; this flow no longer
  routes through `processHumanIntervention`.
- `store/chat/slices/operation/selectors.ts` — `getOperationsByContext` /
  `hasRunningOperationByContext` now take `MessageMapKeyInput` so scope/
  group/subAgent fields are honoured end-to-end.

### Server

- `ExecAgentSchema` / `InternalExecAgentParams.resumeApproval` — optional
  `{ decision, parentMessageId, rejectionReason?, toolCallId }`.
- `AiAgentService.execAgent`
  - `resumeApproval` implies resume semantics (skip user-message creation,
    reuse `parentMessageId` as the target tool message). Folded into a
    single `effectiveResume` flag so the existing resume branches apply.
  - Validates parent is a `role='tool'` message whose `tool_call_id`
    matches the request — guards stale / double-clicks.
  - Writes the decision to DB before `historyMessages` is fetched so the
    runtime sees the updated tool message on the first step:
    * `approved` → `intervention: { status: 'approved' }`
    * `rejected` / `rejected_continue` → tool content =
      "User reject this tool calling [with reason: X]",
      `intervention: { status: 'rejected', rejectedReason }`.
  - Branches initial runtime context:
    * `approved` → `phase: 'human_approved_tool'` + `approvedToolCall`
      payload rebuilt from the tool message plugin → runtime executes
      the tool.
    * `rejected` / `rejected_continue` → `phase: 'user_input'` with
      empty content → LLM re-reads history (now including the rejected
      tool) and responds. Both decisions share this path: the client
      split is only about optimistic writes and button UX; once the
      rejection is persisted there's nothing meaningful to differentiate
      server-side.

### Tests

- `conversationControl.test.ts` — rewrote the three server-mode blocks
  to spy `executeGatewayAgent` and assert the `resumeApproval` payload
  shape. Added a regression test covering group-scope lookup so dropping
  scope/groupId from `#hasRunningServerOp` breaks the suite.
- `execAgent.resumeApproval.test.ts` (new) — covers approved and the
  unified rejected branches (parameterized), the no-reason fallback, and
  the role/tool_call_id validation guards.

Relates to LOBE-7152.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:17:22 +08:00
Arvin Xu
75626de0b3
🐛 fix: forward serverUrl in WS auth for apiKey verification (#13824)
* 🐛 fix: forward serverUrl in WS auth for apiKey verification

The agent gateway verifies an apiKey by calling
\`\${serverUrl}/api/v1/users/me\` with the token, so \`serverUrl\` has to be
part of the WebSocket auth handshake. The device-gateway-client already
does this; \`lh agent run\` was missing it, producing
"Gateway auth failed: Missing serverUrl for apiKey auth".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔨 chore: bump cli to 0.0.7

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-15 21:16:11 +08:00
Innei
ad634daf32
🧹 chore(onboarding): remove builtin-agent-onboarding and consolidate prompts (#13825)
* 🧹 chore: remove builtin-agent-onboarding and consolidate web onboarding

- Merge agent system role into builtin-agents; colocate toolSystemPrompt in builtin-tool-web-onboarding
- Drop unused QuestionRenderer client bundle
- Gate onboarding footer switch/skip on AGENT_ONBOARDING_ENABLED for agent route

Made-with: Cursor

* 🧪 test: fix onboarding layout translation mock

* 🧪 test: align onboarding layout test with feature flag

* 🧪 test: type onboarding business const mock
2026-04-15 20:40:37 +08:00
Arvin Xu
f99935e992
🐛 fix(agent-runtime): carry persisted assistant id into state.messages (#13841)
When `call_llm` pushed the assistant turn into `state.messages`, it
dropped the DB id even though the row was already persisted. The
downstream `request_human_approve` executor filters parent lookup on
`m.role === 'assistant' && m.id`, and the DB fallback query is not
reliably finding the just-written row on every topology — so when
human-approve fires on the fresh LLM turn the op errors out with
"No assistant message found as parent for pending tool messages".

Attach `assistantMessageItem.id` to the pushed message so the existing
in-memory lookup hits, and nextContext's `parentMessageId` and
`state.messages` agree on a single source of truth.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:02:40 +08:00
Neko
632a6383f0
feat(app): working panel, and many agent document feat (#13766) 2026-04-15 19:18:24 +08:00
LiJian
15fcce97c9
♻️ refactor: add more tools in lobe-agent-manangerment(modify、update、delete) (#13842)
* feat: add more tools in lobe-agent-manangerment

* feat: add the ensureAgentLoaded to modify it

* feat: add the update prompt tools
2026-04-15 17:57:05 +08:00
Neko
e5be1801a1
🐛 fix(userMemories,database): bm25 should escape more characters like < and > (#13843) 2026-04-15 17:30:32 +08:00
Innei
64fc6d4bbd
feat(database): add document history table and update related models
- Introduced a new `document_histories` table to track changes made to documents, including fields for `editor_data`, `save_source`, and `saved_at`.
- Updated foreign key relationships to link `document_histories` with `documents` and `users`.
- Modified existing models and tests to accommodate the new document history functionality, including changes to pagination and retrieval methods.
- Removed the versioning system from documents in favor of a more flexible history tracking approach.

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-15 14:39:37 +08:00
LiJian
524e07540c
🐛 fix: update the builtin lobehub skill manifest (#13840)
* fix: update the lobehub skill manifest

* fix: remove the no use code

* fix: update the test
2026-04-15 13:24:57 +08:00
Arvin Xu
9f61b58a29
feat(agent-runtime): server-side human approval flow (#13829)
*  feat(agent-runtime): implement server-side human approval flow

Port the client-mode human approval executors (request_human_approve,
call_tool resumption, handleHumanIntervention) to the server agent
runtime so that execServerAgentRuntime can correctly pause on
waiting_for_human and resume on approve / reject / reject_continue.

- request_human_approve now creates one `role='tool'` message per pending
  tool call with `pluginIntervention: { status: 'pending' }` and ships
  the `{ toolCallId → toolMessageId }` mapping on the `tools_calling`
  stream chunk.
- call_tool gains a `skipCreateToolMessage` branch that updates the
  pre-existing tool message in-place (prevents duplicate rows / parent_id
  FK violations that show up as LOBE-7154 errors).
- AgentRuntimeService.handleHumanIntervention implements all three
  paths: approve → `phase: 'human_approved_tool'`; reject → interrupted
  with `reason: 'human_rejected'`; reject_continue → `phase: 'user_input'`.
- ProcessHumanIntervention schema carries `toolMessageId` and a new
  `reject_continue` action; schema remains permissive (handler no-ops on
  missing toolMessageId) to keep legacy callers working.

Fixes LOBE-7151

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): address LOBE-7151 review (P1 reject_continue, P2 duplicate tool msg)

P1 — reject_continue with remaining pending tools must NOT resume the LLM.
Previously `handleHumanIntervention` kept `status='waiting_for_human'` but
returned `nextContext: { phase: 'user_input' }`, which `executeStep` would
hand to `runtime.step` immediately, breaking batch semantics. Now when
other tools are still pending, the rejection is persisted but no context
is returned; the `user_input` continuation only fires when this is the
last pending tool.

P2 — request_human_approve was pushing an empty placeholder
`{ role: 'tool', tool_call_id, content: '' }` into `newState.messages`
to "reflect" the newly-created pending DB row. On resume, the `call_tool`
skip-create path appends the real tool result, leaving two entries for
the same `tool_call_id` in runtime state. The downstream short-circuit
(`phase=human_approved_tool` → `call_tool`) doesn't consult
state.messages, so the placeholder was unused cost. Removed.

Also fixes a TS 2339 in the skipCreateToolMessage test where
`nextContext.payload` is typed `{}` and needed an explicit cast.

Tests: 99 pass (82 RuntimeExecutors + 17 handleHumanIntervention), type-check clean.
Verified end-to-end via the human-approval eval — it now exercises a
multi-turn retry path (LLM calls the gated tool twice) and both
approvals resolve cleanly through to `completionReason=done`.

Relates to LOBE-7151

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* pin @react-pdf/renderer

* 🐛 fix(deps): pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg

@react-pdf/image@3.1.0 (auto-resolved via layout@4.6.0 ← renderer@4.4.1)
declares `@react-pdf/svg@^1.1.0` as a dependency, but the svg package was
unpublished/made private on npm (returns 404). CI installs blow up with
ERR_PNPM_FETCH_404.

Upstream issue: https://github.com/diegomura/react-pdf/issues/3377

Pin image to 3.0.4 (the last release before the broken svg dep was
introduced) via pnpm.overrides until react-pdf publishes a fix.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:07:06 +08:00
Arvin Xu
f12cf8f2ea
🐛 fix: fail fast when tool/assistant message persist hits a missing parent (#13828)
* 🐛 fix: fail fast when tool/assistant message persist hits a missing parent

When a conversation parent was deleted mid-operation (LOBE-7154), the
runtime was silently swallowing the parent_id FK violation in three tool
persist paths and continuing with a stale parentMessageId. The next LLM
call hit the same FK without context, surfacing as a raw SQL error to
the user after burning several LLM + tool call round trips.

Changes

- packages/types: add AgentRuntimeErrorType.ConversationParentMissing
- new messagePersistErrors.ts helper: FK detection + structured error
  constructor + persist-fatal marker (keeps RuntimeExecutors smaller)
- RuntimeExecutors:
  - call_tool: publish error event + re-throw on persist failure;
    outer catch propagates when persist-fatal
  - call_tools_batch: same, mark so the per-tool outer catch doesn't
    swallow and fall back to the already-deleted parent
  - resolve_aborted_tools: same pattern
  - call_llm: preflight parent existence via findById so we fail before
    the LLM call instead of after
- tests: replace old swallow-on-fail expectations, add LOBE-7158 cases
  for each executor plus focused unit tests for the helper module

Fixes LOBE-7158

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 chore: publish normalized ConversationParentMissing on persist failure

Review feedback on LOBE-7158: the three persist catches were emitting
the raw DB exception as a stream `error` event before normalizing it.
Clients treat `error` events as terminal and surface `event.data.error`
directly, so the raw SQL text leaked to users and ended the stream
before the typed `ConversationParentMissing` throw could propagate.

Move normalization ahead of the publish in call_tool, call_tools_batch,
and resolve_aborted_tools so the stream event always carries the
intended business error. Add a regression assertion on the
call_tool FK test that the error event's `errorType` is
`ConversationParentMissing` and no `Failed query` text leaks through.

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-15 09:27:01 +08:00
Arvin Xu
1a98e1b5aa
💄 style(nav-panel): remove nav panel content switch animation (#13814)
Drop the `motion/react` slide + fade transition on NavPanel content
switches (e.g. navigating from `/` to `/agent`). The new content now
renders directly without the 0.28s x-translate animation.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:10:37 +08:00
Innei
b4fc85b57b
💄 style(proxy-settings): sticky pill SaveBar + instant enable toggle (#13821)
* 🔖 chore(release): release version v2.1.49 [skip ci]

* 💄 style(proxy-settings): sticky pill SaveBar + instant enable toggle

- Split enableProxy into instant-apply (no save required)
- Floating pill SaveBar fixed bottom-center, visible only when dirty
- Test connection feedback moved to toast (@lobehub/ui)
- Refresh style guidance: prefer createStaticStyles + cssVar

Fixes LOBE-7071

* 🐛 fix(proxy-settings): rollback enable toggle on save failure, preserve in-progress edits

---------

Co-authored-by: lobehubbot <i@lobehub.com>
2026-04-15 00:05:00 +08:00
Rdmclin2
fd0d846975
feat: support layout custom sort and fix copy (#13812)
* fix: menu locale keys

* feat: support resort sidebar

* feat: add lock to middle messages

* feat: add memory menu and default hidden

* fix: lint error

* fix: legacy secion order

* chore: add test cases

* chore: remove top zone

* feat: custom sidebar reorder

* chore: fix sidebar items
2026-04-14 23:49:47 +08:00
Arvin Xu
41efd16bba
🔨 chore: update cli version (#13822)
update cli version
2026-04-14 23:37:28 +08:00
Arvin Xu
f6081c9914
🔨 chore: add headless approval and apiKey WS auth to lh agent run (#13819)
 feat: add headless approval and apiKey ws auth to `lh agent run`

Two fixes so `lh agent run` works end-to-end against the WebSocket agent
gateway when the user is authenticated via LOBEHUB_CLI_API_KEY.

- Default to `userInterventionConfig: { approvalMode: 'headless' }` when
  running the agent from the CLI. Without this flag the runtime waits
  for human tool-call approval and local-device commands hang forever.
  Users who want interactive approval can pass `--no-headless`.
- Pass `tokenType` (`jwt` | `apiKey`) in the WebSocket auth handshake so
  the gateway knows how to verify the token. Previously the CLI sent
  only the raw token value and the gateway assumed JWT, rejecting valid
  API keys.

Fixes LOBE-6939

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:28:01 +08:00
Arvin Xu
d6f11f80b6
🐛 fix(agent-runtime): harden classifyLLMError so it never masks the original provider error (#13774)
* 🐛 fix(agent-runtime): harden classifyLLMError so it never masks the original provider error

Production traces across multiple providers (openrouter, openai, google)
surface a single opaque error — `e.trim is not a function` with
`errorType: 'unknown'` — hiding whatever the upstream actually returned.

Root cause: `normalizeCode` / `normalizeErrorType` assumed their input is
always `string | undefined` (matching the TypeScript signature), but real
provider error objects frequently carry a numeric `code` (HTTP status) or
a structured object in `errorType`. `value?.trim()` short-circuits only
on null/undefined, so a truthy non-string turns into a TypeError that
the outer catch records as the "final" error, erasing the upstream one.

Fixes:
- Guard `normalizeCode` / `normalizeErrorType` on `typeof value ===
  'string'`, widen parameter type to `unknown`.
- Wrap the whole `classifyLLMError` in a try/catch that falls back to a
  conservative `stop` decision and preserves the best-effort message of
  the ORIGINAL error. A classifier that throws is worse than a
  classifier that's wrong — it must never shadow the real failure.
- `bestEffortMessage` swallows property-access errors (hostile Proxy
  etc.) to guarantee the fallback itself can't throw.

Regression tests cover: numeric `code`, structured `errorType`, nested
OpenAI-SDK-shaped `error.error.code`, and a hostile Proxy that throws on
every property access.

This is a forcing function for root-cause diagnosis: after this lands,
the real upstream errors behind the 'e.trim' mask will finally surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove fallback warning in classifyLLMError

Removed console warning for classification failure.

* 🐛 fix(agent-runtime): treat numeric provider code as status fallback

Bare HTTP proxies sometimes surface the HTTP status ONLY as a numeric `code`
on the error object (no `status`/`statusCode`, no digits in the message).
After widening `normalizeCode` to require `typeof === 'string'`, those numeric
codes were dropped entirely and auth/permission failures fell through to
retry — wasting the full retry budget on permanent errors.

Forward numeric `raw.code` / `nested?.code` / `nestedError?.code` into the
status chain (after the real status/statusCode lookups, before the
message-digit extractor) so classifyKind still maps 401/403 → stop and
429/5xx → retry.

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 23:23:21 +08:00
Rdmclin2
1c75686b70
🐛 fix: gateway typing error (#13820)
fix: gateway typing error
2026-04-14 23:15:41 +08:00
Arvin Xu
7e89fa782d
🐛 fix: detect truncated tool_calls arguments in builtin tools (#13817)
* 🐛 fix: detect truncated tool_call arguments in builtin tools

When an LLM hits max_tokens mid tool_call, the arguments JSON is
truncated. The previous flow passed `{}` to the tool, which returned a
generic "required field missing" error; the model re-tried with the same
payload and the truncation repeated — one observed trace burned 17 min
and $2.46 on 5 blind retries.

Detect structural truncation (unclosed braces/brackets/strings) in
BuiltinToolsExecutor before schema validation, and return a dedicated
TRUNCATED_ARGUMENTS error telling the model to reduce payload size or
raise max_tokens instead of retrying.

Fixes LOBE-7148

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 chore: echo raw arguments string and reject all unparseable JSON

Two improvements based on review:

- Append the received arguments string to the error content so the model
  can verify the payload is exactly what it produced (stops it from
  blaming upstream or guessing what went wrong).
- Treat ANY unparseable non-empty argsStr as an error (new code
  INVALID_JSON_ARGUMENTS), not just truncation. The previous fallback
  of passing `{}` to the tool produced generic "missing field" errors
  that hid the real cause. Empty argsStr still falls through to `{}`
  for tools that take no parameters.

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 22:50:49 +08:00
Arvin Xu
18bc2716b2
🔨 fix: wire Gateway-mode stop via direct tRPC interrupt (#13815)
*  feat: wire Gateway-mode stop button to WS interrupt

Frontend half of [LOBE-7142](https://linear.app/lobehub/issue/LOBE-7142)
— the stop button previously silently failed in Gateway mode because:

1. `stopGenerateMessage` only filtered `execAgentRuntime`, so
   `execServerAgentRuntime` ops (Gateway) were skipped.
2. Even if the local op got cancelled, nothing bridged the cancel to
   the server-side agent loop running behind the Agent Gateway WS.

## Changes

**`conversationControl.ts::stopGenerateMessage`** — extend the type
filter to include both op types so both client-side and Gateway-mode
runs are cancelled from the same entry point.

**`gateway.ts::executeGatewayAgent` + `reconnectToGatewayOperation`** —
register an `onOperationCancel` handler on the local `gatewayOpId` that
forwards the server-side operation id to `interruptGatewayAgent(...)`,
which sends `{ type: 'interrupt' }` over the Agent Gateway WS. The
closure cleanly resolves the "local op id vs server op id" mapping —
no metadata lookup needed.

**`operation/actions.ts::cancelOperation`** — `isAborting` flag was
gated on `execAgentRuntime`. Extend to `execServerAgentRuntime` too so
the UI loading state transitions out immediately on Gateway-mode stop,
without waiting for the round-trip `session_complete` from the server.

## What this doesn't do (follow-ups)

- **Backend**: new `POST /api/agent/interrupt` route + Redis LPUSH
  (LOBE-7145). Without it, the WS interrupt reaches Agent Gateway but
  never gets forwarded to cloud.
- **Agent loop**: `AgentRuntimeService.executeStep` LPOP polling of the
  interrupt key (LOBE-7146). Without it, the state never flips to
  `interrupted` server-side.
- **Agent Gateway DO** (external repo): `_forwardInterrupt` HTTP POST
  from the WS interrupt handler (LOBE-7147).

With only this PR merged, clicking stop will clear the local UI state
and send the WS frame correctly — the server-side loop keeps running
until those three are merged too.

## Tests

- `conversationControl.test.ts`: +1 — stopGenerateMessage cancels
  `execServerAgentRuntime`, invokes the onCancel handler, sets
  `isAborting: true`.
- `gateway.test.ts`: +1 — `executeGatewayAgent` registers a handler
  against the local opId, handler invokes `interruptGatewayAgent`
  with the server opId.

All 123 touched-slice tests pass; type-check clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔨 chore: switch Gateway stop to direct tRPC instead of WS roundtrip

Rewiring only — no new behaviour on top of the previous commit. See
the discussion in PR #13815 for the full reasoning.

TL;DR the WS-based path (client → Agent Gateway WS → DO forwards
HTTP → cloud route → Redis LPUSH → loop LPOP) has the same end-effect
as the tRPC-direct path (client → tRPC → AgentRuntimeService
.interruptOperation → DB state flip), except:

- the tRPC path is one hop instead of three
- the tRPC path reuses infrastructure that's *already on canary* —
  `aiAgentService.interruptTask` → `AiAgentService.interruptTask` →
  `AgentRuntimeService.interruptOperation` → `coordinator.saveAgentState`
  with status='interrupted' — and the existing step-boundary polling
  in `executeStep` (AgentRuntimeService.ts:474, 565) already picks it up
- zero new server code required; zero Agent Gateway (external repo)
  coordination required

The only reason the WS path was in the original spec (LOBE-7142) was
symmetry with the Phase 6.4 tool_execute/tool_result path, but
`interrupt` is a one-shot control signal, not stream data — there's
no actual benefit to routing it through the same channel. Mid-step
abort would require threading an AbortSignal into `runtime.step(...)`,
which WS doesn't help with either.

Closes out the need for LOBE-7145 / LOBE-7146 / LOBE-7147.

Changes:
- `gateway.ts`: both `executeGatewayAgent` and
  `reconnectToGatewayOperation` register the cancel handler against
  the local op id, but the handler body now calls
  `aiAgentService.interruptTask({ operationId: serverOpId })` via
  tRPC instead of `this.interruptGatewayAgent(serverOpId)` (which sent
  the WS interrupt frame).
- `gateway.test.ts`: adjust the one new test case to verify the
  tRPC call rather than the WS-path spy; add `interruptTask` to the
  `aiAgentService` mock.

`AgentStreamClient.sendInterrupt()` and `interruptGatewayAgent()` are
kept as-is — public API, might be useful elsewhere. Just not called
from the cancel handler anymore.

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 22:41:45 +08:00
Rdmclin2
636a3b77c3
🐛 fix: message gateway queue error (#13816)
* fix: gateway sync

* fix: skip  error connection

* feat: add disconnect all &  MESSAGE_GATEWAY_ENABLED env vairable

* chore: add gateway test case

* chore: clean lobehub connnections when switch to message gateway

* chore: optimize disconnect all

* chore: disconnect gateway connnections when using lobehub gateway

* chore: clean up exsiting gateway connections after reconnect and avoid gateway callback when not enabled
2026-04-14 22:10:17 +08:00
Arvin Xu
c70ac84da7
feat: support run client tools in agent gateway mode (#13792)
*  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>
2026-04-14 21:30:13 +08:00
LiJian
116495bd1e
🐛 fix: slove the execAgents tools exec types not correct (#13807)
* fix: slove the execAgents tools exec types not correct

* fix: should inject source:discovery when tools type is lost

* fix: delete the source inject test
2026-04-14 17:51:08 +08:00
LiJian
922f7ace41
🐛 fix: fixed the when call saveCreds the bad request problem (#13809)
* fix: fixed the when call saveCreds the bad request problem

* fix: add the empty kv checked
2026-04-14 17:51:00 +08:00
YuTengjing
b369c53bda
🐛 fix(model-bank): disable GLM-5.1 built-in search in LobeHub (#13806) 2026-04-14 17:05:42 +08:00
René Wang
5ecccf4b9e
📝 docs: add April 13 weekly changelog (#13808) 2026-04-14 17:02:10 +08:00
Rdmclin2
f9fbd45fee
feat: discord support slash commands and DM (#13805)
* fix: slack not respond to text commands

* feat: add slack slash commands instructions

* chore: add slack validate in test connections

* chore: update slack docs

* chore: remove text commands for slack
2026-04-14 16:48:16 +08:00
LiJian
0b490a7268
🐛 fix: execAgent should get builtin discoverable tools into manifests (#13804)
* fix: execAgent should get all tools manifests

* fix: should add the tools source into payload source

* fix: add the discoverable tools into tools enginer

* fix: update the test, should include the discoverable tools
2026-04-14 16:07:49 +08:00
Innei
a9c5badb80
♻️ refactor(navigation): stable navigate hook and imperative routing (#13795)
*  fix: implement stable navigation hook and refactor navigation handling

- Introduced `useStableNavigate` hook to provide a stable `navigate` function that can be used across the application.
- Refactored components to utilize the new stable navigation approach, replacing direct access to the navigation function from the global store.
- Updated `NavigatorRegistrar` to sync the `navigate` function into a ref for consistent access.
- Removed deprecated navigation handling from various components and actions, ensuring a cleaner and more maintainable codebase.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: refactor navigation handling to prevent state mutation

- Updated navigation reference handling in the global store to use a dedicated function for creating navigation refs, ensuring that the initial state is not mutated by nested writes.
- Adjusted tests and components to utilize the new navigation ref creation method, enhancing stability and maintainability of navigation logic.

Signed-off-by: Innei <tukon479@gmail.com>

*  test: mock Electron's net.fetch in unit tests

- Added a mock for Electron's net.fetch in the AuthCtr and BackendProxyProtocolManager tests to ensure proper handling of remote server requests.
- This change allows tests to simulate network interactions without relying on the actual fetch implementation, improving test reliability.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-14 13:28:12 +08:00
LiJian
cd0f65210c
♻️ refactor: update the codesandbox systemRole(preinstalled_software) (#13799)
refactor: update the codesandbox systemRole(preinstalled_software)
2026-04-14 12:11:44 +08:00
Arvin Xu
24be35fd84
🐛 fix(agent-runtime): resolve S3 image keys when refreshing messages (#13794)
messageModel.query() calls inside RuntimeExecutors were missing a
postProcessUrl callback, so imageList/videoList/fileList entries retained
raw S3 keys (e.g. `files/user_xxx/icon.png`). After the first tool batch,
the refreshed state fed those raw keys straight into the next LLM call,
and providers like Anthropic reject anything that isn't an absolute URL or
data URI ("Invalid image URL"). Wire a lazy FileService-backed
postProcessUrl into all three query sites (topic reference resolution,
compression, and post-batch refresh) so imageLists stay resolved across
multi-step operations.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:29:49 +08:00
Arvin Xu
46adf43453
🐛 fix: dispatch executor=client tools to desktop callers when DEVICE_GATEWAY is configured (#13793)
🐛 fix: dispatch executor=client tools to desktop caller even with DEVICE_GATEWAY configured

Two fixes to make Phase 6.4 (LOBE-7076) actually reach a desktop caller on
canary, where DEVICE_GATEWAY is configured and a separate remote device
may be registered.

### 1. AgentToolsEngine: suppress RemoteDevice for desktop callers

The `lobe-remote-device` tool is meant for the legacy "tunnel commands to
a separately registered desktop" flow. When the caller itself is a
desktop Electron client, that's redundant — and worse, the LLM was
picking `listOnlineDevices` + `activateDevice` *first*, then routing the
subsequent `readLocalFile` to a different registered host (a remote
Linux VM in our E2E trace, returning ENOENT for a path that only exists
on the caller).

Adds `&& !hasClientExecutor` to the RemoteDevice enable rule. Desktop
callers now see only `local-system` in their manifest.

### 2. aiAgent.execAgent: mark executor='client' for desktop callers

The existing gate was `if (!gatewayConfigured) { executorMap[...] = 'client' }`.
On canary, `gatewayConfigured === true` (DEVICE_GATEWAY set), so
`local-system` / stdio MCP stayed server-executed and were dispatched to
the Remote Device proxy instead of back to the caller's Agent Gateway WS.

Extends the gate to:
  `if (clientRuntime === 'desktop' || !gatewayConfigured)`

So a caller that explicitly signals it can receive `tool_execute` bypasses
the DEVICE_GATEWAY heuristic. Legacy behaviour unchanged for web callers
and for callers that don't send `clientRuntime`.

### Tests

- AgentToolsEngine: +1 case verifying RemoteDevice is suppressed when
  `clientRuntime === 'desktop'` even with `gatewayConfigured: true`
- execAgent.deviceToolPipeline: +3 cases
  - local-system gets executor='client' for desktop + DEVICE_GATEWAY
  - stdio MCP gets executor='client' for desktop + DEVICE_GATEWAY
  - web caller preserves legacy routing (executor unset)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:22:18 +08:00
Arvin Xu
f0a811ef83
🐛 fix: enable executor=client tools for desktop Electron callers (#13790)
*  feat: enable executor=client tools for desktop Electron callers

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>

* ♻️ refactor: untangle runtime / platform / device-proxy flags in AgentToolsEngine

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>

* 🐛 fix: decouple hasClientExecutor from hasDeviceProxy in local-system 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>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:12:45 +08:00
Arvin Xu
10914ff015
🐛 fix: add image-to-video options to CLI generate video command (#13788)
*  feat: add image-to-video options to CLI generate video command

Why: CLI only supported text-to-video. Backend already accepts imageUrl/endImageUrl
for image-to-video, but the CLI had no way to pass them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update cli version

* update cli version

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:12:10 +08:00
Innei
b9c4b87a90
🩹 fix(database): enforce document history ownership and pagination 2026-04-14 00:51:13 +08:00
Adam Bellinson
b857ae6c57
🐛 fix(desktop): use Electron net.fetch for remote server requests (#13400)
* use Electron's net.fetch() so system trusted certs are honored

* 🐛 fix(tests): mock netFetch in unit tests broken by net.fetch migration

Both LocalFileCtr and RemoteServerConfigCtr tests were patching
global.fetch / stubGlobal, which no longer intercepts calls now that
the controllers route through Electron's net.fetch via @/utils/net-fetch.
Hoist the fetch mock and point vi.mock('@/utils/net-fetch') at it directly.
2026-04-14 00:45:54 +08:00