Skip to content

[pull] canary from lobehub:canary#363

Merged
pull[bot] merged 12 commits into
cresseelia:canaryfrom
lobehub:canary
Jun 15, 2026
Merged

[pull] canary from lobehub:canary#363
pull[bot] merged 12 commits into
cresseelia:canaryfrom
lobehub:canary

Conversation

@pull

@pull pull Bot commented Jun 15, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

arvinxx and others added 12 commits June 15, 2026 09:55
…s registry (#15850)

♻️ refactor(swr): converge remaining store-layer keys into swrKeys registry

Migrate all ad-hoc SWR keys still living in the store/service layer onto the
central registry (src/libs/swr/keys.ts), under the uniform `domain:resource`
naming. New domains: discover, eval (agent eval), ragEval, knowledgeBase,
device (incl. git), userMemory, agentKnowledge, agentBot, file, chatTool.

- Pure key convergence: no tiering/caching change. The new prefixes are kept
  deliberately OUT of CACHE_TIERS, so every migrated key stays memory-only
  exactly as before (agentKnowledge:/agentBot: avoid the cached `agent:` tier).
- Behavior preserved: key array shapes, mutate matchers (key[0] === *.root),
  and personal-vs-workspace match semantics are unchanged; string-join keys
  (discover assistant/social) become arrays with equivalent identity.
- UI-embedded SWR keys (features/routes/components/packages) intentionally left
  for a later pass.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…nalize (#15849)

The subagent run coordinator keys thread creation purely on the in-memory
`runs` map. On a cold serverless replica / BatchIngester retry the map is
empty, and `refreshSubagentRunsFromDb` only rehydrates `Processing` isolation
threads — a spawn that already finalized (thread flipped `Active`) is excluded.
So a replayed first-event for a finished subagent hits the `!existing` branch of
`ensureRun` and forks a SECOND thread with the identical title ("一模一样的两个
thread"). Sibling of #15838 (main-turn) / #15808 (subagent-turn), but for the
thread-create step.

Fix: give thread creation a DB-homed, status-independent idempotency guard keyed
by `sourceToolCallId`.
- `SubagentRunsState` gains `finalizedParents: Set<string>`; `finalizeRun`
  records the parent there (instead of just deleting the run), so `ensureRun`
  returns a no-op for a replayed finished spawn — no duplicate thread or message.
- `refreshSubagentRunsFromDb` seeds `finalizedParents` from this operation's
  `Active` isolation threads (without resurrecting them as live runs, which would
  mint empty assistants / re-finalize churn).

Regression: subagent reducer unit test (finalize → replay first event → 0
intents) + handler cold-replica test (finished subagent replay → still 1 thread).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ool/global/userMemory) (#15853)

♻️ refactor(swr): converge remaining discover/tool/global/userMemory store keys

Completes the store-layer SWR key convergence into the central registry
(batch3 only partially covered discover). Migrates the remaining ~39 ad-hoc
keys:

- discover: model/plugin/provider/skill/mcp/groupAgent list+detail+categories
  and user profile (the `.join('-')` string keys → registry array factories).
- tool: agentSkills, installedPlugins, builtin uninstalled-tools, lobehubSkill
  store, mcpPluginList, klavis store. (The dynamic `plugins`-array key is left
  as-is — it's data-derived, not a named key.)
- global: latest/server version, system status.
- userMemory: retrieve / memoryDetail / activities / contexts / experiences /
  identityList / preferences. The `purgeAllMemories` invalidation was rewritten
  from `startsWith('useFetch…')` string matchers to array `key[0] === *.root`
  matchers, in lockstep with the fetch keys.

No tiering/caching change: all new prefixes (discover/tool/global/userMemory)
are kept out of CACHE_TIERS, so everything stays memory-only as before.
Behavior preserved (key identity, mutate match sets, personal-vs-workspace).
UI-layer keys + the cross-layer `cronTopicsWithJobInfo` remain for the next PR.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ng back to unknown (#15831)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…issed wakeups (#15855)

* 🐛 fix(agent-runtime): harden async sub-agent suspend/resume against missed wakeups

The server callSubAgent async park/resume chain (#15481) had a one-shot,
no-retry recovery: a single transient miss left the parent stuck in
waiting_for_async_tool forever. Harden the resume barrier and watchdog
(LOBE-10385 parts 1-3, 5; the park-side deadline fallback follows separately):

- Read-your-writes barrier: completeSubAgentBridge passes the just-backfilled
  toolMessageId to the barrier, which trusts that local write instead of
  re-reading message_plugins from a possibly-stale read replica.
- Bounded backoff watchdog: verifyAsyncToolBarrier now re-arms with exponential
  backoff (15s→30s→60s→120s→240s, 5 attempts) until the barrier passes or the
  op is terminal, replacing the single 15s shot that never re-armed.
- Plug silent bails: !state and pending.length===0 now warn + emit a metric;
  the empty-pending case also arms a fallback verify for snapshot-persist lag.
- Observability: new agent_runtime_async_tool_resume_total counter keyed by
  outcome (resumed/barrier_held/no_pending/no_state/lost_cas/verify_exhausted)
  so missed wakeups surface instead of accumulating silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(hetero): reconstruct queued upload files from filesPreview on run continuation

When continuing a heterogeneous agent run with remaining queued messages, rebuild
the upload file items from filesPreview metadata instead of passing bare { id }
stubs, so file context (name/type/preview) survives the continuation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#15461)

* ✨ feat(composio): add Composio integration layer as Klavis replacement

- Add @composio/core SDK client factory (src/libs/composio)
- Add COMPOSIO_API_KEY server config + enableComposio flag
- Add COMPOSIO_APP_TYPES const with 21 curated apps (appSlug-based)
- Add lambda/composio tRPC router (createConnection, deleteConnection, getConnection, updateComposioPlugin)
- Add tools/composio tRPC router (executeAction, listActions, getActions)
- Add ComposioService with executeComposioTool + getComposioManifests
- Add composioStore Zustand slice (7 files: types, initialState, action, selectors, index, test)
- Wire composioStore into ToolStore state and action tree
- Add composioStoreSelectors to tool selectors index
- Add handleComposioInstall to AgentManagerRuntime
- Extend CustomPluginParams with composio field
- Add enableComposio to GlobalServerConfig types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔥 refactor(klavis): remove Klavis integration and migrate all references to Composio

- Delete all Klavis source files (libs, config, const, routers, services, store, UI components)
- Rename KlavisX components to ComposioX equivalents
- Replace all Klavis store selectors, types, and action names with Composio counterparts
- Fix authConfigId to be server-side managed (auto-fetch/create from Composio API)
- Update DB customParams.klavis → customParams.composio throughout
- Fix ToolSource type: 'klavis' → 'composio'
- Fix TaskTemplateSkillSource: 'klavis' → 'composio'
- Fix RecommendedSkillType.Klavis → RecommendedSkillType.Composio
- Remove klavis npm package dependency
- Update builtin-tool-creds: connectKlavisService → connectComposioService
- Update RuntimeExecutors: KLAVIS_SERVICES_LIST → COMPOSIO_SERVICES_LIST
- All Composio-related type errors: 0 remaining

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(composio): complete the klavis→composio migration and wire the OAuth callback

The composio branch had renamed the klavis modules but left consumers
half-migrated, so the OAuth connect link did not work end-to-end. Finish it:

- Add the missing OAuth callback route `/api/composio/oauth/callback` (Composio
  uses managed auth, so it only lands the user back and closes the popup; the
  opener then polls getConnection and syncs tools). Allowlist it as a public
  cross-site redirect landing in the proxy define-config.
- Remove leftover `import { type Klavis } from 'composio'` (non-existent package)
  and type the prop as `string`.
- Fix undefined `oauthUrl` → `redirectUrl` in every OAuth popup opener.
- Map `serverName` to `appSlug` (API) / `label` (display); unify every
  createComposioConnection call to `{ appSlug, identifier, label }`.
- Compare against the `ComposioServerStatus` enum instead of the `'ACTIVE'`
  string literal.
- Use the renamed store fields `composioServers` / `isComposioServersInit`.
- executeComposioTool: `toolName` → `toolSlug`.
- Rename onboarding `KlavisServerItem.tsx` → `ComposioServerItem.tsx` to match
  its import.

* 🐛 fix(composio): use connectedAccounts.link for Composio-managed OAuth

`connectedAccounts.initiate` is no longer supported for Composio-managed OAuth
auth configs (HTTP 400), which broke connecting apps like Gmail. Switch to
`connectedAccounts.link` (POST /api/v3/connected_accounts/link) — same
`{ callbackUrl }` options and `{ id, redirectUrl }` result, so it is a drop-in.

Also treat Composio's `status=failed` callback query param as a failed
authorization in the OAuth callback page.

* 🐛 fix(composio): correct tool sync, execution, callback build, and list dedup

Four fixes found while testing the Composio integration end-to-end:

- listActions: use `getRawComposioTools` (raw defs with slug/inputParameters)
  instead of `tools.get()` (provider-wrapped, name/params under `.function`).
  The wrapped shape left every synced tool with an empty name, so they all
  collapsed to `${identifier}____` and the LLM rejected the request with
  "Tool names must be unique."
- tools.execute: pass `dangerouslySkipVersionCheck: true` (manual execution
  otherwise throws ComposioToolVersionRequiredError when the toolkit version
  resolves to "latest"). Applied to both the executeAction router and the
  ComposioService used by the agent runtime.
- OAuth callback route: escape only `<`/`>`/`&` for the inline-script payload;
  the previous regex embedded literal U+2028/U+2029 line separators which broke
  the regex literal at build time ("Unterminated regular expression").
- installed-plugin selectors: filter out `customParams.composio` (was still
  checking the old `customParams.klavis`), so a connected Composio app no longer
  shows up twice in the skill picker / tool discovery list.

* ✨ feat(composio): pin auth config id per toolkit via env

Add `COMPOSIO_AUTH_CONFIG_IDS` (JSON map of `identifier -> authConfigId`) so a
pre-created Composio auth config (e.g. a custom/white-label OAuth app set up in
the dashboard) can be used directly per toolkit. `createConnection` now resolves
the pinned auth config first, then falls back to discovering an existing one for
the toolkit (matched case-insensitively), and only auto-creates a
Composio-managed config when nothing is configured.

* 🐛 fix(composio): update plugin invoke test to composio + sort tool initialState imports

- action.test.ts: the action was renamed invokeKlavisTypePlugin → invokeComposioTypePlugin
  (Klavis is being removed); update the test to call the composio action and drop
  the klavis-era naming/mock field.
- store/tool/initialState.ts: order the composioStore import before connector to
  satisfy simple-import-sort/imports.

* 🐛 fix(composio): stop client deleting remote connections by static allowlist

useFetchUserComposioConnections no longer deletes remote connections/plugins
for identifiers outside the compile-time COMPOSIO_APP_TYPES list — an outdated
client bundle would silently destroy a legitimate connection. Unknown
identifiers are now only hidden locally.

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

* 🐛 fix(composio): resolve connectedAccountId server-side in executeAction

executeAction now takes `identifier` and looks up the connectedAccountId from
the caller's own user-scoped plugin record (PluginModel), instead of trusting a
connectedAccountId supplied by the client — which would let a user drive
another user's connection. Callers (callComposioTool, composioExecutor) pass
identifier accordingly.

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

* 🐛 fix(composio): enable plugin only after OAuth succeeds

Move enablePluginForAgent into the ACTIVE and post-auth-success branches so a
cancelled/timed-out authorization no longer leaves an enabled-but-unauthorized
Composio tool on the agent.

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

* 🔥 fix(composio): drop dead OAuth callback postMessage

The lobe-composio-oauth postMessage had no consumer — the OAuth wait uses
polling + window.closed detection. Remove it and its escaping helpers.

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

* 🐛 fix(composio): resolve type-check errors after canary merge

- Guard authConfigId to a definite string before persisting/returning it
  (createConnection), fixing the string|undefined assignment in both the
  server router and the composio store server object.
- Replace leftover KLAVIS_SERVER_TYPES with COMPOSIO_APP_TYPES in AgentTool.
- Update SkillAuthRow test to a composio source/provider (klavis is removed
  from TaskTemplateSkillSource).

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

* ♻️ refactor(composio): remove leftover klavis naming after migration

Klavis is deprecated and fully replaced by Composio. The migration kept the
underlying composio wiring but left klavis-named identifiers, comments, prompt
tags, i18n keys, and files throughout. Sweep them to composio:

- Code identifiers/comments across ~70 files (isKlavisEnabled→isComposioEnabled,
  allKlavisServers→allComposioServers, klavisManifests→composioManifests, etc.)
- LLM prompt tags (<klavis_tools>→<composio_tools>, KLAVIS_SERVICES_LIST→
  COMPOSIO_SERVICES_LIST) — kept consistent across definition and substitution
- i18n keys tools.klavis.*→tools.composio.* + user-facing "Klavis"→"Composio"
  brand strings, in default setting.ts and all locale setting.json files
- Rename useKlavisOAuth→useComposioOAuth, useKlavisServerActions→
  useComposioServerActions (+ imports)
- klavis.ai homepage URLs → composio.dev
- Remove the dead `klavis` npm peerDependency; swap .env.example Klavis section
  for Composio; update product docs

Changelog history left untouched. Pure rename — no behavior change.

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

* 🐛 fix(composio): remove duplicate composio key in CustomPluginParams

The klavis→composio rename collapsed the deprecated klavis param block onto
the live composio one, producing a duplicate `composio` property. The klavis
shape (instanceId/serverName/serverUrl/isAuthenticated) is dead — no code reads
it — so drop it and keep the live composio shape.

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

* 🐛 fix(composio): let pending/errored connections re-authorize or be deleted

A Composio connection link (lk_...) expires after a while. Previously a
pending/errored row only offered to reopen the stored — now expired —
redirectUrl, and the delete action existed only for ACTIVE connections, so an
expired link left the tool permanently stuck: unauthenticatable and
unremovable.

- Add reauthorizeComposioConnection store action: best-effort delete the stale
  connection, then mint a fresh link (replaces the record in place)
- Settings skill item + chat toolbar item: PENDING/ERROR now render a ··· menu
  with Re-authorize (fresh link) and Delete
- Onboarding: pending/errored row click re-mints a fresh link instead of
  reopening the stale one
- i18n: add tools.composio.reauthorize (en-US + zh-CN)

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

* 🐛 fix(composio): return auth link instead of opening popup from agent

connectComposioService runs from the agent's response, which carries no user
gesture, so window.open was blocked by the browser and the flow always failed
with "Authorization was cancelled or timed out". Instead of opening the popup
ourselves, return the authorization redirectUrl in the tool result so the agent
can surface a clickable link — the user's click is a real gesture and completes
the OAuth normally. Drops the now-unused popup/poll helper.

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

* 💄 fix(composio): match pending toolbar item to sibling authorize affordance

The ··· dropdown I added to the chat-toolbar Composio item was a bare icon
(inconsistent color/size with the app's standard menus), its popup was
mis-anchored/offset, and replacing the visible "authorize" cue with a ···
made an un-authorized (pending) row look connected.

Match the sibling LobehubSkillServerItem instead: render a clickable
"Re-authorize" text + external-link icon for PENDING/ERROR. Clicking re-mints a
fresh link (the prior one may have expired) and opens it. No dropdown, so no
offset; the explicit affordance makes it clear the row still needs auth. Delete
stays on the settings page (siblings have no inline delete here either).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…15858)

Completes the SWR key convergence by migrating the remaining UI-layer ad-hoc
keys (features / routes / components) into the central registry. New domains:
stats, messenger, verify, inbox, share, fork, portal, favorite, changelog,
onboarding, agentHome, agentProfile, agentSignal, ollama, auth, cron,
topicAction — plus extensions to discover (mcpAgents/skillAgents/market),
device (gitBranches/repoType), session (createSession), group (queryAgents*).

- Shared keys (availablePlatforms, agentsForBinding, bindingScopes,
  shared-topic, favorite-status, openNewTopicOrSaveTopic, portal-document-header,
  inbox notifications/unread) are routed through one factory at every call site
  so they still dedupe to a single cache entry.
- The notifications useSWRInfinite getKey and the userMemory-style matcher
  invalidations were migrated in lockstep with their fetch keys.
- No tiering/caching change: every new prefix is kept out of CACHE_TIERS, and
  names avoid the cached prefixes (share:/portal:/agentHome:/agentProfile: etc.
  instead of topic:/document:/agent:). Behavior preserved.
- Folds in the lone cross-layer `cronTopicsWithJobInfo` store mutate.

Packages (builtin-tool *) keep their local keys — they can't import from
`@/libs/swr/keys`; left as-is intentionally.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…efetch key (#15863)

* ♻️ refactor(swr): converge the last straggler SWR keys + fix stale prefetch key

Final cleanup of the SWR key convergence. Migrates the remaining ad-hoc keys
that earlier grep-based sweeps missed (they hid behind non-obvious const names
like SWR_KEY / FETCH_*_KEY / SWR_RESOURCES, template-literal keys, the electron
store, and assorted one-off hooks):

- hooks: usePrefetchAgent, useHomeDailyBrief, useGatewayReconnect
- features: OpenInAppButton, Recommendations/useHeteroDetections,
  RecommendTaskTemplates, ResourceManager search
- routes: provider ClientMode + DisabledModels (useSWRInfinite), memory
  analysis task, sidebar task groups, imessage bridge status, Review git patches
- store: user initState + checkTrace, builtin agent init, file resources,
  electron settings/gateway/sync

New registry domains: home, taskTemplate, resource, provider, recommendations,
openInApp, gateway, user, builtinAgent, imessage, sidebar, electron — plus
extensions to aiModel (disabledModelsPage), device (gitReviewPatches /
gitRemoteBranches), userMemory (analysisTask).

🐛 Fix: usePrefetchAgent warmed `['FETCH_AGENT_CONFIG', agentId]`, which never
matched what `useFetchAgentConfig` reads. It now warms
`augmentKey(agentConfigKeys.config(agentId), getActiveWorkspaceId())` — the
exact workspace-scoped key the consumer subscribes to, so hover-prefetch
actually populates the cache.

No tiering/caching change: every new prefix is kept out of CACHE_TIERS
(names avoid the cached agent:/task:/brief: tiers). The electron factory roots
retain their original `electron:getXxx` strings, so those cache identities are
unchanged.

After this, the only ad-hoc SWR keys left are in `packages/*` (can't import
`@/libs/swr/keys`); every `src/` SWR call site now routes through the registry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(swr): drop suspense: true from data-fetching hooks

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✅ test(swr): update refreshUserState assertion to registry key

Follow-up to the prior commit: the auth-slice test still expected
mutate('initUserState'); refreshUserState now passes userKeys.initState()
(['user:initState']). Assert against the factory so it tracks the registry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…agent (#15862)

Switching to or opening an agent tab could flash the conversation
header/welcome back to the inbox "Lobe AI" identity. Two causes:

- `useFetchAgentConfig.onData` set the global `activeAgentId` to whatever
  config resolved, so a background/secondary fetch (the inbox config from
  the home input, a side-panel copilot, or another open tab) hijacked the
  routed agent. It now only adopts the fetched agent when none is active;
  route-level sync (AgentIdSync on desktop/mobile, the popup pages' own
  setState) owns `activeAgentId`.
- `AgentInfo` (the agent conversation welcome) read the global
  `currentAgentMeta` / `isInboxAgent`. Scope it to the conversation's agent
  via `useConversationStore(contextSelectors.agentId)` + `*ById` selectors,
  so it renders the routed agent even if the global races.

Also remove the dead `Conversation/AgentWelcome/{index,OpeningQuestions}`
(the conversation welcome is `AgentHome`/`AgentInfo`; this variant was
unreferenced).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…w source (#15867)

🐛 fix(updater): render release notes as Markdown instead of raw source

The update modal injected release notes via dangerouslySetInnerHTML, but
the content is a Markdown source string (e.g. `## Canary Build`, GFM
tables), so headings/tables/bold were shown literally as raw text.

Render it with @lobehub/ui's <Markdown> component instead. Also handle the
`ReleaseNoteInfo[]` shape of `releaseNotes` by rendering each note.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…text (#15865)

* 🐛 fix(chat): preserve subAgentId/documentId in message bucket key context

`replaceMessages` and `internal_getConversationContext` rebuilt the
conversation context with a hand-picked field whitelist, silently dropping
`subAgentId` (and others). Since `messageMapKey` uses `subAgentId` as the
group_agent scope subTopicId, group-agent writes collapsed into the wrong
bucket. Spread the whole context instead and only special-case the fields
that need a fallback/assertion (agentId, topicId), so every bucket-key
field carries through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✅ test(database): deterministic ordering in topic.duplicate test

Both seed messages were inserted in one transaction with no explicit
createdAt, so they shared the same `now()` default. `duplicate`'s
`orderBy(createdAt)` then returned the tied rows in arbitrary order,
making the positional assertions flaky. Give them distinct createdAt
(user before assistant) so the order is well-defined.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Jun 15, 2026
@pull pull Bot added the ⤵️ pull label Jun 15, 2026
@pull pull Bot merged commit 8ad6c21 into cresseelia:canary Jun 15, 2026
3 of 4 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants