@@ -39,6 +39,7 @@ import { resetMidRunUsageCaches } from '@/lib/billing/core/mid-run-usage'
3939import { NEAR_LIMIT_USAGE_GATE_TTL_MS } from '@/lib/billing/core/usage-gate-cache'
4040import { markFailureKind } from '@/lib/core/errors/failure-log'
4141import { isNonRetryableExecutionError } from '@/lib/execution/non-retryable-error'
42+ import { AgentTurnSession } from '@/lib/memory/agent-turn-session'
4243import { AgentTurnStateMachine } from '@/lib/memory/turn-state'
4344import type { ExecutionContext } from '@/executor/types'
4445import { readTrustedExecutionCost } from '@/executor/utils/errors'
@@ -55,7 +56,7 @@ import {
5556} from '@/providers/conversation-history'
5657import { executeProviderTool } from '@/providers/runtime-context'
5758import { providerFailureLogLevel } from '@/providers/streaming-tool-loop-shared'
58- import { ProviderError , type ProviderRequest } from '@/providers/types'
59+ import { ProviderError , type ProviderRequest , type ProviderToolConfig } from '@/providers/types'
5960
6061const { mockCheckExecutionUsageLimits } = billingUsageGateCacheMockFns
6162const { mockExecuteTool } = toolsMockFns
@@ -203,8 +204,54 @@ describe('run usage limit', () => {
203204 expect ( readTrustedExecutionCost ( failure ) ) . toEqual ( { input : 1 , output : 1 , total : 2 } )
204205 } )
205206
207+ it ( "counts a resumed turn's journaled spend toward the limit once, however often the run reopens it" , async ( ) => {
208+ mockCheckExecutionUsageLimits . mockResolvedValue ( {
209+ isExceeded : false ,
210+ payerUsage : { currentUsage : 1 , limit : 5 } ,
211+ } )
212+ const reopenTurn = ( ) =>
213+ new AgentTurnSession (
214+ 'turn-1' ,
215+ { save : vi . fn ( ) } ,
216+ {
217+ version : 1 ,
218+ steps : [
219+ {
220+ id : 'earlier-process-step' ,
221+ assistant : { role : 'assistant' , content : 'Partial answer' } ,
222+ calls : [ ] ,
223+ results : [ ] ,
224+ usage : { input : 1 , output : 1 } ,
225+ cost : { input : 1 , output : 1 , total : 2 } ,
226+ } ,
227+ ] ,
228+ }
229+ )
230+ const run = startRun ( )
231+ mockExecuteRequest . mockImplementation ( async ( request : ProviderRequest ) => {
232+ await prepareConversationGeneration ( request , 'anthropic' , { messages : request . messages } )
233+ throw new Error ( 'Model call sent' )
234+ } )
235+ const request = { model : 'model-a' , messages : [ { role : 'user' as const , content : 'Continue' } ] }
236+
237+ const attempt = ( ) =>
238+ executeProviderRequest ( 'anthropic' , request , {
239+ executionContext : run ,
240+ agentConversation : reopenTurn ( ) ,
241+ } )
242+
243+ await expect ( attempt ( ) ) . rejects . toThrow ( 'Model call sent' )
244+ await expect ( attempt ( ) ) . rejects . toThrow ( 'Model call sent' )
245+ expect ( run . unrecordedModelCost ?. total ) . toBe ( 2 )
246+ } )
247+
206248 it ( 'bills a retried pending tool call once, through the journal that records it' , async ( ) => {
207- const tool = { id : 'search' , name : 'search' , description : 'search' , params : { } }
249+ const tool : ProviderToolConfig = {
250+ id : 'search' ,
251+ description : 'search' ,
252+ params : { } ,
253+ parameters : { type : 'object' , properties : { } , required : [ ] } ,
254+ }
208255 const session = new AgentTurnStateMachine (
209256 { save : vi . fn ( ) } ,
210257 {
0 commit comments