Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/telemetry/src/PostHogTelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
shouldReportApiErrorToTelemetry,
isApiProviderError,
extractApiProviderErrorProperties,
isConsecutiveMistakeError,
extractConsecutiveMistakeErrorProperties,
} from "@roo-code/types"

import { BaseTelemetryClient } from "./BaseTelemetryClient"
Expand Down Expand Up @@ -100,13 +102,16 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
console.info(`[PostHogTelemetryClient#captureException] ${error.message}`)
}

// Auto-extract properties from ApiProviderError and merge with additionalProperties.
// Auto-extract properties from known error types and merge with additionalProperties.
// Explicit additionalProperties take precedence over auto-extracted properties.
let mergedProperties = additionalProperties

if (isApiProviderError(error)) {
const extractedProperties = extractApiProviderErrorProperties(error)
mergedProperties = { ...extractedProperties, ...additionalProperties }
} else if (isConsecutiveMistakeError(error)) {
const extractedProperties = extractConsecutiveMistakeErrorProperties(error)
mergedProperties = { ...extractedProperties, ...additionalProperties }
}

// Override the error message with the extracted error message.
Expand Down
127 changes: 127 additions & 0 deletions packages/types/src/__tests__/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
ApiProviderError,
isApiProviderError,
extractApiProviderErrorProperties,
ConsecutiveMistakeError,
isConsecutiveMistakeError,
extractConsecutiveMistakeErrorProperties,
} from "../telemetry.js"

describe("telemetry error utilities", () => {
Expand Down Expand Up @@ -389,4 +392,128 @@ describe("telemetry error utilities", () => {
expect(properties).toHaveProperty("errorCode", 0)
})
})

describe("ConsecutiveMistakeError", () => {
it("should create an error with correct properties", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 5, 3, "no_tools_used")

expect(error.message).toBe("Test error")
expect(error.name).toBe("ConsecutiveMistakeError")
expect(error.taskId).toBe("task-123")
expect(error.consecutiveMistakeCount).toBe(5)
expect(error.consecutiveMistakeLimit).toBe(3)
expect(error.reason).toBe("no_tools_used")
})

it("should be an instance of Error", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3)
expect(error).toBeInstanceOf(Error)
})

it("should handle zero values", () => {
const error = new ConsecutiveMistakeError("Zero test", "task-000", 0, 0)

expect(error.taskId).toBe("task-000")
expect(error.consecutiveMistakeCount).toBe(0)
expect(error.consecutiveMistakeLimit).toBe(0)
})

it("should default reason to unknown when not provided", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3)
expect(error.reason).toBe("unknown")
})

it("should accept tool_repetition reason", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3, "tool_repetition")
expect(error.reason).toBe("tool_repetition")
})

it("should accept no_tools_used reason", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3, "no_tools_used")
expect(error.reason).toBe("no_tools_used")
})
})

describe("isConsecutiveMistakeError", () => {
it("should return true for ConsecutiveMistakeError instances", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 3, 3)
expect(isConsecutiveMistakeError(error)).toBe(true)
})

it("should return false for regular Error instances", () => {
const error = new Error("Test error")
expect(isConsecutiveMistakeError(error)).toBe(false)
})

it("should return false for ApiProviderError instances", () => {
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
expect(isConsecutiveMistakeError(error)).toBe(false)
})

it("should return false for null and undefined", () => {
expect(isConsecutiveMistakeError(null)).toBe(false)
expect(isConsecutiveMistakeError(undefined)).toBe(false)
})

it("should return false for non-error objects", () => {
expect(isConsecutiveMistakeError({})).toBe(false)
expect(
isConsecutiveMistakeError({
taskId: "task-123",
consecutiveMistakeCount: 3,
consecutiveMistakeLimit: 3,
}),
).toBe(false)
})

it("should return false for Error with wrong name", () => {
const error = new Error("Test error")
error.name = "CustomError"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).taskId = "task-123"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).consecutiveMistakeCount = 3
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).consecutiveMistakeLimit = 3
expect(isConsecutiveMistakeError(error)).toBe(false)
})
})

describe("extractConsecutiveMistakeErrorProperties", () => {
it("should extract all properties from ConsecutiveMistakeError", () => {
const error = new ConsecutiveMistakeError("Test error", "task-123", 5, 3, "no_tools_used")
const properties = extractConsecutiveMistakeErrorProperties(error)

expect(properties).toEqual({
taskId: "task-123",
consecutiveMistakeCount: 5,
consecutiveMistakeLimit: 3,
reason: "no_tools_used",
})
})

it("should handle zero values correctly", () => {
const error = new ConsecutiveMistakeError("Zero test", "task-000", 0, 0)
const properties = extractConsecutiveMistakeErrorProperties(error)

expect(properties).toEqual({
taskId: "task-000",
consecutiveMistakeCount: 0,
consecutiveMistakeLimit: 0,
reason: "unknown",
})
})

it("should handle large numbers", () => {
const error = new ConsecutiveMistakeError("Large test", "task-large", 1000, 500, "tool_repetition")
const properties = extractConsecutiveMistakeErrorProperties(error)

expect(properties).toEqual({
taskId: "task-large",
consecutiveMistakeCount: 1000,
consecutiveMistakeLimit: 500,
reason: "tool_repetition",
})
})
})
})
50 changes: 50 additions & 0 deletions packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,53 @@ export function extractApiProviderErrorProperties(error: ApiProviderError): Reco
...(error.errorCode !== undefined && { errorCode: error.errorCode }),
}
}

/**
* Reason why the consecutive mistake limit was reached.
*/
export type ConsecutiveMistakeReason = "no_tools_used" | "tool_repetition" | "unknown"

/**
* Error class for "Roo is having trouble" consecutive mistake scenarios.
* Triggered when the task reaches the configured consecutive mistake limit.
* Used for structured exception tracking via PostHog.
*/
export class ConsecutiveMistakeError extends Error {
constructor(
message: string,
public readonly taskId: string,
public readonly consecutiveMistakeCount: number,
public readonly consecutiveMistakeLimit: number,
public readonly reason: ConsecutiveMistakeReason = "unknown",
) {
super(message)
this.name = "ConsecutiveMistakeError"
}
}

/**
* Type guard to check if an error is a ConsecutiveMistakeError.
* Used by telemetry to automatically extract structured properties.
*/
export function isConsecutiveMistakeError(error: unknown): error is ConsecutiveMistakeError {
return (
error instanceof Error &&
error.name === "ConsecutiveMistakeError" &&
"taskId" in error &&
"consecutiveMistakeCount" in error &&
"consecutiveMistakeLimit" in error
)
}

/**
* Extracts properties from a ConsecutiveMistakeError for telemetry.
* Returns the structured properties that can be merged with additionalProperties.
*/
export function extractConsecutiveMistakeErrorProperties(error: ConsecutiveMistakeError): Record<string, unknown> {
return {
taskId: error.taskId,
consecutiveMistakeCount: error.consecutiveMistakeCount,
consecutiveMistakeLimit: error.consecutiveMistakeLimit,
reason: error.reason,
}
}
13 changes: 11 additions & 2 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { serializeError } from "serialize-error"
import { Anthropic } from "@anthropic-ai/sdk"

import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
import { ConsecutiveMistakeError } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"

import { t } from "../../i18n"
Expand Down Expand Up @@ -776,8 +777,16 @@ export async function presentAssistantMessage(cline: Task) {
// Add user feedback to chat.
await cline.say("user_feedback", text, images)

// Track tool repetition in telemetry.
TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
Comment thread
daniel-lxs marked this conversation as resolved.
// Track tool repetition in telemetry via PostHog exception tracking.
TelemetryService.instance.captureException(
new ConsecutiveMistakeError(
`Tool repetition limit reached for ${block.name}`,
cline.taskId,
cline.consecutiveMistakeCount,
cline.consecutiveMistakeLimit,
"tool_repetition",
),
)
}

// Return tool result message about the repetition
Expand Down
15 changes: 13 additions & 2 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
MAX_CHECKPOINT_TIMEOUT_SECONDS,
MIN_CHECKPOINT_TIMEOUT_SECONDS,
TOOL_PROTOCOL,
ConsecutiveMistakeError,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
Expand Down Expand Up @@ -2236,8 +2237,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

await this.say("user_feedback", text, images)

// Track consecutive mistake errors in telemetry.
TelemetryService.instance.captureConsecutiveMistakeError(this.taskId)
// Track consecutive mistake errors in telemetry via PostHog exception tracking.
// The reason is "no_tools_used" because this limit is reached via initiateTaskLoop
// which increments consecutiveMistakeCount when the model doesn't use any tools.
TelemetryService.instance.captureException(
new ConsecutiveMistakeError(
`Task reached consecutive mistake limit (${this.consecutiveMistakeLimit})`,
this.taskId,
this.consecutiveMistakeCount,
this.consecutiveMistakeLimit,
"no_tools_used",
),
)
}

this.consecutiveMistakeCount = 0
Expand Down
Loading