fix(cli): persist skipped duplicate tool results

This commit is contained in:
yiliang114 2026-08-20 22:28:30 +08:00
parent a20a724ec0
commit 71f23b79c7
2 changed files with 62 additions and 6 deletions

View file

@ -28500,7 +28500,7 @@ describe('Session', () => {
});
});
it('drops repeated duplicate provider functionCall ids after the first synthetic response', async () => {
it('records repeated duplicate provider calls without returning results', async () => {
const execute = vi.fn().mockResolvedValue({
llmContent: 'should not run',
returnDisplay: 'should not run',
@ -28529,6 +28529,7 @@ describe('Session', () => {
],
]),
);
const usedIds = new Set(['shell_1']);
const [duplicatePart] = core.normalizeModelToolCallIds(
[
{
@ -28539,7 +28540,7 @@ describe('Session', () => {
},
},
],
new Set(['shell_1']),
usedIds,
new Set<string>(),
);
const duplicateCall = duplicatePart.functionCall!;
@ -28549,6 +28550,19 @@ describe('Session', () => {
).runToolCalls(new AbortController().signal, 'prompt-history-dup', [
duplicateCall,
]);
const [repeatedPart] = core.normalizeModelToolCallIds(
[
{
functionCall: {
id: 'shell_1',
name: 'read_file',
args: { file_path: 'b.ts' },
},
},
],
usedIds,
new Set<string>(),
);
const toolLoopState: DaemonToolLoopState = {
totalToolCalls: 0,
invalidToolParamErrors: new Map<string, number>(),
@ -28558,13 +28572,14 @@ describe('Session', () => {
repeatedToolFailureMode: 'off',
repeatedToolFailureState: createRepeatedToolFailureGuardState(),
};
expect(repeatedPart.functionCall?.id).toBe('shell_1__qwen_dup_3');
const secondResult = await (
session as unknown as ToolCallInternals
).runToolCalls(
new AbortController().signal,
'prompt-history-dup',
[
duplicateCall,
repeatedPart.functionCall!,
{ id: 'fresh_shell', name: 'read_file', args: { file_path: 'c.ts' } },
],
toolLoopState,
@ -28589,9 +28604,39 @@ describe('Session', () => {
core.LoopType.GLOBAL_TOOL_CALL_DUPLICATE,
);
expect(mockChatRecordingService.recordToolResult).toHaveBeenCalledTimes(
1,
3,
);
expect(mockClient.sessionUpdate).toHaveBeenCalledTimes(1);
expect(
mockChatRecordingService.recordToolResult.mock.calls
.slice(1)
.map(([parts, metadata]) => ({
callId: metadata.callId,
responseId: parts[0]?.functionResponse?.id,
error: parts[0]?.functionResponse?.response?.['error'],
status: metadata.status,
executionStatus: metadata.executionStatus,
})),
).toEqual([
{
callId: 'shell_1__qwen_dup_3',
responseId: 'shell_1__qwen_dup_3',
error: expect.stringContaining(
'loop detection stopped the current turn',
),
status: 'error',
executionStatus: 'not_started',
},
{
callId: 'fresh_shell',
responseId: 'fresh_shell',
error: expect.stringContaining(
'loop detection stopped the current turn',
),
status: 'error',
executionStatus: 'not_started',
},
]);
expect(mockClient.sessionUpdate).toHaveBeenCalledTimes(3);
});
it('suppresses duplicate TodoWrite calls without emitting plan updates', async () => {

View file

@ -9055,11 +9055,22 @@ export class Session implements SessionContext {
} else {
debugLogger.warn(message);
}
return await finalizeRunToolResult({
await Promise.all(
dedupedFunctionCalls.map((fc) =>
recordSkippedToolCall(
fc,
LOOP_DETECTED_SKIP_MESSAGE,
false,
ToolErrorType.UNKNOWN,
),
),
);
const result = await finalizeRunToolResult({
parts: [],
stopAfterPermissionCancel: false,
loopDetected: true,
});
return { ...result, parts: [] };
}
const pushDuplicateBatch = (