Skip to content
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
14 changes: 9 additions & 5 deletions src/core/task/AutoApprovalHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface AutoApprovalResult {
export class AutoApprovalHandler {
private consecutiveAutoApprovedRequestsCount: number = 0
private consecutiveAutoApprovedCost: number = 0
private costResetMessageIndex: number = 0

/**
* Check if auto-approval limits have been reached and handle user approval if needed
Expand Down Expand Up @@ -91,8 +92,9 @@ export class AutoApprovalHandler {
): Promise<AutoApprovalResult> {
const maxCost = state?.allowedMaxCost || Infinity

// Calculate total cost from messages
this.consecutiveAutoApprovedCost = getApiMetrics(messages).totalCost
// Calculate total cost from messages after the last reset point
const messagesAfterReset = messages.slice(this.costResetMessageIndex)
this.consecutiveAutoApprovedCost = getApiMetrics(messagesAfterReset).totalCost

// Use epsilon for floating-point comparison to avoid precision issues
const EPSILON = 0.0001
Expand All @@ -104,8 +106,9 @@ export class AutoApprovalHandler {

// If we get past the promise, it means the user approved and did not start a new task
if (response === "yesButtonClicked") {
// Note: We don't reset the cost to 0 here because the actual cost
// is calculated from the messages. This is different from the request count.
// Reset the cost tracking by recording the current message count
// Future cost calculations will only include messages after this point
this.costResetMessageIndex = messages.length
return {
shouldProceed: true,
requiresApproval: true,
Expand All @@ -126,10 +129,11 @@ export class AutoApprovalHandler {
}

/**
* Reset the request counter (typically called when starting a new task)
* Reset the request counter and cost tracking (typically called when starting a new task)
*/
resetRequestCount(): void {
this.consecutiveAutoApprovedRequestsCount = 0
this.costResetMessageIndex = 0
}

/**
Expand Down
58 changes: 54 additions & 4 deletions src/core/task/__tests__/AutoApprovalHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,67 @@ describe("AutoApprovalHandler", () => {
expect(result3.requiresApproval).toBe(true)
})

it("should not reset cost to zero on approval", async () => {
it("should reset cost tracking on approval", async () => {
const messages: ClineMessage[] = [
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 1000 },
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 2000 },
]

// First check - cost exceeds limit (6.0 > 5.0)
mockGetApiMetrics.mockReturnValue({ totalCost: 6.0 })
mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" })

const result1 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
expect(result1.shouldProceed).toBe(true)
expect(result1.requiresApproval).toBe(true)

// Add more messages after reset
messages.push(
{ type: "say", say: "api_req_started", text: '{"cost": 2.0}', ts: 3000 },
{ type: "say", say: "api_req_started", text: '{"cost": 1.0}', ts: 4000 },
)

// Second check - should only count messages after reset (3.0 < 5.0)
mockGetApiMetrics.mockReturnValue({ totalCost: 3.0 })
const result2 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)

// Should not require approval since cost after reset is under limit
expect(result2.shouldProceed).toBe(true)
expect(result2.requiresApproval).toBe(false)

// Verify it's only calculating cost from messages after reset point
expect(mockGetApiMetrics).toHaveBeenLastCalledWith(messages.slice(2))
})

it("should track multiple cost resets correctly", async () => {
const messages: ClineMessage[] = []

// First cost limit hit
messages.push({ type: "say", say: "api_req_started", text: '{"cost": 6.0}', ts: 1000 })
mockGetApiMetrics.mockReturnValue({ totalCost: 6.0 })
mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" })

await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)

// Cost should still be calculated from messages, not reset
const state = handler.getApprovalState()
expect(state.currentCost).toBe(6.0)
// Add more messages
messages.push(
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 2000 },
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 3000 },
)

// Second cost limit hit (only counting from index 1)
mockGetApiMetrics.mockReturnValue({ totalCost: 6.0 })
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)

// Add more messages after second reset
messages.push({ type: "say", say: "api_req_started", text: '{"cost": 2.0}', ts: 4000 })

// Third check - should only count from last reset
mockGetApiMetrics.mockReturnValue({ totalCost: 2.0 })
const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)

expect(result.requiresApproval).toBe(false)
expect(mockGetApiMetrics).toHaveBeenLastCalledWith(messages.slice(3))
})
})

Expand Down
Loading