Skip to content

Commit 38c8028

Browse files
roomote[bot]roomotedaniel-lxs
authored
fix: properly reset cost limit tracking when user clicks "Reset and Continue" (#6890)
Co-authored-by: Roo Code <[email protected]> Co-authored-by: daniel-lxs <[email protected]>
1 parent 1de480b commit 38c8028

File tree

2 files changed

+144
-47
lines changed

2 files changed

+144
-47
lines changed

src/core/task/AutoApprovalHandler.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface AutoApprovalResult {
1010
}
1111

1212
export class AutoApprovalHandler {
13+
private lastResetMessageIndex: number = 0
1314
private consecutiveAutoApprovedRequestsCount: number = 0
1415
private consecutiveAutoApprovedCost: number = 0
1516

@@ -25,7 +26,7 @@ export class AutoApprovalHandler {
2526
) => Promise<{ response: ClineAskResponse; text?: string; images?: string[] }>,
2627
): Promise<AutoApprovalResult> {
2728
// Check request count limit
28-
const requestResult = await this.checkRequestLimit(state, askForApproval)
29+
const requestResult = await this.checkRequestLimit(state, messages, askForApproval)
2930
if (!requestResult.shouldProceed || requestResult.requiresApproval) {
3031
return requestResult
3132
}
@@ -36,19 +37,23 @@ export class AutoApprovalHandler {
3637
}
3738

3839
/**
39-
* Increment the request counter and check if limit is exceeded
40+
* Calculate request count and check if limit is exceeded
4041
*/
4142
private async checkRequestLimit(
4243
state: GlobalState | undefined,
44+
messages: ClineMessage[],
4345
askForApproval: (
4446
type: ClineAsk,
4547
data: string,
4648
) => Promise<{ response: ClineAskResponse; text?: string; images?: string[] }>,
4749
): Promise<AutoApprovalResult> {
4850
const maxRequests = state?.allowedMaxRequests || Infinity
4951

50-
// Increment the counter for each new API request
51-
this.consecutiveAutoApprovedRequestsCount++
52+
// Calculate request count from messages after the last reset point
53+
const messagesAfterReset = messages.slice(this.lastResetMessageIndex)
54+
// Count API request messages (simplified - you may need to adjust based on your message structure)
55+
this.consecutiveAutoApprovedRequestsCount =
56+
messagesAfterReset.filter((msg) => msg.type === "say" && msg.say === "api_req_started").length + 1 // +1 for the current request being checked
5257

5358
if (this.consecutiveAutoApprovedRequestsCount > maxRequests) {
5459
const { response } = await askForApproval(
@@ -58,7 +63,8 @@ export class AutoApprovalHandler {
5863

5964
// If we get past the promise, it means the user approved and did not start a new task
6065
if (response === "yesButtonClicked") {
61-
this.consecutiveAutoApprovedRequestsCount = 0
66+
// Reset tracking by recording the current message count
67+
this.lastResetMessageIndex = messages.length
6268
return {
6369
shouldProceed: true,
6470
requiresApproval: true,
@@ -91,8 +97,9 @@ export class AutoApprovalHandler {
9197
): Promise<AutoApprovalResult> {
9298
const maxCost = state?.allowedMaxCost || Infinity
9399

94-
// Calculate total cost from messages
95-
this.consecutiveAutoApprovedCost = getApiMetrics(messages).totalCost
100+
// Calculate total cost from messages after the last reset point
101+
const messagesAfterReset = messages.slice(this.lastResetMessageIndex)
102+
this.consecutiveAutoApprovedCost = getApiMetrics(messagesAfterReset).totalCost
96103

97104
// Use epsilon for floating-point comparison to avoid precision issues
98105
const EPSILON = 0.0001
@@ -104,8 +111,9 @@ export class AutoApprovalHandler {
104111

105112
// If we get past the promise, it means the user approved and did not start a new task
106113
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.
114+
// Reset tracking by recording the current message count
115+
// Future calculations will only include messages after this point
116+
this.lastResetMessageIndex = messages.length
109117
return {
110118
shouldProceed: true,
111119
requiresApproval: true,
@@ -126,10 +134,12 @@ export class AutoApprovalHandler {
126134
}
127135

128136
/**
129-
* Reset the request counter (typically called when starting a new task)
137+
* Reset the tracking (typically called when starting a new task)
130138
*/
131139
resetRequestCount(): void {
140+
this.lastResetMessageIndex = 0
132141
this.consecutiveAutoApprovedRequestsCount = 0
142+
this.consecutiveAutoApprovedCost = 0
133143
}
134144

135145
/**

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

Lines changed: 124 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,15 @@ describe("AutoApprovalHandler", () => {
4040
mockState.allowedMaxCost = 10
4141
const messages: ClineMessage[] = []
4242

43-
// First call should be under limit
43+
// First call should be under limit (count = 1)
4444
const result1 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
4545
expect(result1.shouldProceed).toBe(true)
4646
expect(result1.requiresApproval).toBe(false)
4747

48-
// Second call should trigger request limit
48+
// Add a message to simulate first request completed
49+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 1000 })
50+
51+
// Second call should trigger request limit (1 message + current = 2 > 1)
4952
mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" })
5053
const result2 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
5154

@@ -64,27 +67,35 @@ describe("AutoApprovalHandler", () => {
6467
mockState.allowedMaxRequests = 3
6568
})
6669

67-
it("should increment request count on each check", async () => {
70+
it("should calculate request count from messages", async () => {
6871
const messages: ClineMessage[] = []
6972

70-
// Check state after each call
71-
for (let i = 1; i <= 3; i++) {
72-
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
73-
const state = handler.getApprovalState()
74-
expect(state.requestCount).toBe(i)
75-
}
73+
// First check - no messages yet, count should be 1 (for current request)
74+
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
75+
let state = handler.getApprovalState()
76+
expect(state.requestCount).toBe(1)
77+
78+
// Add API request messages
79+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 1000 })
80+
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
81+
state = handler.getApprovalState()
82+
expect(state.requestCount).toBe(2) // 1 message + current request
83+
84+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 2000 })
85+
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
86+
state = handler.getApprovalState()
87+
expect(state.requestCount).toBe(3) // 2 messages + current request
7688
})
7789

7890
it("should ask for approval when limit is exceeded", async () => {
7991
const messages: ClineMessage[] = []
8092

81-
// Make 3 requests (within limit)
93+
// Add 3 API request messages (to simulate 3 requests made)
8294
for (let i = 0; i < 3; i++) {
83-
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
95+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 1000 + i })
8496
}
85-
expect(mockAskForApproval).not.toHaveBeenCalled()
8697

87-
// 4th request should trigger approval
98+
// Next check should trigger approval (3 messages + current = 4 > 3)
8899
mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" })
89100
const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
90101

@@ -99,29 +110,35 @@ describe("AutoApprovalHandler", () => {
99110
it("should reset count when user approves", async () => {
100111
const messages: ClineMessage[] = []
101112

102-
// Exceed limit
113+
// Add messages to exceed limit
103114
for (let i = 0; i < 3; i++) {
104-
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
115+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 1000 + i })
105116
}
106117

107-
// 4th request should trigger approval and reset
118+
// Next request should trigger approval and reset
108119
mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" })
109120
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
110121

111-
// Count should be reset
122+
// Add more messages after reset
123+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 4000 })
124+
125+
// Next check should only count messages after reset
126+
const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
127+
expect(result.requiresApproval).toBe(false) // Should not require approval (1 message + current = 2 <= 3)
128+
112129
const state = handler.getApprovalState()
113-
expect(state.requestCount).toBe(0)
130+
expect(state.requestCount).toBe(2) // 1 message after reset + current request
114131
})
115132

116133
it("should not proceed when user rejects", async () => {
117134
const messages: ClineMessage[] = []
118135

119-
// Exceed limit
136+
// Add messages to exceed limit
120137
for (let i = 0; i < 3; i++) {
121-
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
138+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 1000 + i })
122139
}
123140

124-
// 4th request with rejection
141+
// Next request with rejection
125142
mockAskForApproval.mockResolvedValue({ response: "noButtonClicked" })
126143
const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
127144

@@ -183,17 +200,67 @@ describe("AutoApprovalHandler", () => {
183200
expect(result3.requiresApproval).toBe(true)
184201
})
185202

186-
it("should not reset cost to zero on approval", async () => {
203+
it("should reset cost tracking on approval", async () => {
204+
const messages: ClineMessage[] = [
205+
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 1000 },
206+
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 2000 },
207+
]
208+
209+
// First check - cost exceeds limit (6.0 > 5.0)
210+
mockGetApiMetrics.mockReturnValue({ totalCost: 6.0 })
211+
mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" })
212+
213+
const result1 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
214+
expect(result1.shouldProceed).toBe(true)
215+
expect(result1.requiresApproval).toBe(true)
216+
217+
// Add more messages after reset
218+
messages.push(
219+
{ type: "say", say: "api_req_started", text: '{"cost": 2.0}', ts: 3000 },
220+
{ type: "say", say: "api_req_started", text: '{"cost": 1.0}', ts: 4000 },
221+
)
222+
223+
// Second check - should only count messages after reset (3.0 < 5.0)
224+
mockGetApiMetrics.mockReturnValue({ totalCost: 3.0 })
225+
const result2 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
226+
227+
// Should not require approval since cost after reset is under limit
228+
expect(result2.shouldProceed).toBe(true)
229+
expect(result2.requiresApproval).toBe(false)
230+
231+
// Verify it's only calculating cost from messages after reset point
232+
expect(mockGetApiMetrics).toHaveBeenLastCalledWith(messages.slice(2))
233+
})
234+
235+
it("should track multiple cost resets correctly", async () => {
187236
const messages: ClineMessage[] = []
188237

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

192243
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
193244

194-
// Cost should still be calculated from messages, not reset
195-
const state = handler.getApprovalState()
196-
expect(state.currentCost).toBe(6.0)
245+
// Add more messages
246+
messages.push(
247+
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 2000 },
248+
{ type: "say", say: "api_req_started", text: '{"cost": 3.0}', ts: 3000 },
249+
)
250+
251+
// Second cost limit hit (only counting from index 1)
252+
mockGetApiMetrics.mockReturnValue({ totalCost: 6.0 })
253+
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
254+
255+
// Add more messages after second reset
256+
messages.push({ type: "say", say: "api_req_started", text: '{"cost": 2.0}', ts: 4000 })
257+
258+
// Third check - should only count from last reset
259+
mockGetApiMetrics.mockReturnValue({ totalCost: 2.0 })
260+
const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
261+
262+
expect(result.requiresApproval).toBe(false)
263+
expect(mockGetApiMetrics).toHaveBeenLastCalledWith(messages.slice(3))
197264
})
198265
})
199266

@@ -205,16 +272,21 @@ describe("AutoApprovalHandler", () => {
205272

206273
mockGetApiMetrics.mockReturnValue({ totalCost: 3.0 })
207274

208-
// First two requests should pass
209-
for (let i = 0; i < 2; i++) {
210-
const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
211-
expect(result.shouldProceed).toBe(true)
212-
expect(result.requiresApproval).toBe(false)
213-
}
275+
// First request should pass (count = 1)
276+
let result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
277+
expect(result.shouldProceed).toBe(true)
278+
expect(result.requiresApproval).toBe(false)
214279

215-
// Third request should trigger request limit (not cost limit)
280+
// Add a message and check again (count = 2)
281+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 1000 })
282+
result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
283+
expect(result.shouldProceed).toBe(true)
284+
expect(result.requiresApproval).toBe(false)
285+
286+
// Add another message - third request should trigger request limit (count = 3 > 2)
287+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 2000 })
216288
mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" })
217-
const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
289+
result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
218290

219291
expect(mockAskForApproval).toHaveBeenCalledWith(
220292
"auto_approval_max_req_reached",
@@ -227,23 +299,38 @@ describe("AutoApprovalHandler", () => {
227299
})
228300

229301
describe("resetRequestCount", () => {
230-
it("should reset the request counter", async () => {
302+
it("should reset tracking", async () => {
231303
mockState.allowedMaxRequests = 5
304+
mockState.allowedMaxCost = 10.0
232305
const messages: ClineMessage[] = []
233306

234-
// Make some requests
307+
// Add some messages
235308
for (let i = 0; i < 3; i++) {
236-
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
309+
messages.push({ type: "say", say: "api_req_started", text: "{}", ts: 1000 + i })
237310
}
238311

312+
mockGetApiMetrics.mockReturnValue({ totalCost: 5.0 })
313+
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
314+
239315
let state = handler.getApprovalState()
240-
expect(state.requestCount).toBe(3)
316+
expect(state.requestCount).toBe(4) // 3 messages + current
317+
expect(state.currentCost).toBe(5.0)
241318

242319
// Reset
243320
handler.resetRequestCount()
244321

322+
// After reset, counts should be zero
245323
state = handler.getApprovalState()
246324
expect(state.requestCount).toBe(0)
325+
expect(state.currentCost).toBe(0)
326+
327+
// Next check should start fresh
328+
mockGetApiMetrics.mockReturnValue({ totalCost: 8.0 })
329+
await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval)
330+
331+
state = handler.getApprovalState()
332+
expect(state.requestCount).toBe(4) // All messages counted again
333+
expect(state.currentCost).toBe(8.0)
247334
})
248335
})
249336
})

0 commit comments

Comments
 (0)