3535 * @module services/session-log-reader
3636 */
3737
38- import { readFile , readdir , stat } from "node:fs/promises" ;
38+ import { readdir , readFile , stat } from "node:fs/promises" ;
3939import { homedir } from "node:os" ;
4040import { join } from "node:path" ;
4141
@@ -229,7 +229,10 @@ export function buildSessionLogCandidates(
229229) : CandidateSessionLogPath [ ] {
230230 const encoded = encodeClaudeCodeProjectDir ( workdir ) ;
231231 return [
232- { dir : join ( home , ".claude" , "projects" , encoded ) , label : "claude-projects" } ,
232+ {
233+ dir : join ( home , ".claude" , "projects" , encoded ) ,
234+ label : "claude-projects" ,
235+ } ,
233236 {
234237 dir : join ( workdir , ".claude" , "session-logs" ) ,
235238 label : "workspace-local" ,
@@ -244,9 +247,7 @@ interface SessionLogReaderLogger {
244247
245248const NOOP_LOGGER : SessionLogReaderLogger = { } ;
246249
247- async function listSessionLogFilesIn (
248- dir : string ,
249- ) : Promise < string [ ] > {
250+ async function listSessionLogFilesIn ( dir : string ) : Promise < string [ ] > {
250251 try {
251252 const entries = await readdir ( dir , { withFileTypes : true } ) ;
252253 const files : string [ ] = [ ] ;
@@ -362,6 +363,30 @@ function asNullableString(value: unknown): string | null {
362363 return typeof value === "string" ? value : null ;
363364}
364365
366+ function isClaudeCodeAssistantEvent (
367+ event : ClaudeCodeSessionEvent ,
368+ ) : event is ClaudeCodeAssistantEvent {
369+ if ( event . type !== "assistant" ) return false ;
370+ const message = ( event as { message ?: unknown } ) . message ;
371+ return (
372+ isRecord ( message ) &&
373+ message . role === "assistant" &&
374+ Array . isArray ( message . content )
375+ ) ;
376+ }
377+
378+ function isClaudeCodeUserEvent (
379+ event : ClaudeCodeSessionEvent ,
380+ ) : event is ClaudeCodeUserEvent {
381+ if ( event . type !== "user" ) return false ;
382+ const message = ( event as { message ?: unknown } ) . message ;
383+ return (
384+ isRecord ( message ) &&
385+ message . role === "user" &&
386+ ( typeof message . content === "string" || Array . isArray ( message . content ) )
387+ ) ;
388+ }
389+
365390/**
366391 * Parse one JSONL line into a typed event. Returns `null` when the line is
367392 * blank, malformed, or lacks the required `type` field. We don't throw —
@@ -483,7 +508,11 @@ function flattenToolResultContent(
483508 return parts . join ( "\n" ) ;
484509}
485510
486- function makeChildStepId ( parentStepId : string , uuid : string , idx : number ) : string {
511+ function makeChildStepId (
512+ parentStepId : string ,
513+ uuid : string ,
514+ idx : number ,
515+ ) : string {
487516 // Compose a stable, parent-scoped child id. Keeping the parent step prefix
488517 // makes it easy to grep + group child rows back to the parent in BI tools.
489518 const suffix = uuid && uuid . length > 0 ? uuid . slice ( 0 , 12 ) : `n${ idx } ` ;
@@ -505,7 +534,7 @@ export function normalizeSessionEvents(
505534 let idx = 0 ;
506535
507536 for ( const event of events ) {
508- if ( event . type === "assistant" ) {
537+ if ( isClaudeCodeAssistantEvent ( event ) ) {
509538 const ts = parseTimestamp ( event . timestamp ) ;
510539 const model = event . message . model ;
511540 const usage = event . message . usage ;
@@ -561,7 +590,7 @@ export function normalizeSessionEvents(
561590 continue ;
562591 }
563592
564- if ( event . type === "user" ) {
593+ if ( isClaudeCodeUserEvent ( event ) ) {
565594 const ts = parseTimestamp ( event . timestamp ) ;
566595 const content = event . message . content ;
567596 if ( typeof content === "string" ) continue ;
@@ -587,25 +616,21 @@ export function normalizeSessionEvents(
587616 return out ;
588617}
589618
590- function aggregateUsage (
591- events : ClaudeCodeSessionEvent [ ] ,
592- ) : ClaudeCodeUsage {
619+ function aggregateUsage ( events : ClaudeCodeSessionEvent [ ] ) : ClaudeCodeUsage {
593620 const total : ClaudeCodeUsage = { } ;
594621 for ( const event of events ) {
595- if ( event . type !== "assistant" ) continue ;
622+ if ( ! isClaudeCodeAssistantEvent ( event ) ) continue ;
596623 const usage = event . message . usage ;
597624 if ( ! usage ) continue ;
598625 if ( typeof usage . input_tokens === "number" ) {
599626 total . input_tokens = ( total . input_tokens ?? 0 ) + usage . input_tokens ;
600627 }
601628 if ( typeof usage . output_tokens === "number" ) {
602- total . output_tokens =
603- ( total . output_tokens ?? 0 ) + usage . output_tokens ;
629+ total . output_tokens = ( total . output_tokens ?? 0 ) + usage . output_tokens ;
604630 }
605631 if ( typeof usage . cache_read_input_tokens === "number" ) {
606632 total . cache_read_input_tokens =
607- ( total . cache_read_input_tokens ?? 0 ) +
608- usage . cache_read_input_tokens ;
633+ ( total . cache_read_input_tokens ?? 0 ) + usage . cache_read_input_tokens ;
609634 }
610635 if ( typeof usage . cache_creation_input_tokens === "number" ) {
611636 total . cache_creation_input_tokens =
0 commit comments