Avoid duplicate Claude result text (#1095)

This commit is contained in:
Mohamed Boudra
2026-05-19 17:43:26 +08:00
committed by GitHub
parent 0f6641c8c0
commit 0a2307d199
2 changed files with 76 additions and 5 deletions
@@ -1461,4 +1461,64 @@ describe("ClaudeAgentSession context window usage", () => {
expect(timelineEvents).toEqual([]);
expect(events.some((event) => event.type === "turn_completed")).toBe(true);
});
test("result.result is not duplicated when assistant text already streamed with zero token usage", async () => {
const queryFactory = createQueryFactoryForTurns([
[
{
type: "system",
subtype: "init",
session_id: "session-third-party",
permissionMode: "default",
},
{
type: "assistant",
message: {
id: "assistant-third-party-1",
role: "assistant",
content: [{ type: "text", text: "Here is the answer." }],
usage: {
input_tokens: 0,
output_tokens: 0,
},
},
session_id: "session-third-party",
uuid: "assistant-third-party-event-1",
},
{
type: "result",
subtype: "success",
result: "Here is the answer.",
is_error: false,
duration_ms: 100,
duration_api_ms: 80,
num_turns: 1,
stop_reason: null,
total_cost_usd: 0.01,
usage: {
input_tokens: 10,
cache_read_input_tokens: 0,
output_tokens: 0,
},
permission_denials: [],
uuid: "result-third-party-1",
session_id: "session-third-party",
},
],
]);
const client = new ClaudeAgentClient({
logger,
queryFactory,
resolveBinary: async () => "/test/claude/bin",
});
const session = await client.createSession({
provider: "claude",
cwd: process.cwd(),
});
const result = await session.run("turn");
await session.close();
expect(result.timeline).toEqual([{ type: "assistant_message", text: "Here is the answer." }]);
});
});
@@ -1568,6 +1568,7 @@ class ClaudeAgentSession implements AgentSession {
private pendingInterruptAbort = false;
private lastForegroundPromptText: string | null = null;
private foregroundHasVisibleActivity = false;
private activeTurnHasAssistantText = false;
private lastContextWindowUsedTokens: number | undefined;
private lastContextWindowMaxTokens: number | undefined;
private lastStreamRequestInputTokens: number | undefined;
@@ -1692,6 +1693,7 @@ class ClaudeAgentSession implements AgentSession {
const turnId = this.createTurnId("foreground");
this.activeForegroundTurnId = turnId;
this.foregroundHasVisibleActivity = false;
this.activeTurnHasAssistantText = false;
this.transitionTurnState("foreground", "foreground turn started");
this.clearRecentStderr();
@@ -2630,6 +2632,7 @@ class ClaudeAgentSession implements AgentSession {
this.activeForegroundTurnId = null;
this.lastForegroundPromptText = null;
this.cancelCurrentTurn = null;
this.activeTurnHasAssistantText = false;
this.syncTurnState("foreground turn terminal");
}
@@ -2645,9 +2648,11 @@ class ClaudeAgentSession implements AgentSession {
this.activeForegroundTurnId = null;
this.lastForegroundPromptText = null;
this.cancelCurrentTurn = null;
this.activeTurnHasAssistantText = false;
this.syncTurnState("foreground turn terminal");
} else if (this.autonomousTurn) {
this.autonomousTurn = null;
this.activeTurnHasAssistantText = false;
this.syncTurnState("autonomous turn terminal");
}
}
@@ -2660,6 +2665,7 @@ class ClaudeAgentSession implements AgentSession {
this.autonomousTurn = {
id: this.createTurnId("autonomous"),
};
this.activeTurnHasAssistantText = false;
this.notifySubscribers({ type: "turn_started", provider: "claude" });
this.syncTurnState("autonomous turn started");
}
@@ -2670,6 +2676,7 @@ class ClaudeAgentSession implements AgentSession {
}
this.notifySubscribers({ type: "turn_completed", provider: "claude" });
this.autonomousTurn = null;
this.activeTurnHasAssistantText = false;
this.syncTurnState("autonomous turn completed");
}
@@ -2898,7 +2905,6 @@ class ClaudeAgentSession implements AgentSession {
if (events.length === 0) {
return;
}
if (
this.pendingInterruptAbort &&
message.type === "result" &&
@@ -2909,6 +2915,11 @@ class ClaudeAgentSession implements AgentSession {
this.logger.debug("Suppressing stale Claude interrupt terminal result");
return;
}
if (
events.some((event) => event.type === "timeline" && event.item.type === "assistant_message")
) {
this.activeTurnHasAssistantText = true;
}
if (
this.activeForegroundTurnId &&
events.some(
@@ -3231,12 +3242,12 @@ class ClaudeAgentSession implements AgentSession {
if (message.subtype === "success") {
// Built-in slash commands (e.g. /voice, /usage, "Unknown command: …")
// run client-side in the Claude CLI with no model turn — output_tokens
// is 0 and the user-visible text is carried in `result`. Surface it as
// an assistant message so the turn doesn't end silently. Normal turns
// have output_tokens > 0 and their text is already in the stream.
// is 0 and the user-visible text is carried in `result`. Surface it only
// when the turn has not already emitted assistant text so zero-token
// accounting from provider gateways does not duplicate streamed output.
const resultText = typeof message.result === "string" ? message.result.trim() : "";
const outputTokens = message.usage?.output_tokens;
if (resultText.length > 0 && outputTokens === 0) {
if (resultText.length > 0 && outputTokens === 0 && !this.activeTurnHasAssistantText) {
events.push({
type: "timeline",
provider: "claude",