* 🐛 fix(agent-runtime): always persist assistant reasoning to DB
PR #13494 gated message reasoning persistence behind preserveThinking
(agent chatConfig + model extendParams / qwen|zhipu fallback). That gate
is only meant to control whether reasoning is replayed into the next LLM
payload — applying it to the DB write dropped thinking content for every
non-qwen/zhipu reasoning model in server-side agent mode: reasoning
streamed live via stream_end but vanished after refresh.
Restore unconditional reasoning persistence in messageModel.update and
keep the preserveThinking gate only for state.messages payload replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 💄 style(i18n): localize callSubAgent tool labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✅ test(model-runtime): align tool-calling fallback tests with new return shape
#15680 changed generateObject's tool-calling fallback to return the parsed
schema object (same shape as the json_schema path) instead of an array of
tool calls, and reworked its error handling, but left the pre-existing
"tool calling fallback" block in index.test.ts asserting the old behavior,
breaking CI on canary:
- result is now the parsed object, not [{ name, arguments }]
- the no-tool-call path returns undefined via debug log without console.error
- the parse-failure path logs the single matched tool call, not the array
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(model-runtime): surface missing tool call in generateObject fallback as error
tool_choice forces the structured-output function, so a response without a
tool call means the provider misbehaved. #15680 routed this branch to a
debug-namespace log that is invisible in production, leaving callers with
an unexplained undefined. Log it via console.error with the response
message as context, matching the parse-failure branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat: add browser device pairing flow to /settings/devices
- Add "Via Browser" tab to ConnectDeviceModal with pairing code display and input
- Add "Register this browser as a device" callout card above DeviceList
- Support ?pair=<code> URL param to auto-open browser pairing modal with pre-filled code
- Improve DeviceList empty state with method cards (Desktop + CLI)
- Ship en-US and zh-CN i18n keys for all new browser/sync strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🔨 fix(devices): fix lint warnings — import sort order and empty catch block
* fix(devices): add pair API route and invalidate device list cache
- Create /api/devices/pair POST handler that authenticates the user via
Better Auth session, validates the code against the user's registered
devices via DeviceModel.findByDeviceId, and returns JSON.
- Replace the setListKey/key-prop re-mount trick with
lambdaQuery.useUtils().device.listDevices.invalidate() so the tRPC
React Query cache is properly busted after a successful pair (fixes
staleTime: 30s preventing the new device from appearing).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ♻️ refactor(devices): drop browser pairing, fix modal close, redesign UI
- Remove the "Via Browser" pairing flow entirely: browser tab in
ConnectDeviceModal, the "register this browser" callout card, the
?pair=<code> deep-link, and the /api/devices/pair stub route. Only the
real Desktop and CLI connection methods remain.
- Fix the modal that couldn't be closed: @lobehub/ui Modal closes via
onCancel (antd), not onClose — the X button was a no-op.
- Redesign the connect modal (segmented tabs, numbered steps, command
blocks with copy, security footer) and the empty state (onboarding
hero with Desktop/CLI options + capability cards).
- Clean up browser/sync i18n keys; add capabilities + footer keys for
en-US and zh-CN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 💄 fix(devices): apply card radius — cssVar.borderRadius already has unit
The radius tokens (cssVar.borderRadius / borderRadiusLG) already include
their unit, so the trailing `px` produced `var(--…)px`, which browsers
drop — leaving the cards with sharp corners. Drop the `px` so the cards
pick up the same rounded radius as the appearance settings FormGroup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- add resolveExecutionPlan as THE device decision (none/sandbox never
route to a device; offline bindings stay unrouted; single-online-device
auto-activation only for device-capable targets)
- fix executionTarget=none being bypassed by single-device auto-activation
(background runs executed device tools despite 无设备)
- stop exposing the remote-device proxy in none/sandbox sessions
- converge native execAgent, hetero dispatch fork and client
selectRuntimeType onto the shared resolution
- drop the legacy per-platform chatConfig.runtimeEnv.runtimeMode fallback
entirely (no migration: unset targets resolve to platform defaults)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- actions/checkout@v4 -> @v6 in issue-auto-comments.yml
(last remaining @v4 usage; all other 48 uses are already @v6)
- actions/github-script@v7 -> @v8 in release-desktop-canary.yml
(last remaining @v7 usage; all other 4 uses are already @v8)
Co-authored-by: 章岚 <zhanglan@datagrand.com>
* ✨ feat(model-bank): backfill knowledgeCutoff batch 2 and restore lost Anthropic values
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 📝 docs(skills): add model-bank-metadata skill for cutoff/family backfill
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(model-bank): Claude Fable 5 belongs to the claude-mythos family
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 💄 style(desktop): always surface the tab bar by creating a tab on first navigation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ♻️ refactor(model-bank): family is the product lineage (claude-opus/sonnet/haiku), not the brand
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): backfill activeAgentId before paint on tab/route switches
Tab switches are plain route navigations, so leaving an agent page cleared
activeAgentId via a passive useUnmount and the next page re-set it in a
passive useEffect — the first painted frame always had no active id, flashing
a skeleton even when agentMap already cached the config. Move both the
backfill and the unmount clear to layout effects: removed-tree layout
cleanups run before new-tree layout effects in one commit, so the clear can
never wipe a freshly synced id and the id is in place before paint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(agent): surface agent config fetch errors with a retry action
isAgentConfigLoading only knows "no data yet", so a failed fetch (e.g. a 401
that SWR deliberately does not retry, with no focus revalidation inside a
single Electron window) left the agent page on a skeleton forever — only a
manual reload recovered. Record per-agent fetch errors in
agentConfigErrorMap (set by onError, cleared on data / retry), expose
currentAgentConfigError / isAgentConfigError selectors, add a
retryAgentConfigFetch action that revalidates the agent's SWR entries, and
show an error alert with a retry button above the main chat input while the
config is still missing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(ci): sync model metadata test expectations
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(connector): support API key / custom header / OAuth auth in custom connector
Make the connector backend a full replacement for the legacy custom-MCP plugin form:
- connector create/update now accept bearer/apikey/header credentials (encrypted at rest);
oauth2 stays callback-only
- map apikey → bearer auth and header → request headers in both the sync path
(syncTools + callTool) and the agent-runtime manifest path
- pass custom HTTP headers through to the MCP client
- AddConnectorModal becomes a rich form: MCP type (HTTP/STDIO), auth type
(None / API Key / Custom Headers / OAuth), reusing the plugin form inputs;
OAuth keeps the existing popup authorize flow, others create + sync directly
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ♻️ refactor(connector): fold OAuth into the PluginDevModal MCP form
Pivot the custom-MCP entry to reuse the rich PluginDevModal / MCPManifestForm
instead of a bespoke connector modal, and add OAuth as an auth type inside it:
- MCPManifestForm: gated `enableOAuth` adds an "OAuth" auth type with
Client ID / Secret (optional) + redirect-URI hint. Only the custom-connector
entry enables it, so plain custom-plugin DevModal callers (editing plugins,
agent tools, …) are unaffected.
- DevModal: opens the OAuth popup synchronously on the save click (browsers
block window.open once an async boundary is crossed), validates, then hands
the popup to onSave which navigates it to the authorize URL.
- New CustomConnectorModal wraps DevModal and persists every auth type onto the
connector backend (none / bearer / custom headers → create + sync; OAuth →
create with OIDC config + run the authorize popup).
- settings/skill entry now opens CustomConnectorModal; the standalone
AddConnectorModal rich rewrite from the previous commit is reverted to the
canary original (it is only referenced by the unused ConnectorList).
- i18n: dev.mcp.auth.oauth* keys (default + en-US + zh-CN).
Backend stays as in the prior commit (connector create/update accept
bearer/apikey/header credentials; sync + manifest paths apply them).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(connector): route the OAuth auth type through the authorize flow, not the token-less manifest test
Selecting OAuth and clicking "Test connection" called the plugin manifest test
(getStreamableMcpServerManifest), which connects with no token and 401s on any
OAuth-gated server (e.g. Linear MCP / DCR). For OAuth there is nothing to test
without authorizing first, so the button now becomes "Authorize & Connect" and
runs the connector OAuth flow (discovery + DCR + authorize popup), shared with
the footer save button via DevModal.runOAuthFlow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(connector): make connector.create idempotent on (user, identifier)
Re-adding or re-authorizing a custom connector with an existing identifier hit
the user_connectors unique constraint and 500'd. Now an existing row is updated
(reset to disconnected, refreshed name/url/oidcConfig/credentials) and its id
reused, instead of inserting a duplicate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ♻️ refactor(skill-store): route Add Custom MCP through the connector modal, drop the Custom tab
- Skill Store "Add → Add Custom MCP Skill" now opens CustomConnectorModal
(connector backend + OAuth), matching the settings/skill entry, instead of
the legacy plugin DevModal (installCustomPlugin + togglePlugin).
- Remove the now-redundant "Custom" tab from the Skill Store (custom MCP lives
in the connector list now): drop SkillStoreTab.Custom, its tab option,
CustomList render, and the matching search branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ feat(model-bank): backfill knowledgeCutoff for OpenAI/Claude/Llama/Phi families (batch 1)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add family/generation fields with rule-derived data for chat models
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add canonical knowledge-cutoff map with build-time backfill
Adds MODEL_KNOWLEDGE_CUTOFFS (canonical id → YYYY-MM, all values verified
against official provider docs) plus normalizeModelIdForCutoff, which reduces
provider-specific spellings (openrouter/bedrock prefixes, dated snapshots,
-thinking/-fast/-latest/-preview variants, claude dot-versions) to canonical
ids. buildDefaultModelList backfills knowledgeCutoff from the map when a model
card has no inline value, so all aggregator providers inherit cutoffs
automatically; inline values always win.
Covers Anthropic (incl. legacy 3.x), OpenAI, Google Gemini/Gemma, xAI Grok,
Meta Llama, Amazon Nova, and Cohere. DeepSeek/Qwen/GLM/Kimi/MiniMax/Mistral
publish no official cutoffs and are intentionally absent. Anthropic inline
PoC entries migrate into the map (single source of truth).
Cross-checked against the batch-1 inline backfill: 0 value mismatches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(model-bank): correct Claude Sonnet 4.6 cutoff
* ✅ test(model-bank): sync metadata expectations
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add knowledgeCutoff field with Anthropic models as PoC
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add family/generation fields to model card types
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add claude-fable-5 to Anthropic models
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): allow adding directory topics on web when agent targets a bound device
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): deliver sub-agent resume bridge via QStash webhook in queue mode
The callSubAgent completion bridge was a handler-only hook, which lives in
process memory: in queue mode (AGENT_RUNTIME_MODE=queue) HookDispatcher only
delivers webhook-configured hooks, so the bridge never fired — the parent op
stayed parked in waiting_for_async_tool forever after all sub-agents finished.
- Give the bridge hook a webhook config (delivery: qstash) targeting the new
/api/agent/webhooks/subagent-callback endpoint; local mode keeps the
in-process handler. Both paths converge on
AgentRuntimeService.completeSubAgentBridge (backfill + barrier/CAS resume).
- Park-time self-check: after the parked state and operation row are
persisted, re-run the resume barrier once to recover children that
completed before the parent finished parking.
- One-shot verify watchdog: when a completion finds the parent not yet
resumable, schedule a delayed verifyAsyncToolBarrier re-check (no step
lock, CAS-idempotent, never re-arms).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 📝 docs(agent): correct verify-watchdog rationale comment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 📝 docs(agent): clarify eventFields trimming rationale
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ♻️ refactor(agent): align subagent-callback with workspace-scoped step worker
Post-rebase adaptation to canary's runtime restructure (#15609):
- Route the webhook bridge through AiAgentService (like the /run step
worker) so the runtime's models stay workspace-scoped — a bare
AgentRuntimeService would be personal-scoped and the tool-message
backfill / resume barrier could miss workspace-scoped rows.
- Extract SubAgentBridgeParams into agentRuntime/types and add the
completeSubAgentBridge passthrough next to executeStep.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): fail sub-agent callback loudly on backfill or delivery failure
Address two review findings on the resume bridge:
- completeSubAgentBridge now checks updateToolMessage's { success } result
(it swallows transaction errors instead of throwing) and propagates all
infrastructure failures. The webhook endpoint then returns non-2xx so
QStash redelivers the whole bridge — previously a failed backfill was
acked with 200 and the parent stayed parked forever, since the verify
recheck only re-reads the barrier and cannot retry the backfill.
- New AgentHookWebhook.fallback: 'none' opts a qstash-delivered hook out of
the unsigned plain-fetch fallback, which can never authenticate against a
QStash-signed endpoint and only masked publish failures as silently
dropped 401s. The bridge hook uses it; dispatch escalates such delivery
failures to console.error instead of the debug namespace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(model-runtime): emit stop:abort instead of error when stream request is aborted
When user cancels a streaming request, the provider SDK throws abort errors
(e.g. "Request was aborted"). Previously these were propagated as error chunks,
causing the client to display a provider error message. Now abort errors emit
a stop:abort event through the SSE pipeline, allowing the client to handle
cancellation gracefully.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix(model-runtime): fix type error in abort pipeline test
Use `as const` for type literal to satisfy StreamProtocolChunk union type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✅ test(fetch-sse): add planUpgradeAfterFinish to onFinish expectations
#15616 added planUpgradeAfterFinish to the onFinish context but missed
updating fetchSSE.test.ts, breaking 13 tests on canary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(model-runtime): harden abort detection against non-Error throws
isAbortError assumed error.message is always a string, but catch
clauses receive unknown — a non-Error throw (string, object without
message) would make the abort check itself throw inside the stream
error handler, swallowing both ABORT_CHUNK and the first-chunk error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix(cli): handle agent_run_request in `lh connect` so device dispatch doesn't time out
`lh connect` auto-registers the CLI as a device, so the gateway can pick it
as the dispatch target for a heterogeneous agent run (`agent_run_request`).
But the connect daemon only listened for `system_info_request` and
`tool_call_request` — it never handled `agent_run_request`, so it never sent
`agent_run_ack`. The gateway waited out its ack window and returned
`{error:'TIMEOUT',success:false}`, surfaced server-side as "Hetero agent
device dispatch failed".
Add an `agent_run_request` handler mirroring the desktop app: spawn
`lh hetero exec` fire-and-forget and ack `accepted` immediately. The spawned
process owns the full execution + server-ingest pipeline. It re-invokes the
current CLI entry (process.execPath + argv[1]) rather than relying on `lh`
being on PATH, so it works inside the detached daemon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: bump the cli version
* chore: bump the cli manifest
* 🐛 fix(cli): ack agent run only after spawn succeeds, reject on spawn error
`child_process.spawn` reports a missing/inaccessible cwd asynchronously via
the child's `error` event, after the handler had already sent an `accepted`
ack. The gateway/server then recorded dispatch success while no `lh hetero
exec` process existed to emit `heteroFinish`, leaving the assistant message
stuck instead of surfacing a failure.
`spawnHeteroAgentRun` now resolves on the child's outcome: `accepted` on the
`spawn` event (stdin is written only then), `rejected` on an early `error`. A
rejected ack returns the gateway 422 → execAgent writes a ServerAgentRuntimeError
onto the assistant message, so a failed dispatch is visible. Still resolves in
milliseconds, well within the gateway's 10s ack window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🐛 fix: skill list/search commands returning empty results
tRPC endpoints return { data, total } but CLI was treating the result as
an array; switch to result?.data ?? [] and update mocks to match.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>