🐛 fix(heterogeneous-agents): stream subagent Thread + fix parallel-tool orphan#14024
Conversation
…ool orphan When a main-agent step emits a parallel tool_use (e.g. `[Grep, Agent]`), the gateway handler's stream_chunk branch was forwarding the subagent's inner `tools_calling` chunks onto `currentAssistantMessageId` (main), overwriting main.tools[] with subagent tools — main's own Task/Agent tool_use then had no matching entry and every tool message under it rendered with the "orphan tool call" banner. Two coordinated changes: 1. Main-bucket isolation: the executor now drops subagent-tagged `stream_chunk` events before forwarding to the gateway handler. DB persistence continues via `persistSubagent*Chunk` so the subagent content is never lost; only the main-handler in-memory dispatch is suppressed for subagent chunks. 2. Thread-bucket streaming: `internal_dispatchMessage` now accepts a `threadId` override that snaps scope to `thread`, routing create/update payloads to the thread's `messagesMap` bucket. Each `SubagentRunState` carries a thread-scoped dispatcher; ensureSubagentRun seeds user + assistant on lazy Thread creation and at turn boundaries, persistToolBatch gets an `onToolCreated` hook that the subagent path uses to seed role:'tool' rows, persistSubagent*Chunk dispatches tools[] / content / reasoning updates on every chunk, and the tool_result branch mirrors subagent tool_result content (+ pluginState) into the thread bucket. Thread view now streams token-by-token with the same cadence as the main bubble. Tests: - `does NOT forward subagent-tagged stream_chunks to the gateway handler` — asserts main bucket isolation under parallel main+subagent tool use. - `streams subagent create/update dispatches into the thread messagesMap bucket` — asserts user/assistant/tool createMessage dispatches land in the thread scope, plus streaming updateMessage for tools[], content, and tool_result, with no bleed into the main bucket. Local repro verified end-to-end: main assistant.tools=[Grep, Agent] stays intact across two parallel runs, thread bucket populates 14 rows (user + 2 subagent assistants with Bash/Glob then Read×8 + 10 tool results) during the run, `mainOrphans`/`threadOrphans`/ `threadIntoMainBleed` all empty, orphan warning DOM count = 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## canary #14024 +/- ##
==========================================
+ Coverage 66.86% 66.91% +0.05%
==========================================
Files 2100 2103 +3
Lines 179535 179787 +252
Branches 21192 17778 -3414
==========================================
+ Hits 120038 120299 +261
+ Misses 59373 59364 -9
Partials 124 124
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 63c00290a2
ℹ️ 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".
…r-spawn sub-operation
Replace the threadId-override on `internal_dispatchMessage` with a
proper per-spawn child operation, eliminating the second context
expression at the dispatch boundary.
The previous design accepted `{ operationId, threadId? }` and snapped
scope to `'thread'` when the override was present. That was a leaky
parallel path to the operation registry — the same "which messagesMap
bucket should this dispatch hit?" question got answered two different
ways. `startOperation` already supports `parentOperationId` + context
inheritance + recursive cancel cascade, so the right move is to model
the subagent run as a first-class child op and let
`internal_getConversationContext` do its normal job.
Changes:
- Add `'subagentThread'` to `OperationType` (NOT in
`AI_RUNTIME_OPERATION_TYPES` — it's a context container, not an
independent loading state, so it shouldn't double-count for spinners).
- `executeHeterogeneousAgent` opens the sub-op in `beginSubagentRun`
via `startOperation({ type: 'subagentThread', parentOperationId,
context: { ...context, threadId, scope: 'thread' } })` and binds a
thread-scoped dispatcher to that sub-op's id.
- `SubagentRunState.subOperationId` carries the id so `finalizeSubagentRun`
can mark it completed when the spawn's tool_result arrives (or on the
`onComplete` fallback for crash/abort paths). Cancel cascade + cleanup
flow through the existing parent/child op linkage.
- Revert the `threadId` override in `internal_dispatchMessage` — the
store boundary is back to a single context expression
(`{ operationId? }`).
Test:
- Add `startOperation` mock to `createMockStore` (returns monotonic
`sub-op-N` ids).
- Update the streaming regression to identify the sub-op via the
`startOperation` call with `type: 'subagentThread'`, assert the
sub-op's parent + context shape, filter Thread bucket dispatches by
`ctx.operationId === subOperationId`, and verify
`completeOperation(subOperationId)` fires when the run finalizes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sh confirms `finalizeSubagentRun`'s buffer reset used to run unconditionally after the flush try/catch, so a transient `messageService.updateMessage` failure silently wiped the accumulated streamed text/reasoning — the later `onComplete` fallback then had nothing left to retry, leaving the subagent's streamed content absent from persisted thread history. Move the clear into the success branch. A second concern surfaces once the clear moves: after the flush block, the `resultContent` branch advances `currentAssistantMsgId` to the newly created terminal assistant, so a naive retry that reads `currentAssistantMsgId` would overwrite the authoritative terminal content with the leftover streamed buffer — corrupting the subagent summary with stale partial text. Pin the flush target via a new `SubagentRunState.pendingFlushTarget`: captured before the DB attempt, carried on the run when the flush fails, cleared alongside the buffers on success. The retry uses the pinned target instead of the live `currentAssistantMsgId`, so leftover streamed buffers always land on the streaming turn's assistant — never on the terminal row. Test: `retains subagent buffers + pinned target when the finalize flush fails` stubs `updateMessage` to throw once for the subagent streaming write, runs streamed text → spawn `tool_result` → `onComplete`, and asserts (1) the leftover content eventually reaches DB across ≥2 write attempts and (2) every attempt targets the streaming turn's assistant — not the terminal row created by `resultContent`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# 🚀 LobeHub v2.1.53 (20260427) **Release Date:** April 27, 2026 **Since v2.1.52:** 194 merged PRs · 17 contributors > Introduce Heterogeneous Agent — Claude Code and Codex run as first-class desktop runtimes, paired with a new Agent Signal package, sharper desktop UX, and a wave of flagship model additions. --- ## ✨ Highlights - **Introduce Heterogeneous Agent** — Claude Code and Codex run as first-class desktop agents: subagent rendering, partial-message streaming, multi-turn resume, terminal error surfacing, rich tool inspectors, and runtime polish. (#14162, #13754, #14067, #14001, #13970, #13942) - **Screen capture & Quick Chat tray** — New desktop screen capture overlay (macOS permission-gated) with Quick Chat tray and upload pipeline improvements; chat input auto-focuses on overlay mount. (#13818, #14097, #14105) - **Desktop topic & tab UX** — Dedicated topic popup window with cross-window sync, Cmd+W/Cmd+T tab shortcuts, TabBar polish, recent working directories expanded to 20, and human approval notifications. (#13957, #13983, #13972, #14036, #14092) - **Git workflow built-in** — One-click pull/push from the branch chip, ahead/behind badge, and submodule/worktree repo detection. (#14041, #13980, #13978) - **Agent Signal package** — New `@lobechat/agent-signal` runtime for dynamic memory feedback signals, with OTel metrics and self-iteration in Lab. (#14157, #14170, #14159, #14169, #14187) - **New models** — Claude Opus 4.7 with `xhigh` effort tier, GPT-5.5, DeepSeek V4 Flash/Pro with reasoning slider, Kimi K2.6, MiMo-V2.5/Pro, gpt-image-2, Qwen3.6 Flash/Plus, and Pixverse-c1. (#13903, #14147, #14114, #14004, #14089, #14039, #13923) - **New providers** — OpenCode Zen, OpenCode Go, and Azure OpenAI Router runtime. (#13943, #14064, #13823) - **Mobile settings overhaul** — Full settings menu and responsive profile layout for mobile. (#14019) --- ## 🏗️ Heterogeneous Agent - Claude Code runtime, working-directory awareness, and sidebar polish. (#13970) - CC subagent rendering with persistent streamed text; parallel-tool orphan fix. (#14001, #13968, #14024) - Per-step usage persisted to each step assistant message. (#13964) - Per-phase workflow expand defaults; full-expand toggle with three-level expansion. (#14171, #13906) - Hetero-mode actions bar; tool inspector polish. (#13963, #14034, #14030) - Codex desktop integration with rich tool rendering and devtools preview. (#14067, #14100) - Codex terminal error surfacing and CLI output tracing. (#14166) - Tighten `isCanUseVision` default and add aggregator fallback. (#14172) - Persist `ccSessionId` in topic metadata for CC multi-turn resume. (#13902) - CC account card, topic filter, and integration polish. (#13955, #13942, #13950) - Token-level deltas streamed via `--include-partial-messages`. (#13929) --- ## 🧠 Agent Signal & Self-Iteration - New `@lobechat/agent-signal` package with dynamic feedback signals. (#14157) - AgentSignalRuntime wired through agent-tracing and observability-otel metrics. (#14170, #14159) - Self-iteration feature flag added to Lab; front-side flag check. (#14169, #14186) - Signal policy for receiving memory feedback dynamically. (#14187) --- ## 💬 Conversation - Queue follow-up sends during running CC turns. (#14179) - Persist per-topic chat scroll position; pin user message + fold long messages. (#14191, #14056) - Inline resend when editing last user message. (#14080) - Disable first-block markdown streaming to prevent flicker. (#14193, #13904) - Prevent Markdown stream replay when vlist remounts streaming items. (#14086) - Stop repinning after manual scroll; unify scroll-to-user + spacer hooks. (#14099, #14132) --- ## 📱 Platforms & Integrations ### Desktop / Electron - Screen capture overlay, Quick Chat tray, and upload pipeline improvements. (#13818) - macOS permission gate for screen capture; auto-focus chat panel input. (#14097, #14105) - Dedicated topic popup window with cross-window sync. (#13957) - TabBar polish: `+` button for new topic, dark theme blend, close icon by default. (#13972, #14203, #13973) - Recent working directories expanded from 5 to 20; submodule/worktree repo detection. (#14036, #13978) - Cmd+W / Cmd+T tab shortcuts and global shortcut consolidation. (#13983, #13880) - Linux icon configuration; human approval desktop notifications. (#14042, #14092) ### Git Workflow - One-click pull/push from branch chip; ahead/behind badge with refactored GitCtr. (#14041, #13980) ### Mobile - Full settings menu and responsive profile layout. (#14019) - Agent route added to mobile router; mobile agent topic route registered. (#14103, #14158) - Session list skeleton row layout corrected. (#14040) ### Bot / Messaging - DM strategy support; bot emoji and markdown render optimization. (#14201, #14091, #14140) - Slack webhook fix; bot platform setup guide reference. (#14052, #14121) --- ## 🤖 Models & Providers ### New models - **Claude Opus 4.7** with `xhigh` effort tier; strip temperature/top_p. (#13903, #13909) - **GPT-5.5**. (#14147) - **DeepSeek V4** Flash/Pro cards with reasoning slider; cache-hit and Pro discount pricing. (#14114, #14209, #14196, #14131) - **Kimi K2.6** model with LobeHub-hosted card. (#14004, #14006) - **MiMo-V2.5 / V2.5-Pro**. (#14089) - **gpt-image-2**, **Qwen3.6 Flash/Plus**, **Pixverse-c1**. (#14039, #13923) ### New providers - **OpenCode Zen** and **OpenCode Go** with env-var support. (#13943, #14064) - **Azure OpenAI Router** runtime support. (#13823) - Model alias mapping for image and video runtimes. (#13896) - Seedance video models migrated to Dreamina. (#14144) ### Runtime reliability - Sanitize invalid tool_call arguments to unbreak strict providers. (#14033) - Tolerate null `function.name` in streaming tool_call deltas. (#14139) - Preserve Gemini 3 `thoughtSignature` in `call_tools_batch` normalization. (#14032) - Downgrade `image_url` parts when target model lacks vision. (#14029) - Preserve Cloudflare provider error context. (#14136) - Use `safety_identifier` for OpenAI Responses API. (#14148) - Unwrap underlying PG error in `formatErrorEventData`. (#14038) --- ## 🖥️ User Experience - **Onboarding** — Preset agent naming suggestions, structured hunk ops for `updateDocument`, persona analytics snapshot, footer promotion pipeline, wrap-up button. (#13931, #13989, #13930, #13853, #13934) - **Document workflow** — Agent documents promoted as primary workspace panel; history management and compare workflow; web-crawl docs associated with agent documents. (#13924, #13725, #13893) - **cmdk** — Agent identity surfaced on topic search results; topic/message search scoped to current agent. (#14204, #13960) - **Floating chat panel** and workspace improvements. (#13887) - **Topic completion status** with dropdown action and filter. (#14005) --- ## 🔧 Tooling - Redis-backed feature flag provider for runtime config. (#14098) - Vite upgraded to 8.0.0 with Rolldown strict execution order. (#12720, #14058) - `@lobechat/model-bank` automated npm release with provenance. (#14015, #14017, #14018) - Skill activation fallback when `activateTools` cannot find identifier. (#14010) - Cron tool: timezone and existing jobs injected into system prompt; clarified `lobe-gtd` and `lobe-cron` descriptions. (#14012, #14013) --- ## 🔒 Security & Reliability - **Security:** uuid bumped to v14 (advisory). (#14083) - **Security:** validate avatar URL and scope old-avatar deletion to owner. (#13982) - **Security:** clear OIDC sessions on better-auth signout; return 401 (not 500) for expired OIDC JWT. (#13916, #14014) - **Reliability:** scope pending-approval check to current assistant turn. (#14182) - **Reliability:** sanitize heterogeneous-agent attachment cache filenames. (#13937) - **Reliability:** reduce subagent task status error noise. (#14026) --- ## 👥 Contributors Huge thanks to **17 contributors** who shipped **194 merged PRs** this week. @hardy · @shaun0927 · @hezhijie0327 · @sxjeru · @arvinxx · @Innei · @tjx666 · @lijian · @neko · @rdmclin2 · @AmAzing129 · @sudongyuer · @CanisMinor · @rivertwilight Plus @lobehubbot and renovate[bot] for maintenance. --- **Full Changelog**: v2.1.52...v2.1.53
…y handler (#14838) The forwarding guard only filtered `stream_chunk` events. `tool_start` and `tool_end` for subagent inner tools still reached the main handler, where `tool_end` fired a `fetchAndReplaceMessages(main)` on every subagent inner tool result — wasted work AND a state-drift window that surfaced as the "orphan tool call" banner on the spawn's bubble even after DB had settled. `tool_start(subagent)` was also leaking `dispatchOnBeforeCall` invocations against the main context for what is actually a subagent inner tool, firing renderer onBeforeCall hooks in the wrong scope. Broadens the guard to drop ALL events with `event.data.subagent`. Safe because: - `tool_result(subagent)` is already handled inline at executor:1407 with an early `return`. - `stream_chunk(subagent)` is routed through `persistSubagent*Chunk` into the per-spawn thread scope; the subagent's own in-thread renderer state is streamed via the thread-scoped dispatcher introduced in #14024. - `tool_start` / `tool_end` are pure renderer-notification hooks; the subagent has no business firing them on the main bucket. Regression test asserts: - No forwarded event with `event.data.subagent` reaches the handler. - Main's own `tool_start` / `tool_end` (no subagent flag) still reach the handler so the main bubble's animation + onAfterCall hooks fire. Closes LOBE-8991. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Three coordinated commits addressing parallel-tool orphan, subagent thread streaming, and a buffer-flush durability concern surfaced during code review.
stream_chunkevents are no longer forwarded to the main-agent gateway handler. Under a parallel main tool_use ([Grep, Agent]), subagent inner tools no longer overwritemain.tools[]in memory, so main's Task/Agent tool_use stays paired with itsrole:'tool'message (orphan tool-call banner no longer triggers).parentOperationId = main opandcontext = { ...mainContext, threadId, scope: 'thread' }. The thread-scoped dispatcher binds to that sub-op's id, so every Thread bucket dispatch resolves through the standardinternal_getConversationContext→messageMapKeypath — no special-cased threadId override at the dispatch boundary. Cancel cascade + cleanup ride on the existing parent/child operation linkage.finalizeSubagentRunonly drains itsaccumulatedContent/accumulatedReasoningaftermessageService.updateMessageconfirms the write. Failure paths now keep the buffer AND pin the flush target onSubagentRunState.pendingFlushTargetso theonCompletefallback retries against the streaming turn's assistant — never against the terminal row that theresultContentbranch advancedcurrentAssistantMsgIdonto.The dispatcher streams Thread-bucket
createMessage(user/assistant/tool seeds) andupdateMessage(tools[]/content/reasoning/tool-result) on every chunk, so the Thread view streams token-by-token with the same cadence as the main bubble. Without it the Thread stayed empty until SWR re-fetched on next open (fetchAndReplaceMessagesis main-topic scoped).Test plan
heterogeneousAgentExecutor.test.ts40/40 pass, incl. three regressions:does NOT forward subagent-tagged stream_chunks to the gateway handler— main-bucket isolation under parallel main+subagent tool use.streams subagent create/update dispatches via a thread-scoped sub-operation— verifies (1)startOperationis called withtype: 'subagentThread', parented to the main op, with the Thread's ConversationContext; (2) every Thread bucketcreateMessage/updateMessagecarries the sub-op id; (3) tool result lands on the in-thread tool message id; (4) no main-bucket leak; (5)completeOperation(subOpId)fires on finalize.retains subagent buffers + pinned target when the finalize flush fails— stubsupdateMessageto throw once on the streaming write, asserts the leftover content reaches DB across ≥2 attempts and every attempt targets the streaming turn's assistant — not the terminal row.bun run type-check).internal_dispatchMessageis back to its original{ operationId? }signature, no leaky parallel context path remains in the store boundary.[Grep, Agent]):tools = [Grep, Agent]; all 4role:'tool'rows paired to their parent assistant; orphan DOM node count = 0.[Bash, Glob]then[Read×8]+ 10 tool results); Thread view renders user→assistant(tools + streaming content).mainOrphans: [],threadOrphans: [],threadIntoMainBleed: [].🤖 Generated with Claude Code