🚀 release: 20260610#15647
Conversation
…e 8 packages into CI (#15448) * ✨ feat(agent-management): paginate searchAgent with real totals and cap notice The searchAgent tool silently clamped limit to 20 with no pagination and reported totalCount as the returned page size, so models (and users) could never discover agents beyond the 20 most recently updated ones. - AgentModel: extract shared where builder, add countAgents (same conditions as queryAgents) - lambda router + client agent service: expose countAgents - server tool runtime & AgentManagerRuntime: pass offset through, report real totals (workspace + marketplace), emit explicit notes when the requested limit is capped and when more pages exist, explain out-of-range offsets instead of claiming no matches - manifest: add offset param, document pagination - agent-manager-runtime: add vitest config + test scripts (suite was previously unrunnable), repair stale store mocks * 👷 build(ci): wire 8 tested packages into the package test workflow An audit found 8 packages carrying test:coverage scripts that were never added to the CI PACKAGES allowlist, so their suites never ran: - agent-gateway-client, device-gateway-client, device-identity, eval-dataset-parser: already green, added as-is - eval-rubric, fetch-sse: had no package-level vitest config, so vitest fell back to the root config whose setup/aliases break outside src/ — added minimal configs - heterogeneous-agents: one assertion drifted (labels registry gained amp/hermes/openclaw/opencode) with nobody noticing — updated - agent-manager-runtime: wired in the previous commit All 8 verified locally with the exact CI command (bun run --filter <pkg> test:coverage). * ✅ test(agent-management): cover searchAgent error path and market totalCount fallback Codecov flagged 3 uncovered lines in the patch: the searchAgents catch block (2 misses) and the totalCount ?? items.length fallback (1 partial). Add the missing failure-path and fallback tests on both execution paths (client AgentManagerRuntime + server tool runtime).
…tions (#15445) Long-running queries (e.g. an insert stuck for 700s on lock contention) could block indefinitely because Postgres' statement_timeout defaults to 0 (no limit) and neither the node nor neon pool configured one. Add an optional DATABASE_STATEMENT_TIMEOUT env (milliseconds, no default) applied to both NodePool and NeonPool as statement_timeout and idle_in_transaction_session_timeout, so Postgres aborts a stuck statement or idle transaction on the server side. Unset keeps the previous behavior. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… DeepSeek compatibility (#15413)
…builtin agent (#15443) Move the self-iteration skill-management action off the inline policy implementation onto an execAgent-dispatched builtin agent (slug `skill-management`), mirroring the S3/S4 memoryWriter + self-iteration migration. Adds the `agentSignalSkillManagement` serverRuntime, the builtin-tool-agent-signal skill-management manifest/systemRole, and the builtin-agents skill-management agent; strips the ~3.5k-line inline skillManagement policy down to the dispatch shim. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 🗃️ feat(database): add workspace_id columns to existing tables Add a nullable `workspace_id text` column to user-owned business tables (agents, sessions, topics, messages, files, tasks, RAG/eval, RBAC, devices, connectors, etc.) so records can later be scoped to a workspace. Workspace tables themselves already landed on canary via 0105_add_usage_agent_share_workspace. Also folds in the additive device schema from #15356: the structured `working_dirs` jsonb column + `WorkingDirEntry` type (recent_cwds kept, now @deprecated). Scope is deliberately column-only — the lowest-risk slice: - migration 0106 is pure `ADD COLUMN IF NOT EXISTS` (metadata-only, ~ms locks per table, online-safe, no app code change since columns are all NULL). - FKs, btree indexes, and the per-user→workspace-scoped unique-constraint conversions are intentionally deferred to follow-up PRs so each can use the production-safe execution path Drizzle can't express (NOT VALID + VALIDATE, CREATE INDEX CONCURRENTLY, atomic unique swap). Scoping notes: - devices / user_connectors / user_connector_tools: scoped (user-owned resources). - push_tokens: left user/device-level — an Expo token is one per app install and receives a person's notifications across all their workspaces. - agent_shares: no workspace_id — scoped transitively via agent_id → agents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🐛 fix(database): satisfy inferred row types after adding workspace_id Adding workspace_id made it a required key in the Drizzle-inferred row types ($inferSelect), breaking call sites that build those shapes by hand: - rbac.getUserRoles: include workspace_id in the explicit select projection - session action: add workspaceId to the constructed chat-group literal - test mocks (apiKey / generation / generationBatch / generationTopic): add workspaceId: null Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ✅ test(database): use toMatchObject for topic.create row assertions The two `expect(createdTopic).toEqual({ ...full literal })` snapshots broke on every new column (here: workspace_id). Switch them to toMatchObject so the returned row may carry extra columns without churning the expected literal. The dbTopic↔createdTopic strict comparisons are left as toEqual. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge (#15457) * ♻️ refactor(message): prefer dedicated usage column over metadata.usage Token usage was promoted out of metadata.usage into a dedicated messages.usage column, but nothing populated it and all reads still went through metadata.usage. - Centralize write-side promotion in the DB model (update / updateMetadata / create), so all executor callers populate the usage column from a top-level usage payload, falling back to metadata.usage. metadata.usage stays dual-written for backward-compatible reads. - Reads prefer the usage column and fall back to metadata.usage: message queries, getTokenHeatmaps, recomputeTopicUsage, the usage record service, and context token accounting. - Add top-level usage to UpdateMessageParams + DBMessageItem types. - Mark metadata.usage and the legacy flat token fields as @deprecated, pointing to the top-level usage field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(message): dual-write metadata.usage for top-level usage updates When a caller passed the new top-level `usage` param without also sending `metadata.usage`, the update wrote only `messages.usage` and left `metadata.usage` stale/absent — legacy readers and rollback paths still consume it during the dual-write transition. Fold the resolved usage into the metadata patch so `metadata.usage` stays in sync regardless of how usage was passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 🗃️ db(database): add workspace_id FK constraints (migration 0107)
Phase 2 of workspace_id rollout: add the FK constraint on the 70 tables
that gained a bare `workspace_id` column in Phase 1 (0106), referencing
workspaces(id) ON DELETE CASCADE.
- schema: add `.references(() => workspaces.id, { onDelete: 'cascade' })`
to all 70 nullable workspace_id columns
- 0107_add_workspace_id_fk.sql: idempotent drizzle migration
(DROP CONSTRAINT IF EXISTS + ADD), runs in CI / dev / self-host
- 0107_concurrent.sql: production-safe out-of-band runbook
(NOT VALID + VALIDATE) to avoid write-blocking locks on large tables;
NOT run by drizzle
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🔥 db(database): remove stray 0107_concurrent migration file
* 🐛 fix(database): break user/workspace schema circular dependency
Move userInstalledPlugins from user.ts into connector.ts to break the
user.ts <-> workspace.ts import cycle flagged by dpdm. connector.ts
already imports both users and workspaces, and consumers import the
table from the schemas barrel, so no call sites change.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Stdio MCP servers live on the user's machine, but in gateway (cloud) mode the agent runs server-side and `executeMCPTool` tried to spawn the stdio binary on the cloud server — which has neither the binary nor access to the user's machine, so local MCP tools (e.g. tasks calling a local kimi-datasource MCP) always failed. Add a dedicated `executeMcpCall` path that forwards the stdio connection params (command/args/env) to a connected device, which spawns the MCP server and runs the call locally. It rides the existing `/api/device/tool-call` relay — the gateway forwards `toolCall` opaquely — so the device-gateway worker needs no changes; the device routes on the presence of `toolCall.mcpParams`. Server-side only: when no device is connected, behavior is unchanged (standalone Electron still spawns in-process). The desktop-side receiver that runs the forwarded call lands in a follow-up. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 🗃️ db(database): add workspace_id indexes (migration 0108) Phase 3 of the workspace DB migration (LOBE-9961). Adds a btree index on workspace_id to 70 tenant tables, plus 7 workspace-scoped partial unique indexes (WHERE workspace_id IS NOT NULL) that pre-build the "new" side of the Phase 4 (0109) unique-constraint cutover. A separate production-safe runbook (0108_concurrent.sql, CREATE INDEX CONCURRENTLY, ordered smallest->largest) is intentionally NOT committed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🗃️ db(database): make 0108 index migration idempotent Add IF NOT EXISTS to all 70 CREATE INDEX + 7 CREATE UNIQUE INDEX statements, per the db-migrations standard flow (defensive/idempotent SQL), matching how 0107 used DROP CONSTRAINT IF EXISTS. Safe to re-run and safe if the concurrent runbook already built the indexes before the auto-migrator reaches 0108. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
🐛 fix: prefer internal app url for comfyui calls
…ls (#15473) * ✨ feat(gateway): add explicit type discriminator to tunneled tool calls The device-gateway relays builtin local-system calls and tunneled stdio MCP calls over one `tool-call` channel. The device was meant to tell them apart by sniffing whether `toolCall.params` exists — fragile: any future builtin tool that grows a `params` field would be misrouted to the MCP client. Add an explicit `toolCall.type` discriminator (`'builtin' | 'mcp'`). The HTTP client stamps it: `executeToolCall` → `'builtin'`, `executeMcpCall` → `'mcp'`. The device routes on `type`, never on payload shape. Optional + back-compatible: an older server that omits it is treated as `'builtin'`. The desktop receiver switches to this discriminator in a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ✨ feat(desktop): execute tunneled stdio MCP calls from the gateway (#15470) Receiving half of the gateway stdio-MCP work. When the cloud server tunnels a stdio MCP tool call to this device (a `tool_call_request` carrying `mcpParams`), run it locally instead of falling through to the builtin local-system tool switch (which keys on apiName and has no MCP context, so it rejected these as "not available on this device"). - `gatewayConnectionSrv`: add a dedicated `mcpCallHandler` + `setMcpCallHandler`; `handleToolCallRequest` routes on the presence of `toolCall.mcpParams`, sharing the existing response-envelope path. - `GatewayConnectionCtr`: wire `setMcpCallHandler` → `executeMcpCall`, which maps the wire payload to `McpCtr.runStdioMcpTool`. - `McpCtr`: extract `runStdioMcpTool` core from the `callTool` IPC method so both the renderer and the gateway tunnel share one stdio execution path (no SuperJSON round-trip for the in-process caller). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…5472) * 🗃️ db(database): migrate unique constraints to workspace scope (migration 0109) Replace the legacy user-scoped UNIQUE constraints with workspace-scoped partial unique indexes across agents, agent evals, agent skills, documents, sessions, tasks, and rbac roles/user-roles. Adds migration 0109_migrate_unique_constraints and updates the affected schemas. * 🐛 fix(database): match partial unique index in getBuiltinAgent upsert Migration 0109 turned `agents_slug_user_id_unique` into a partial index (WHERE workspace_id IS NULL). A plain `ON CONFLICT (slug, user_id)` no longer matches it (Postgres 42P10), breaking getBuiltinAgent. Add the same predicate via onConflictDoNothing's `where` option; builtin agents are always workspace-less so the predicate always holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🔨 chore(database): use bare onConflictDoNothing in getBuiltinAgent for 0109 transition Index-shape-agnostic upsert so the builtin-agent path works whether agents_slug_user_id_unique is the legacy full unique or the 0109 partial, removing the deploy-ordering coupling. Re-tighten to { target, where } in a follow-up once 0109 has flipped the index everywhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix: bypass audits for headless tool calls * 🐛 fix: block high-risk headless tools at execution * Revert "🐛 fix: block high-risk headless tools at execution" This reverts commit 1d4b534. * 🐛 fix: restore headless audit bypass * 🐛 fix: resolve headless blocked tools * 🐛 fix: simplify blocked tool results * 🧹 chore: remove unrelated prompt diff * 🐛 fix: narrow blocked tool instruction type * 🐛 fix: split security blacklist policies * 🐛 fix: simplify security blacklist policy rules * 💄 style: tighten security blacklist diff * 💄 style: reduce agent config doc diff * 💄 style: tighten headless audit diff * 💄 style: minimize audit policy diff * 💄 style: clarify global audit match naming * 🐛 fix: auto-run required global audits in headless * 💄 style: clarify headless intervention comments * 💄 style: clarify headless global audit comment * 💄 style: use blocked tool instruction type * 💄 style: clarify headless audit tests * 💄 style: annotate headless blocked tool tests * 🐛 fix: type security blacklist policy filter * 💄 style: clarify local system 403 guidance * 🐛 fix: use current persist error helper
…15475) 🔨 chore(database): re-tighten getBuiltinAgent onConflict to the 0109 partial index Now that migration 0109 has flipped agents_slug_user_id_unique to a partial index (WHERE workspace_id IS NULL) in all environments, restore the precise conflict arbiter { target: [slug, userId], where: isNull(workspaceId) } so unexpected unique violations surface instead of being silently swallowed by the bare onConflictDoNothing() transition form. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…odel fetch logic (#15376) * 🗑️ chore(opencode-go): remove MiMo V2 Omni and MiMo V2 Pro models * ✨ feat(opencode-go): fetch model list from API with models.dev enrichment - Try API /models first for real-time available models - Enrich with models.dev data (pricing, abilities, SDK routing) - Fallback to models.dev + model-bank if API fails - Dynamic Anthropic SDK routing via provider.npm field * 💰 fix(opencode-go): update MiMo pricing to match models.dev - mimo-v2.5: input $0.14, output $0.28, cache_read $0.0028 - mimo-v2.5-pro: input $1.74, output $3.48, cache_read $0.0145 * ✨ feat(opencode-go): add MiniMax M3 and remove deprecated Qwen3.5 Plus - Add minimax-m3: 512K context, vision support (image+video), 131K output, pricing 0.6/2.4/0.12 USD per M tokens, released 2026-05-31 - Remove qwen3.5-plus: marked deprecated in models.dev * 🐛 fix(opencode-go): restore Anthropic routing fallback when models.dev is unreachable Codex P2 review on #15376: - `routers` is called with `ClientOptions` (no `client` field), so `options.client?.models.list?.()` silently returned `undefined` via optional chaining; the `catch` never ran and `modelIds` stayed `[]`. - In API + models.dev double-failure scenarios, `getAnthropicModels([])` returned an empty list, regressing Anthropic SDK routing for MiniMax / Qwen models. Fix: - Make `getAnthropicModels` self-contained: takes no parameters. - Fallback chain: models.dev → static model-bank prefix match → `[]`. - `routers` no longer touches `options.client`. * ✨ feat(opencode-go): enrich model list with models.dev metadata The model list pipeline previously forwarded only `{ id }` from the API and models.dev, so displayName / pricing / context / modalities all came from the static model-bank. When models.dev disagrees with model-bank (e.g. a price update or new model), the runtime would show stale data. Map models.dev fields into the flat shape that `processModelCard` understands, so each card is enriched with: - displayName (dev.name) - contextWindowTokens / maxOutput (dev.limit) - releasedAt (dev.release_date) - functionCall / reasoning / vision / structuredOutput (dev.flags + dev.modalities.input) - pricing (dev.cost → flat input/output/cachedInput/writeCacheInput; processModelCard's formatPricing converts it to units) Fields models.dev doesn't have (description, organization, settings .extendParams, etc.) still fall back to the model-bank entry via processModelCard's knownModel lookup, keeping the static config as the source of truth for UX-only fields. * ✨ feat(opencode-go): drive reasoning_content handling from models.dev The `reasoningInterleavedModels` list was hardcoded and drifted from models.dev: - Missing: kimi-k2.5, kimi-k2.6, mimo-v2-omni, mimo-v2-pro - Stale: qwen3.7-max (no longer has `interleaved` in models.dev) Move the source of truth into the models.dev cache. `fetchModelsDevData` now also builds an `interleavedIds: Set<string>` from `m.interleaved.field` alongside `anthropicModels`, so every derived field stays in sync with a single fetch. The new `getInterleavedModelIds` sync accessor lets `buildOpenAIPayload` keep its sync signature; it returns the cached set when populated and falls back to a hardcoded snapshot of the last-known models.dev state on the very first chat request before any fetch has run.
…15353) Consume the `working_dirs` column: model `updateDevice`, tRPC `updateDevice` input + `listDevices` output, and the client cwd pickers now operate on `WorkingDirEntry[]` instead of the flat `recentCwds: string[]`. - model / tRPC: `workingDirs` (input capped at 20, validated `{ path, repoType? }`) - client `deviceCwd`: `nextRecentCwds` → `nextWorkingDirs` - UI: DeviceWorkingDirectory / WorkingDirectory / DeviceDetailPanel / DeviceItem render the detected repo type via the shared `renderDirIcon` Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ feat(task): auto-ensure qstash schedule chore: cleanup code chore: cleanup code chore: cleanup code * chore: migrate qstash init workflow to startServer chore: migrate qstash init workflow to startServer * fix: set default QSTASH_URL to eu region, same as SDK fix: set default QSTASH_URL to eu region, same as SDK
* 🐛 fix(agent-documents): render system docs in editor * ✨ feat(agent-documents): autosave highlight editor with safe unmount flush Add debounced autosave to the non-markdown highlight editor and a StrictMode-safe unmount flush via queueMicrotask, plus a beforeunload guard against dirty buffers. * ✅ test: fix agent document PR type checks
… triggers a new run (#14873) * ✨ feat(task-detail): split task panel comment from topic-thread reply CommentInput in TaskActivities stays as-is on canary — avatar + EditorCanvas + attachment + send button, posting a plain task-level comment. TopicChatDrawer footer becomes a FeedbackInput that calls the in-scope ConversationProvider's sendMessage, continuing the existing topic conversation instead of attaching a comment + restarting the run. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ✨ feat(task-detail): keep FeedbackInput visible while topic is running Drop the canLeaveFeedback gate so the in-thread reply box renders even when the topic is pending/running. ConversationStore.sendMessage already queues messages during an in-flight stream, so this just exposes the queue affordance to the user — letting them steer the next step without waiting for the current run to terminate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 💄 style(task-detail): collapse FeedbackInput behind a follow-up button + add attach action FeedbackInput now starts collapsed as a full-width "Send follow up message" button. Click expands a ChatInput shell with EditorCanvas inside and a footer that carries an AttachmentUploadButton on the left (+ icon) and the send button on the right. Files are inserted inline into the editor (same pattern as CommentInput) so they ride along on sendMessage's editorData. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 💄 style(task-detail): tighten CommentInput card & switch follow-up button to filled - CommentInput card: padding-block 8px → 4px, editor placeholder fontSize 14px - FeedbackInput collapsed button: default size + variant="filled" for a less obtrusive look that sits flush in the chat footer Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 💄 style(task-detail): drop top padding above FeedbackInput in topic drawer Use paddingBlock="0 12px" so the follow-up button hugs the last message instead of floating with a 12px gap above. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 🐛 fix(task-detail): clear FeedbackInput editor before awaiting sendMessage Previously the editor cleanup ran after the awaited sendMessage call, so the box kept the just-sent text on screen until the entire send + stream lifecycle resolved. Move clearContent / collapse before the await so the input feels responsive (sendMessage already snapshots markdown and editorData for its optimistic update). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 🐛 fix(task-detail): keep FeedbackInput expanded after sending Drop the setExpanded(false) call in handleSubmit so the ChatInput remains open once the user has opened it. Collapsing it back to the "Send follow up message" button right after every reply was disruptive mid-conversation; the button only makes sense as the initial resting state of the drawer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ✨ feat(chat): add forceRuntime override to SendMessageParams Plumb a new optional forceRuntime field through SendMessageParams → ConversationLifecycle.sendMessage → selectRuntimeType(parentRuntime). parentRuntime already wins over every other signal in the dispatcher, so callers can pin a send to 'gateway' / 'client' / 'hetero' regardless of the agent's local/cloud config. Also propagate forceRuntime through the message queue (QueuedMessage + MergedQueuedMessage + mergeQueuedMessages + both drain sites in the client and hetero executors) so a follow-up queued during an in-flight run keeps its runtime pin when it eventually fires. FeedbackInput in TopicChatDrawer passes forceRuntime: 'gateway' so task-topic follow-ups stay on the server-side path that runTask originally used, even if the user's global runtime preference is local. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* ✨ feat(topic): add one-click collapse/expand all groups in topic sidebar Add a toggle button in the topic sidebar header (next to Filter and the more-actions menu) that collapses or expands all topic groups at once. It reuses the existing `expandTopicGroupKeys` global status, so it stays in sync with manual per-group toggling, and hides itself when there are fewer than two groups (e.g. flat mode). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(topic): hide group toggle in flat mode In flat mode, groupedTopicsForSidebar falls through to time grouping so the computed group count can exceed one, but List renders FlatMode with no accordion for the toggle to affect. Hide the control explicitly when topicGroupMode === 'flat' instead of relying on the group count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 💄 style(topic): use 2-corner minimize/maximize icons for group toggle Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <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>
…ch doesn't time out (#15634) * 🐛 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>
… aborted (#13677) * 🐛 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>
…ueue mode (#15620) * 🐛 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>
…15638) 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>
feat: support workspace (full) — store→business-hook + workspace router
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 524e701d61
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #15647 +/- ##
==========================================
- Coverage 70.88% 65.18% -5.70%
==========================================
Files 3223 3815 +592
Lines 319680 365840 +46160
Branches 28141 36170 +8029
==========================================
+ Hits 226593 238489 +11896
- Misses 92911 127160 +34249
- Partials 176 191 +15 Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
🐳 Database Docker Build Completed!Version: Pull ImageDownload the Docker image to your local machine: docker pull lobehub/lobehub:pr-release-weekly-20260610-recut-3-813ff2cImportant This build is for testing and validation purposes. |
524e701 to
b8339ab
Compare
🚀 Desktop App Build Completed!Version: 📦 Release Download · 📥 Actions Artifacts Build Artifacts
Warning Note: This is a temporary build for testing purposes only. |
🚀 LobeHub Release (20260610)
Release Date: June 10, 2026
Since v2.2.2: 131 merged PRs · 13 contributors
✨ Highlights
lh connectagent dispatch paths. (🐛 fix(desktop): unbreak dev cold-start + restore UI language across reloads #15547, 🐛 fix(desktop): skip browser beforeunload guard so auto-update can quit #15525, 🐛 fix(desktop): pin electron-builder to 26.14.0 to fix broken macOS update signing #15527, 🐛 fix(desktop): bump node-gyp to 12.x so Windows build finds Visual Studio 2026 #15562, 🐛 fix(cli): skill list/search commands returning empty results #15632, 🐛 fix(cli): handle agent_run_request inlh connectso device dispatch doesn't time out #15634)🏗️ Core Product & Architecture
Agent Runtime & Heterogeneous Agents
📱 Platforms, Integrations & UX
Connectors, Sandbox & Tools
Desktop, CLI & Web UX
lh connectso device dispatch doesn't time out #15634, 🐛 fix(cli): skill list/search commands returning empty results #15632)🔒 Security, Reliability & Rollout Notes
workspace_idcolumns to existing tables #15446, 🗃️ build(database): add workspace_id FK constraints #15465, 🗃️ build(database): addworkspace_idindexes #15468, 🗃️ build(database): migrate unique constraints to workspace scope #15472)canaryand includes the latestmainrelease-version commit sov2.2.2is the verified compare base.👥 Contributors
@ONLY-yours, @sxjeru, @hardy-one, @xujingli, @hezhijie0327, @Coooolfan, @arvinxx, @tjx666, @Innei, @rivertwilight, @rdmclin2, @cy948, @AmAzing129
Full Changelog: v2.2.2...release/weekly-20260610-recut-3