@@ -65,7 +65,6 @@ import type { GatewayRequestHandlers, RespondFn } from "./types.js";
6565const COST_USAGE_CACHE_TTL_MS = 30_000 ;
6666const COST_USAGE_CACHE_MAX = 256 ;
6767const SESSIONS_USAGE_AGENT_LOAD_CONCURRENCY = 12 ;
68- const DAY_MS = 24 * 60 * 60 * 1000 ;
6968
7069type DateRange = { startMs : number ; endMs : number } ;
7170// Keep validation and parsed timestamps in one result so handlers cannot forward
@@ -74,6 +73,7 @@ type DateRangeResolution = { ok: true; value: DateRange } | { ok: false; error:
7473type DateInterpretation =
7574 | { mode : "utc" | "gateway" }
7675 | { mode : "specific" ; utcOffsetMinutes : number } ;
76+ type DateParts = { year : number ; monthIndex : number ; day : number } ;
7777
7878type CostUsageCacheEntry = {
7979 summary ?: CostUsageSummary ;
@@ -139,9 +139,7 @@ function resolveSessionUsageFileOrRespond(
139139 return { config, entry, agentId, sessionId, sessionFile } ;
140140}
141141
142- const parseDateParts = (
143- raw : unknown ,
144- ) : { year : number ; monthIndex : number ; day : number } | undefined => {
142+ const parseDateParts = ( raw : unknown ) : DateParts | undefined => {
145143 if ( typeof raw !== "string" || ! raw . trim ( ) ) {
146144 return undefined ;
147145 }
@@ -170,6 +168,29 @@ const parseDateParts = (
170168 return { year, monthIndex, day } ;
171169} ;
172170
171+ const shiftDateParts = ( parts : DateParts , days : number ) : DateParts => {
172+ const shifted = new Date ( Date . UTC ( parts . year , parts . monthIndex , parts . day + days ) ) ;
173+ return {
174+ year : shifted . getUTCFullYear ( ) ,
175+ monthIndex : shifted . getUTCMonth ( ) ,
176+ day : shifted . getUTCDate ( ) ,
177+ } ;
178+ } ;
179+
180+ const datePartsToStartMs = ( parts : DateParts , interpretation : DateInterpretation ) : number => {
181+ const { year, monthIndex, day } = parts ;
182+ if ( interpretation . mode === "gateway" ) {
183+ return new Date ( year , monthIndex , day ) . getTime ( ) ;
184+ }
185+ if ( interpretation . mode === "specific" ) {
186+ return Date . UTC ( year , monthIndex , day ) - interpretation . utcOffsetMinutes * 60 * 1000 ;
187+ }
188+ return Date . UTC ( year , monthIndex , day ) ;
189+ } ;
190+
191+ const datePartsToEndMs = ( parts : DateParts , interpretation : DateInterpretation ) : number =>
192+ datePartsToStartMs ( shiftDateParts ( parts , 1 ) , interpretation ) - 1 ;
193+
173194// usage.cost / sessions.usage accept optional startDate/endDate. parseDateParts returns
174195// undefined for both absent and invalid input, so an explicitly supplied but unparseable
175196// date (bad format or impossible calendar date like 2026-02-30) would otherwise silently
@@ -243,6 +264,25 @@ const resolveDayBucketUtcOffsetMinutes = (interpretation: DateInterpretation) =>
243264 ? interpretation . utcOffsetMinutes
244265 : 0 ;
245266
267+ const getDateParts = ( date : Date , interpretation : DateInterpretation ) : DateParts => {
268+ if ( interpretation . mode === "gateway" ) {
269+ return { year : date . getFullYear ( ) , monthIndex : date . getMonth ( ) , day : date . getDate ( ) } ;
270+ }
271+ if ( interpretation . mode === "specific" ) {
272+ const shifted = new Date ( date . getTime ( ) + interpretation . utcOffsetMinutes * 60 * 1000 ) ;
273+ return {
274+ year : shifted . getUTCFullYear ( ) ,
275+ monthIndex : shifted . getUTCMonth ( ) ,
276+ day : shifted . getUTCDate ( ) ,
277+ } ;
278+ }
279+ return {
280+ year : date . getUTCFullYear ( ) ,
281+ monthIndex : date . getUTCMonth ( ) ,
282+ day : date . getUTCDate ( ) ,
283+ } ;
284+ } ;
285+
246286/**
247287 * Parse a date string (YYYY-MM-DD) to start-of-day timestamp based on interpretation mode.
248288 * Returns undefined if invalid.
@@ -255,33 +295,17 @@ const parseDateToMs = (
255295 if ( ! parts ) {
256296 return undefined ;
257297 }
258- const { year, monthIndex, day } = parts ;
259- if ( interpretation . mode === "gateway" ) {
260- const ms = new Date ( year , monthIndex , day ) . getTime ( ) ;
261- return Number . isNaN ( ms ) ? undefined : ms ;
262- }
263- if ( interpretation . mode === "specific" ) {
264- const ms = Date . UTC ( year , monthIndex , day ) - interpretation . utcOffsetMinutes * 60 * 1000 ;
265- return Number . isNaN ( ms ) ? undefined : ms ;
266- }
267- const ms = Date . UTC ( year , monthIndex , day ) ;
268- return Number . isNaN ( ms ) ? undefined : ms ;
298+ return datePartsToStartMs ( parts , interpretation ) ;
269299} ;
270300
271- const getTodayStartMs = ( now : Date , interpretation : DateInterpretation ) : number => {
272- if ( interpretation . mode === "gateway" ) {
273- return new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) ) . getTime ( ) ;
274- }
275- if ( interpretation . mode === "specific" ) {
276- const shifted = new Date ( now . getTime ( ) + interpretation . utcOffsetMinutes * 60 * 1000 ) ;
277- return (
278- Date . UTC ( shifted . getUTCFullYear ( ) , shifted . getUTCMonth ( ) , shifted . getUTCDate ( ) ) -
279- interpretation . utcOffsetMinutes * 60 * 1000
280- ) ;
281- }
282- return Date . UTC ( now . getUTCFullYear ( ) , now . getUTCMonth ( ) , now . getUTCDate ( ) ) ;
301+ const formatDateLabel = ( ms : number , interpretation : DateInterpretation ) : string => {
302+ const parts = getDateParts ( new Date ( ms ) , interpretation ) ;
303+ return formatDateParts ( parts . year , parts . monthIndex , parts . day ) ;
283304} ;
284305
306+ const formatDateParts = ( year : number , monthIndex : number , day : number ) : string =>
307+ `${ year } -${ String ( monthIndex + 1 ) . padStart ( 2 , "0" ) } -${ String ( day ) . padStart ( 2 , "0" ) } ` ;
308+
285309const parseDays = ( raw : unknown ) : number | undefined => {
286310 if ( typeof raw === "number" && Number . isFinite ( raw ) ) {
287311 return Math . floor ( raw ) ;
@@ -314,6 +338,15 @@ const resolveRangeDays = (raw: unknown): number | "all" | undefined => {
314338 return undefined ;
315339} ;
316340
341+ const resolveTrailingDays = (
342+ endDateParts : DateParts ,
343+ days : number ,
344+ interpretation : DateInterpretation ,
345+ ) : DateRange => ( {
346+ startMs : datePartsToStartMs ( shiftDateParts ( endDateParts , - ( days - 1 ) ) , interpretation ) ,
347+ endMs : datePartsToEndMs ( endDateParts , interpretation ) ,
348+ } ) ;
349+
317350/**
318351 * Get date range from params (startDate/endDate or days).
319352 * Falls back to last 30 days if not provided.
@@ -336,39 +369,37 @@ const resolveDateRange = (params: {
336369
337370 const now = new Date ( ) ;
338371 const interpretation = resolveDateInterpretation ( params ) ;
339- const todayStartMs = getTodayStartMs ( now , interpretation ) ;
340- const todayEndMs = todayStartMs + DAY_MS - 1 ;
372+ const todayDateParts = getDateParts ( now , interpretation ) ;
373+ const todayEndMs = datePartsToEndMs ( todayDateParts , interpretation ) ;
341374
342- const startMs = parseDateToMs ( params . startDate , interpretation ) ;
343- const endMs = parseDateToMs ( params . endDate , interpretation ) ;
375+ const startDateParts = parseDateParts ( params . startDate ) ;
376+ const endDateParts = parseDateParts ( params . endDate ) ;
344377
345- if ( startMs !== undefined && endMs !== undefined ) {
346- if ( startMs > endMs ) {
378+ if ( startDateParts && endDateParts ) {
379+ const startMs = datePartsToStartMs ( startDateParts , interpretation ) ;
380+ const endStartMs = datePartsToStartMs ( endDateParts , interpretation ) ;
381+ if ( startMs > endStartMs ) {
347382 return { ok : false , error : "startDate must not be after endDate" } ;
348383 }
349- // endMs should be end of day
350- return { ok : true , value : { startMs, endMs : endMs + DAY_MS - 1 } } ;
384+ return { ok : true , value : { startMs, endMs : datePartsToEndMs ( endDateParts , interpretation ) } } ;
351385 }
352386
353387 const rangeDays = resolveRangeDays ( params . range ) ;
354388 if ( rangeDays === "all" ) {
355389 return { ok : true , value : { startMs : 0 , endMs : todayEndMs } } ;
356390 }
357391 if ( rangeDays !== undefined ) {
358- const start = todayStartMs - ( rangeDays - 1 ) * DAY_MS ;
359- return { ok : true , value : { startMs : start , endMs : todayEndMs } } ;
392+ return { ok : true , value : resolveTrailingDays ( todayDateParts , rangeDays , interpretation ) } ;
360393 }
361394
362395 const days = parseDays ( params . days ) ;
363396 if ( days !== undefined ) {
364397 const clampedDays = Math . max ( 1 , days ) ;
365- const start = todayStartMs - ( clampedDays - 1 ) * DAY_MS ;
366- return { ok : true , value : { startMs : start , endMs : todayEndMs } } ;
398+ return { ok : true , value : resolveTrailingDays ( todayDateParts , clampedDays , interpretation ) } ;
367399 }
368400
369401 // Default to last 30 days
370- const defaultStartMs = todayStartMs - 29 * DAY_MS ;
371- return { ok : true , value : { startMs : defaultStartMs , endMs : todayEndMs } } ;
402+ return { ok : true , value : resolveTrailingDays ( todayDateParts , 30 , interpretation ) } ;
372403} ;
373404
374405type DiscoveredSessionWithAgent = DiscoveredSession & { agentId : string } ;
@@ -904,7 +935,6 @@ export const testApi = {
904935 parseUtcOffsetToMinutes,
905936 resolveDateInterpretation,
906937 parseDateToMs,
907- getTodayStartMs,
908938 parseDays,
909939 resolveDateRange,
910940 discoverAllSessionsForUsage,
@@ -978,9 +1008,8 @@ export const usageHandlers: GatewayRequestHandlers = {
9781008 }
9791009 const config = context . getRuntimeConfig ( ) ;
9801010 const { startMs, endMs } = dateRange . value ;
981- const dailyUtcOffsetMinutes = resolveDayBucketUtcOffsetMinutes (
982- resolveDateInterpretation ( { mode : p . mode , utcOffset : p . utcOffset } ) ,
983- ) ;
1011+ const dateInterpretation = resolveDateInterpretation ( { mode : p . mode , utcOffset : p . utcOffset } ) ;
1012+ const dailyUtcOffsetMinutes = resolveDayBucketUtcOffsetMinutes ( dateInterpretation ) ;
9841013 const limit = typeof p . limit === "number" && Number . isFinite ( p . limit ) ? p . limit : 50 ;
9851014 const includeContextWeight = p . includeContextWeight ?? false ;
9861015 const specificKey = normalizeOptionalString ( p . key ) ?? null ;
@@ -1426,12 +1455,6 @@ export const usageHandlers: GatewayRequestHandlers = {
14261455 }
14271456 }
14281457
1429- // Format dates back to YYYY-MM-DD strings
1430- const formatDateStr = ( ms : number ) => {
1431- const d = new Date ( ms ) ;
1432- return `${ d . getUTCFullYear ( ) } -${ String ( d . getUTCMonth ( ) + 1 ) . padStart ( 2 , "0" ) } -${ String ( d . getUTCDate ( ) ) . padStart ( 2 , "0" ) } ` ;
1433- } ;
1434-
14351458 const tail = buildUsageAggregateTail ( {
14361459 byChannelMap,
14371460 latencyTotals,
@@ -1471,8 +1494,8 @@ export const usageHandlers: GatewayRequestHandlers = {
14711494
14721495 const result : SessionsUsageResult = {
14731496 updatedAt : now ,
1474- startDate : formatDateStr ( startMs ) ,
1475- endDate : formatDateStr ( endMs ) ,
1497+ startDate : formatDateLabel ( startMs , dateInterpretation ) ,
1498+ endDate : formatDateLabel ( endMs , dateInterpretation ) ,
14761499 sessions,
14771500 totals : aggregateTotals ,
14781501 aggregates,
0 commit comments