fix(cli,web): group orphaned subagent trace by parentToolUseId (#1175)

This commit is contained in:
Junmo Kim
2026-07-26 23:01:46 +08:00
committed by GitHub
parent d90bde0b88
commit deb5d63695
8 changed files with 249 additions and 3 deletions
+5
View File
@@ -29,6 +29,11 @@ const RawJSONLinesBaseSchema = z.object({
uuid: z.string().optional(),
parentUuid: z.string().nullable().optional(),
isSidechain: z.boolean().optional(),
// The tool_use id of the Agent/Task tool_use that spawned this sidechain
// message, when present. Preserved (not just consumed) so downstream (web
// tracer) can group sidechain messages directly by this id rather than
// solely by exact-matching a sidechain root's prompt text.
parentToolUseId: z.string().optional(),
isMeta: z.boolean().optional(),
isCompactSummary: z.boolean().optional(),
userType: z.string().optional(),
@@ -1041,6 +1041,63 @@ describe('SDKToLogConverter', () => {
})
})
describe('Sidechain parentToolUseId preservation (subagent trace grouping fix)', () => {
it('preserves parent_tool_use_id as parentToolUseId on sidechain user messages', () => {
const sdkMessage = {
type: 'user',
parent_tool_use_id: 'toolu_abc123',
message: { role: 'user', content: 'sidechain prompt' }
} as unknown as SDKUserMessage
const logMessage = converter.convert(sdkMessage) as any
expect(logMessage?.isSidechain).toBe(true)
expect(logMessage?.parentToolUseId).toBe('toolu_abc123')
})
it('preserves parent_tool_use_id on sidechain assistant messages (subagent turns)', () => {
const sdkMessage = {
type: 'assistant',
parent_tool_use_id: 'toolu_abc123',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'subagent reply' }]
}
} as unknown as SDKAssistantMessage
const logMessage = converter.convert(sdkMessage) as any
expect(logMessage?.isSidechain).toBe(true)
expect(logMessage?.parentToolUseId).toBe('toolu_abc123')
})
it('does not set parentToolUseId on non-sidechain (top-level) messages', () => {
const sdkMessage: SDKUserMessage = {
type: 'user',
message: { role: 'user', content: 'top-level message' }
}
const logMessage = converter.convert(sdkMessage) as any
expect(logMessage?.isSidechain).toBe(false)
expect(logMessage?.parentToolUseId).toBeUndefined()
})
it('preserves parentToolUseId on interrupted sidechain tool results', () => {
const logMessage = converter.generateInterruptedToolResult('toolu_child', 'toolu_parent') as any
expect(logMessage?.isSidechain).toBe(true)
expect(logMessage?.parentToolUseId).toBe('toolu_parent')
})
it('does not set parentToolUseId on interrupted top-level tool results', () => {
const logMessage = converter.generateInterruptedToolResult('toolu_child') as any
expect(logMessage?.isSidechain).toBe(false)
expect(logMessage?.parentToolUseId).toBeUndefined()
})
})
describe('Convenience function', () => {
it('should convert single message without state', () => {
const sdkMessage: SDKUserMessage = {
@@ -235,14 +235,22 @@ export class SDKToLogConverter {
const timestamp = new Date().toISOString()
let parentUuid = this.lastUuid;
let isSidechain = false;
// Preserved (not just consumed) so the web tracer can group sidechain
// messages directly by the spawning Agent tool_use id, instead of relying
// solely on the SDK emitting a prompt-holding sidechain root to exact-match
// against. Some subagents (e.g. background/task_started) never emit that
// root, orphaning every child that only carries this id.
let parentToolUseId: string | undefined;
if (sdkMessage.parent_tool_use_id) {
isSidechain = true;
parentToolUseId = (sdkMessage as any).parent_tool_use_id;
parentUuid = this.sidechainLastUUID.get((sdkMessage as any).parent_tool_use_id) ?? null;
this.sidechainLastUUID.set((sdkMessage as any).parent_tool_use_id!, uuid);
}
const baseFields = {
parentUuid: parentUuid,
isSidechain: isSidechain,
parentToolUseId,
userType: 'external' as const,
cwd: this.context.cwd,
sessionId: this.context.sessionId,
@@ -506,6 +514,7 @@ export class SDKToLogConverter {
const logMessage: RawJSONLines = {
type: 'user',
isSidechain: isSidechain,
parentToolUseId: parentToolUseId ?? undefined,
uuid,
message: {
role: 'user',