@@ -42,6 +42,7 @@ const transcriptMessageCountCache = new Map<
4242> ( ) ;
4343const MAX_TRANSCRIPT_MESSAGE_COUNT_CACHE_ENTRIES = 5000 ;
4444const TRANSCRIPT_ASYNC_READ_CHUNK_BYTES = 64 * 1024 ;
45+ type TranscriptFileHandle = Awaited < ReturnType < typeof fs . promises . open > > ;
4546
4647function readSessionTitleFieldsCacheKey (
4748 filePath : string ,
@@ -813,43 +814,47 @@ export async function readSessionTitleFieldsFromTranscriptAsync(
813814 if ( cached ) {
814815 return cached ;
815816 }
816- const index = await readSessionTranscriptIndex ( filePath ) ;
817- if ( ! index ) {
818- return { firstUserMessage : null , lastMessagePreview : null } ;
817+
818+ if ( stat . size === 0 ) {
819+ const empty = { firstUserMessage : null , lastMessagePreview : null } ;
820+ setCachedSessionTitleFields ( cacheKey , stat , empty ) ;
821+ return empty ;
819822 }
820823
821- let firstUserMessage : string | null = null ;
822- for ( const entry of index . entries ) {
823- const msg = entry . record . message as TranscriptMessage | undefined ;
824- if ( msg ?. role !== "user" ) {
825- continue ;
826- }
827- if ( opts ?. includeInterSession !== true && hasInterSessionUserProvenance ( msg ) ) {
828- continue ;
829- }
830- const text = extractTextFromContent ( msg . content ) ;
831- if ( text ) {
832- firstUserMessage = text ;
833- break ;
824+ let handle : TranscriptFileHandle | null = null ;
825+ try {
826+ handle = await fs . promises . open ( filePath , "r" ) ;
827+
828+ let firstUserMessage : string | null = null ;
829+ try {
830+ const chunk = await readTranscriptHeadChunkAsync ( handle ) ;
831+ if ( chunk ) {
832+ firstUserMessage = extractFirstUserMessageFromTranscriptChunk ( chunk , opts ) ;
833+ }
834+ } catch {
835+ // ignore head read errors
834836 }
835- }
836837
837- let lastMessagePreview : string | null = null ;
838- for ( const entry of index . entries . toReversed ( ) ) {
839- const msg = entry . record . message as TranscriptMessage | undefined ;
840- if ( ! msg || ( msg . role !== "user" && msg . role !== "assistant" ) ) {
841- continue ;
838+ let lastMessagePreview : string | null = null ;
839+ try {
840+ lastMessagePreview = await readLastMessagePreviewFromOpenTranscriptAsync ( {
841+ handle,
842+ size : stat . size ,
843+ } ) ;
844+ } catch {
845+ // ignore tail read errors
842846 }
843- const text = extractTextFromContent ( msg . content ) ;
844- if ( text ) {
845- lastMessagePreview = text ;
846- break ;
847+
848+ const result = { firstUserMessage, lastMessagePreview } ;
849+ setCachedSessionTitleFields ( cacheKey , stat , result ) ;
850+ return result ;
851+ } catch {
852+ return { firstUserMessage : null , lastMessagePreview : null } ;
853+ } finally {
854+ if ( handle ) {
855+ await handle . close ( ) . catch ( ( ) => undefined ) ;
847856 }
848857 }
849-
850- const result = { firstUserMessage, lastMessagePreview } ;
851- setCachedSessionTitleFields ( cacheKey , stat , result ) ;
852- return result ;
853858}
854859
855860function extractTextFromContent ( content : TranscriptMessage [ "content" ] ) : string | null {
@@ -883,6 +888,18 @@ function readTranscriptHeadChunk(fd: number, maxBytes = 8192): string | null {
883888 return buf . toString ( "utf-8" , 0 , bytesRead ) ;
884889}
885890
891+ async function readTranscriptHeadChunkAsync (
892+ handle : TranscriptFileHandle ,
893+ maxBytes = 8192 ,
894+ ) : Promise < string | null > {
895+ const buffer = Buffer . alloc ( maxBytes ) ;
896+ const { bytesRead } = await handle . read ( buffer , 0 , buffer . length , 0 ) ;
897+ if ( bytesRead <= 0 ) {
898+ return null ;
899+ }
900+ return buffer . toString ( "utf-8" , 0 , bytesRead ) ;
901+ }
902+
886903function extractFirstUserMessageFromTranscriptChunk (
887904 chunk : string ,
888905 opts ?: { includeInterSession ?: boolean } ,
@@ -993,6 +1010,41 @@ function readLastMessagePreviewFromOpenTranscript(params: {
9931010 return null ;
9941011}
9951012
1013+ async function readLastMessagePreviewFromOpenTranscriptAsync ( params : {
1014+ handle : TranscriptFileHandle ;
1015+ size : number ;
1016+ } ) : Promise < string | null > {
1017+ const readStart = Math . max ( 0 , params . size - LAST_MSG_MAX_BYTES ) ;
1018+ const readLen = Math . min ( params . size , LAST_MSG_MAX_BYTES ) ;
1019+ const buffer = Buffer . alloc ( readLen ) ;
1020+ const { bytesRead } = await params . handle . read ( buffer , 0 , readLen , readStart ) ;
1021+ if ( bytesRead <= 0 ) {
1022+ return null ;
1023+ }
1024+
1025+ const chunk = buffer . toString ( "utf-8" , 0 , bytesRead ) ;
1026+ const lines = chunk . split ( / \r ? \n / ) . filter ( ( line ) => line . trim ( ) ) ;
1027+ const tailLines = lines . slice ( - LAST_MSG_MAX_LINES ) ;
1028+
1029+ for ( let i = tailLines . length - 1 ; i >= 0 ; i -- ) {
1030+ const line = tailLines [ i ] ;
1031+ try {
1032+ const parsed = JSON . parse ( line ) ;
1033+ const msg = parsed ?. message as TranscriptMessage | undefined ;
1034+ if ( msg ?. role !== "user" && msg ?. role !== "assistant" ) {
1035+ continue ;
1036+ }
1037+ const text = extractTextFromContent ( msg . content ) ;
1038+ if ( text ) {
1039+ return text ;
1040+ }
1041+ } catch {
1042+ // skip malformed
1043+ }
1044+ }
1045+ return null ;
1046+ }
1047+
9961048export function readLastMessagePreviewFromTranscript (
9971049 sessionId : string ,
9981050 storePath : string | undefined ,
0 commit comments