Skip to content

Commit ed1e6db

Browse files
committed
fix: properly reset cost limit tracking when user clicks "Reset and Continue"
- Track the message index at the time of cost reset - Only calculate costs from messages after the reset point - Prevents the issue where cost limit immediately triggers again after reset Fixes #6889
1 parent 3ee6072 commit ed1e6db

File tree

2 files changed

+63
-9
lines changed

2 files changed

+63
-9
lines changed

src/core/task/AutoApprovalHandler.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface AutoApprovalResult {
1212
export class AutoApprovalHandler {
1313
private consecutiveAutoApprovedRequestsCount: number = 0
1414
private consecutiveAutoApprovedCost: number = 0
15+
private costResetMessageIndex: number = 0
1516

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

94-
// Calculate total cost from messages
95-
this.consecutiveAutoApprovedCost = getApiMetrics(messages).totalCost
95+
// Calculate total cost from messages after the last reset point
96+
const messagesAfterReset = messages.slice(this.costResetMessageIndex)
97+
this.consecutiveAutoApprovedCost = getApiMetrics(messagesAfterReset).totalCost
9698

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

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

128131
/**
129-
* Reset the request counter (typically called when starting a new task)
132+
* Reset the request counter and cost tracking (typically called when starting a new task)
130133
*/
131134
resetRequestCount(): void {
132135
this.consecutiveAutoApprovedRequestsCount = 0
136+
this.costResetMessageIndex = 0
133137
}
134138

135139
/**

src/core/task/__tests__/AutoApprovalHandler.spec.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,17 +183,67 @@ describe("AutoApprovalHandler", () => {
183183
expect(result3.requiresApproval).toBe(true)
184184
})
185185

186-
it("should not reset cost to zero on approval", async () => {
186+
it("should reset cost tracking on approval", async () => {
187+
const messages: ClineMessage[] = [
188+
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 1000 },
189+
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 2000 },
190+
]
191+
192+
// First check - cost exceeds limit (6.0 > 5.0)
193+
mockGetApiMetrics.mockReturnValue({ totalCost: 6.0 })
194+
mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" })
195+
196+
const result1 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
197+
expect(result1.shouldProceed).toBe(true)
198+
expect(result1.requiresApproval).toBe(true)
199+
200+
// Add more messages after reset
201+
messages.push(
202+
{ type: "say", say: "api_req_started", text: '{"cost": 2.0}', ts: 3000 },
203+
{ type: "say", say: "api_req_started", text: '{"cost": 1.0}', ts: 4000 },
204+
)
205+
206+
// Second check - should only count messages after reset (3.0 < 5.0)
207+
mockGetApiMetrics.mockReturnValue({ totalCost: 3.0 })
208+
const result2 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
209+
210+
// Should not require approval since cost after reset is under limit
211+
expect(result2.shouldProceed).toBe(true)
212+
expect(result2.requiresApproval).toBe(false)
213+
214+
// Verify it's only calculating cost from messages after reset point
215+
expect(mockGetApiMetrics).toHaveBeenLastCalledWith(messages.slice(2))
216+
})
217+
218+
it("should track multiple cost resets correctly", async () => {
187219
const messages: ClineMessage[] = []
188220

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

192226
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
193227

194-
// Cost should still be calculated from messages, not reset
195-
const state = handler.getApprovalState()
196-
expect(state.currentCost).toBe(6.0)
228+
// Add more messages
229+
messages.push(
230+
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 2000 },
231+
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 3000 },
232+
)
233+
234+
// Second cost limit hit (only counting from index 1)
235+
mockGetApiMetrics.mockReturnValue({ totalCost: 6.0 })
236+
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
237+
238+
// Add more messages after second reset
239+
messages.push({ type: "say", say: "api_req_started", text: '{"cost": 2.0}', ts: 4000 })
240+
241+
// Third check - should only count from last reset
242+
mockGetApiMetrics.mockReturnValue({ totalCost: 2.0 })
243+
const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
244+
245+
expect(result.requiresApproval).toBe(false)
246+
expect(mockGetApiMetrics).toHaveBeenLastCalledWith(messages.slice(3))
197247
})
198248
})
199249

0 commit comments

Comments
 (0)