From f4f11a0a4219151782befa40fb5509637401a3e8 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 6 May 2026 09:59:10 +0200 Subject: [PATCH] Fix Cursor tool-call normalization duplicates --- src/adapters/cursor-agent.ts | 46 ++++++++++++++++++++++++++---- test/adapters/cursor-agent.test.ts | 4 +++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/adapters/cursor-agent.ts b/src/adapters/cursor-agent.ts index 5cf3410..1f25fc3 100644 --- a/src/adapters/cursor-agent.ts +++ b/src/adapters/cursor-agent.ts @@ -71,6 +71,9 @@ export class CursorAgentAdapter extends BaseAdapter implements RunnerAdapter { const observedReads: string[] = []; const explicitSkillNames = new Set(); const seenToolCalls = new Set(); + const seenCommands = new Set(); + const seenReadPaths = new Set(); + const seenSkillSignals = new Set(); const callBaseDirs = new Map(); let sessionCwd = input.cwd; @@ -110,7 +113,7 @@ export class CursorAgentAdapter extends BaseAdapter implements RunnerAdapter { continue; } - const callId = readString(record, "call_id"); + const callId = readCursorToolCallId(record); const isCompleted = readString(record, "subtype") === "completed"; const hasEmittedCall = callId !== undefined && seenToolCalls.has(callId); const baseDir = @@ -118,7 +121,7 @@ export class CursorAgentAdapter extends BaseAdapter implements RunnerAdapter { ? resolveToolBaseDir(toolCall.args, sessionCwd) : resolveCursorCallBaseDir(callId, toolCall.args, sessionCwd, callBaseDirs); - if (!hasEmittedCall) { + if (!hasEmittedCall && !isCompleted) { events.push({ type: "toolCall", tool: toolCall.tool, @@ -132,11 +135,14 @@ export class CursorAgentAdapter extends BaseAdapter implements RunnerAdapter { } const command = extractCommand(toolCall.tool, toolCall.args); - if (command !== undefined) { + if (command !== undefined && shouldEmitCursorCallDetail(seenCommands, callId, command)) { events.push({ type: "command", command, at }); for (const filePath of extractFilePathsFromCommand(command)) { const resolvedPath = resolveReportedPath(filePath, baseDir); - if (resolvedPath === undefined) { + if ( + resolvedPath === undefined || + !shouldEmitCursorCallDetail(seenReadPaths, callId, resolvedPath) + ) { continue; } @@ -148,14 +154,20 @@ export class CursorAgentAdapter extends BaseAdapter implements RunnerAdapter { const readPath = extractReadPath(toolCall.tool, toolCall.args); if (readPath !== undefined) { const resolvedPath = resolveReportedPath(readPath, baseDir); - if (resolvedPath !== undefined) { + if ( + resolvedPath !== undefined && + shouldEmitCursorCallDetail(seenReadPaths, callId, resolvedPath) + ) { observedReads.push(resolvedPath); events.push({ type: "fileRead", path: resolvedPath, at }); } } const skillName = extractSkillName(toolCall.tool, toolCall.args); - if (skillName !== undefined) { + if ( + skillName !== undefined && + shouldEmitCursorCallDetail(seenSkillSignals, callId, skillName) + ) { explicitSkillNames.add(skillName); events.push({ type: "skillSignal", skill: skillName, signal: "tool:skill", at }); } @@ -297,6 +309,10 @@ function readTimestamp(record: Record): string | undefined { return typeof timestamp === "number" ? new Date(timestamp).toISOString() : undefined; } +function readCursorToolCallId(record: Record): string | undefined { + return readString(record, "call_id") ?? readString(record, "model_call_id"); +} + function extractMessageText(message: Record): string { const content = message.content; if (!Array.isArray(content)) { @@ -396,6 +412,24 @@ function readToolBaseDir(args: unknown): string | undefined { return readString(args, "workingDirectory") ?? readString(args, "cwd"); } +function shouldEmitCursorCallDetail( + seenValues: Set, + callId: string | undefined, + value: string, +): boolean { + if (callId === undefined) { + return true; + } + + const key = `${callId}\u0000${value}`; + if (seenValues.has(key)) { + return false; + } + + seenValues.add(key); + return true; +} + function stringifyUnknown(value: unknown): string { if (typeof value === "string") { return value; diff --git a/test/adapters/cursor-agent.test.ts b/test/adapters/cursor-agent.test.ts index aecc19e..936438f 100644 --- a/test/adapters/cursor-agent.test.ts +++ b/test/adapters/cursor-agent.test.ts @@ -173,6 +173,8 @@ test("CursorAgentAdapter normalize extracts commands, file reads, tool results, expect(report.detectedSkills).toEqual( expect.arrayContaining([expect.objectContaining({ skill: "find-skills" })]), ); + expect(report.events.filter((event) => event.type === "toolCall")).toHaveLength(1); + expect(report.events.filter((event) => event.type === "command")).toHaveLength(1); expect(report.events).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -242,6 +244,7 @@ test("CursorAgentAdapter normalize resolves shell reads against stored call work expect(report.files.observedReads).toEqual([ "/tmp/isolated-workspace/skills/find-skills/SKILL.md", ]); + expect(report.events.filter((event) => event.type === "fileRead")).toHaveLength(1); expect(report.events).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -297,6 +300,7 @@ test("CursorAgentAdapter normalize resolves read tool calls against stored call expect(report.detectedSkills).toEqual( expect.arrayContaining([expect.objectContaining({ skill: "find-skills" })]), ); + expect(report.events.filter((event) => event.type === "fileRead")).toHaveLength(1); expect(report.events).toEqual( expect.arrayContaining([ expect.objectContaining({