@@ -86,15 +86,22 @@ export async function recordUsage(params: RecordUsageParams): Promise<void> {
8686
8787 // Filter to entries with positive cost and derive total
8888 const validEntries = entries . filter ( ( e ) => e . cost > 0 )
89- if ( validEntries . length === 0 ) {
89+ const totalCost = validEntries . reduce ( ( sum , e ) => sum + e . cost , 0 )
90+
91+ // Nothing to write: no cost entries and no counter increments
92+ if ( validEntries . length === 0 && ! additionalStats ) {
9093 return
9194 }
9295
93- const totalCost = validEntries . reduce ( ( sum , e ) => sum + e . cost , 0 )
96+ // Keys managed by recordUsage — callers must not override these via additionalStats
97+ const RESERVED_KEYS = new Set ( [ 'totalCost' , 'currentPeriodCost' , 'lastActive' ] )
98+ const safeStats = additionalStats
99+ ? Object . fromEntries ( Object . entries ( additionalStats ) . filter ( ( [ k ] ) => ! RESERVED_KEYS . has ( k ) ) )
100+ : undefined
94101
95- try {
96- await db . transaction ( async ( tx ) => {
97- // Step 1: Insert all usage_log entries
102+ await db . transaction ( async ( tx ) => {
103+ // Step 1: Insert usage_log entries (only if there are positive-cost entries)
104+ if ( validEntries . length > 0 ) {
98105 await tx . insert ( usageLog ) . values (
99106 validEntries . map ( ( entry ) => ( {
100107 id : crypto . randomUUID ( ) ,
@@ -109,48 +116,39 @@ export async function recordUsage(params: RecordUsageParams): Promise<void> {
109116 executionId : executionId ?? null ,
110117 } ) )
111118 )
119+ }
112120
113- // Step 2: Update userStats — core billing fields derived from entries
114- const updateFields : Record < string , SQL | Date > = {
121+ // Step 2: Update userStats — core billing fields + source-specific counters
122+ const updateFields : Record < string , SQL | Date > = {
123+ lastActive : new Date ( ) ,
124+ ...( totalCost > 0 && {
115125 totalCost : sql `total_cost + ${ totalCost } ` ,
116126 currentPeriodCost : sql `current_period_cost + ${ totalCost } ` ,
117- lastActive : new Date ( ) ,
118- // Merge any source-specific counter increments from the caller
119- ...additionalStats ,
120- }
121-
122- const result = await tx
123- . update ( userStats )
124- . set ( updateFields )
125- . where ( eq ( userStats . userId , userId ) )
126- . returning ( { userId : userStats . userId } )
127+ } ) ,
128+ ...safeStats ,
129+ }
127130
128- if ( result . length === 0 ) {
129- logger . warn ( 'recordUsage: userStats row not found, usage_log entries will roll back' , {
130- userId,
131- totalCost,
132- } )
133- throw new Error ( `userStats row not found for userId: ${ userId } ` )
134- }
135- } )
131+ const result = await tx
132+ . update ( userStats )
133+ . set ( updateFields )
134+ . where ( eq ( userStats . userId , userId ) )
135+ . returning ( { userId : userStats . userId } )
136136
137- logger . debug ( 'Recorded usage' , {
138- userId,
139- totalCost,
140- entryCount : validEntries . length ,
141- sources : [ ...new Set ( validEntries . map ( ( e ) => e . source ) ) ] ,
142- } )
143- } catch ( error ) {
144- logger . error ( 'Failed to record usage' , {
145- error : error instanceof Error ? error . message : String ( error ) ,
146- userId,
147- totalCost,
148- entryCount : validEntries . length ,
149- } )
150- // Don't throw — the caller (execution logger, wand, copilot) decides whether to retry.
151- // Critically, the transaction ensures we never have a state where currentPeriodCost
152- // is incremented without corresponding usage_log entries.
153- }
137+ if ( result . length === 0 ) {
138+ logger . warn ( 'recordUsage: userStats row not found, transaction will roll back' , {
139+ userId,
140+ totalCost,
141+ } )
142+ throw new Error ( `userStats row not found for userId: ${ userId } ` )
143+ }
144+ } )
145+
146+ logger . debug ( 'Recorded usage' , {
147+ userId,
148+ totalCost,
149+ entryCount : validEntries . length ,
150+ sources : [ ...new Set ( validEntries . map ( ( e ) => e . source ) ) ] ,
151+ } )
154152}
155153
156154/**
0 commit comments