🐛 fix(hetero-agent): keep parentId chain across toolless middle steps#14839
🐛 fix(hetero-agent): keep parentId chain across toolless middle steps#14839arvinxx wants to merge 1 commit into
Conversation
… steps LOBE-8993: when a CC step produced only text (e.g. Monitor stdout drove Claude to reply without invoking a tool), the next step's parentId fell back to the previous assistant. MessageCollector only walks the assistant → tool → assistant zigzag, so each Monitor stdout line split into its own bubble. Carry the most recent tool result_msg_id across step boundaries via a `lastToolMsgIdEver` tracker so toolless middle steps still chain back to the originating tool result. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
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: 69d42c6c28
ℹ️ 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".
| // Prefer this step's last tool, then the most recent tool ever seen | ||
| // in the run (rescues toolless middle steps — see LOBE-8993), then | ||
| // the previous assistant as a last resort. | ||
| const stepParentId = lastToolMsgId ?? lastToolMsgIdEver ?? currentAssistantMessageId; |
There was a problem hiding this comment.
Preserve a linear chain for toolless steps
When two or more text-only steps follow the same tool, this fallback assigns every new assistant the same lastToolMsgIdEver parent, making them sibling children of that tool. MessageCollector.collectAssistantChain only follows the first assistant child of each tool and then returns, so the later Monitor replies (and the subsequent tool-using assistant) still render as separate bubbles/branches instead of one chain. The new consecutive-toolless test asserts the duplicated parent IDs but does not exercise the collector behavior that this change is meant to fix.
Useful? React with 👍 / 👎.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## canary #14839 +/- ##
===========================================
- Coverage 82.74% 65.88% -16.86%
===========================================
Files 771 2958 +2187
Lines 61950 261203 +199253
Branches 9492 31714 +22222
===========================================
+ Hits 51261 172100 +120839
- Misses 10531 88945 +78414
Partials 158 158
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
💻 Change Type
🔗 Related Issue
Fixes LOBE-8993
Related to LOBE-7365
🔀 Description of Change
When a Claude Code step produced only text (no
tool_use) — for exampleMonitor pushing stdout that drove Claude to reply "等 list 完。" without
invoking a tool — the next step's
parentIdfell back to the previousassistant message.
MessageCollector.collectAssistantChainonly walksthe
assistant → tool → assistantzigzag, so each Monitor stdout linesplit off into its own bubble, every one re-rendering the model label.
The executor tracks tool persistence in
toolState.payloads, which iscleared on every step boundary. So when a step had no tools,
lastToolMsgIdcame back empty andstepParentIddefaulted tocurrentAssistantMessageId(the toolless assistant) — producing anassistant → assistantlink that the UI couldn't aggregate.Fix: introduce a run-scoped
lastToolMsgIdEvertracker that survivesstep boundaries (resets only when a new executor spawns, i.e. on a new
user message). Step boundary now resolves
stepParentIdin this order:lastToolMsgId— last tool in this step (normal zigzag)lastToolMsgIdEver— last tool ever seen in the run (rescuesconsecutive toolless middle steps)
currentAssistantMessageId— pure-text run with no tools at allThe reader (
MessageCollector) is untouched; the semantics it expectsare now produced correctly on the writer side.
🧪 How to Test
Test changes in
heterogeneousAgentExecutor.test.ts:LOBE-7365 Monitor parentId chain > toolless middle step—asserts Step 2's
parentIdis the Step 0 Monitor tool (tool-1),not the toolless Step 1 assistant.
LOBE-7365 Monitor parentId chain > consecutive toolless steps— replays 3 consecutive toolless text-only steps after a Monitor
tool_use; asserts all 4 follow-up assistants chain back to
tool-1.should fall back to assistant parentId when step has no toolsstill passes (covers the pure-text run case where thefallback is
currentAssistantMessageId).Run:
bunx vitest run --silent='passed-only' src/store/chat/slices/aiChat/actions/__tests__/heterogeneousAgentExecutor.test.ts53/53 passing.
📝 Additional Information
Impact: all Claude Code desktop sessions that use long-running
Monitor(and similar streaming tools —tail -f, CI watch, migrationprogress loops). Beyond UI bubble fragmentation, the broken
assistant → assistantchain could misleadfindLastMessageIdandsession resume parent-pointer logic.
🤖 Generated with Claude Code