@@ -61,17 +61,36 @@ export function resolveSessionCompactionCheckpointReason(params: {
6161}
6262
6363const SESSION_HEADER_READ_MAX_BYTES = 64 * 1024 ;
64+ const SESSION_TAIL_READ_INITIAL_BYTES = 64 * 1024 ;
65+
66+ type AsyncTranscriptFileHandle = Awaited < ReturnType < typeof fs . open > > ;
67+
68+ async function readFileRangeAsync (
69+ fileHandle : AsyncTranscriptFileHandle ,
70+ position : number ,
71+ length : number ,
72+ ) : Promise < Buffer > {
73+ const buffer = Buffer . alloc ( length ) ;
74+ let offset = 0 ;
75+ while ( offset < length ) {
76+ const { bytesRead } = await fileHandle . read ( buffer , offset , length - offset , position + offset ) ;
77+ if ( bytesRead <= 0 ) {
78+ break ;
79+ }
80+ offset += bytesRead ;
81+ }
82+ return offset === length ? buffer : buffer . subarray ( 0 , offset ) ;
83+ }
6484
6585async function readSessionIdFromTranscriptHeaderAsync ( sessionFile : string ) : Promise < string | null > {
66- let fileHandle : Awaited < ReturnType < typeof fs . open > > | undefined ;
86+ let fileHandle : AsyncTranscriptFileHandle | undefined ;
6787 try {
6888 fileHandle = await fs . open ( sessionFile , "r" ) ;
69- const buffer = Buffer . alloc ( SESSION_HEADER_READ_MAX_BYTES ) ;
70- const { bytesRead } = await fileHandle . read ( buffer , 0 , buffer . length , 0 ) ;
71- if ( bytesRead <= 0 ) {
89+ const buffer = await readFileRangeAsync ( fileHandle , 0 , SESSION_HEADER_READ_MAX_BYTES ) ;
90+ if ( buffer . length <= 0 ) {
7291 return null ;
7392 }
74- const chunk = buffer . toString ( "utf-8" , 0 , bytesRead ) ;
93+ const chunk = buffer . toString ( "utf-8" ) ;
7594 const firstLine = chunk
7695 . split ( / \r ? \n / )
7796 . map ( ( line ) => line . trim ( ) )
@@ -92,6 +111,82 @@ async function readSessionIdFromTranscriptHeaderAsync(sessionFile: string): Prom
92111 }
93112}
94113
114+ function parseTranscriptLineId (
115+ line : string ,
116+ ) : { kind : "session" } | { kind : "entry" ; id : string } | null {
117+ try {
118+ const parsed = JSON . parse ( line ) as { type ?: unknown ; id ?: unknown } ;
119+ if ( parsed . type === "session" ) {
120+ return { kind : "session" } ;
121+ }
122+ if ( typeof parsed . id === "string" && parsed . id . trim ( ) ) {
123+ return { kind : "entry" , id : parsed . id . trim ( ) } ;
124+ }
125+ } catch {
126+ return null ;
127+ }
128+ return null ;
129+ }
130+
131+ export async function readSessionLeafIdFromTranscriptAsync (
132+ sessionFile : string ,
133+ maxBytes = MAX_COMPACTION_CHECKPOINT_SNAPSHOT_BYTES ,
134+ ) : Promise < string | null > {
135+ let fileHandle : AsyncTranscriptFileHandle | undefined ;
136+ try {
137+ fileHandle = await fs . open ( sessionFile , "r" ) ;
138+ const stat = await fileHandle . stat ( ) ;
139+ if ( ! stat . isFile ( ) || stat . size <= 0 ) {
140+ return null ;
141+ }
142+
143+ const requestedMaxBytes = Number . isFinite ( maxBytes )
144+ ? Math . max ( 1024 , Math . floor ( maxBytes ) )
145+ : MAX_COMPACTION_CHECKPOINT_SNAPSHOT_BYTES ;
146+ const maxReadableBytes = Math . min ( stat . size , requestedMaxBytes ) ;
147+ let readLength = Math . min ( maxReadableBytes , SESSION_TAIL_READ_INITIAL_BYTES ) ;
148+ while ( readLength > 0 ) {
149+ const readStart = Math . max ( 0 , stat . size - readLength ) ;
150+ const buffer = await readFileRangeAsync ( fileHandle , readStart , readLength ) ;
151+ const lines = buffer . toString ( "utf-8" ) . split ( / \r ? \n / ) ;
152+ // If we did not read from the beginning, the first line may be a suffix of
153+ // a larger JSONL entry. Ignore it and grow the window if no complete entry
154+ // is found.
155+ const candidateLines = readStart > 0 ? lines . slice ( 1 ) : lines ;
156+ for ( let i = candidateLines . length - 1 ; i >= 0 ; i -= 1 ) {
157+ const line = candidateLines [ i ] ?. trim ( ) ;
158+ if ( ! line ) {
159+ continue ;
160+ }
161+ const parsed = parseTranscriptLineId ( line ) ;
162+ if ( ! parsed ) {
163+ continue ;
164+ }
165+ if ( parsed . kind === "session" ) {
166+ return null ;
167+ }
168+ return parsed . id ;
169+ }
170+
171+ if ( readStart === 0 ) {
172+ return null ;
173+ }
174+ const nextReadLength = Math . min ( maxReadableBytes , readLength * 2 ) ;
175+ if ( nextReadLength === readLength ) {
176+ return null ;
177+ }
178+ readLength = nextReadLength ;
179+ }
180+ } catch {
181+ return null ;
182+ } finally {
183+ if ( fileHandle ) {
184+ await fileHandle . close ( ) . catch ( ( ) => undefined ) ;
185+ }
186+ }
187+ return null ;
188+ }
189+
95190/**
96191 * Synchronous version — kept for callers that cannot be made async.
97192 * Prefer captureCompactionCheckpointSnapshotAsync for large transcripts
@@ -165,7 +260,7 @@ export function captureCompactionCheckpointSnapshot(params: {
165260 * (see issue #75414).
166261 */
167262export async function captureCompactionCheckpointSnapshotAsync ( params : {
168- sessionManager : Pick < SessionManager , "getLeafId" > ;
263+ sessionManager ? : Pick < SessionManager , "getLeafId" > ;
169264 sessionFile : string ;
170265 maxBytes ?: number ;
171266} ) : Promise < CapturedCompactionCheckpointSnapshot | null > {
@@ -174,7 +269,11 @@ export async function captureCompactionCheckpointSnapshotAsync(params: {
174269 ? params . sessionManager . getLeafId . bind ( params . sessionManager )
175270 : null ;
176271 const sessionFile = params . sessionFile . trim ( ) ;
177- if ( ! getLeafId || ! sessionFile ) {
272+ if ( ! sessionFile || ( params . sessionManager && ! getLeafId ) ) {
273+ return null ;
274+ }
275+ const liveLeafId = getLeafId ? getLeafId ( ) : undefined ;
276+ if ( getLeafId && ! liveLeafId ) {
178277 return null ;
179278 }
180279 const maxBytes = params . maxBytes ?? MAX_COMPACTION_CHECKPOINT_SNAPSHOT_BYTES ;
@@ -186,10 +285,6 @@ export async function captureCompactionCheckpointSnapshotAsync(params: {
186285 } catch {
187286 return null ;
188287 }
189- const leafId = getLeafId ( ) ;
190- if ( ! leafId ) {
191- return null ;
192- }
193288 const parsedSessionFile = path . parse ( sessionFile ) ;
194289 const snapshotFile = path . join (
195290 parsedSessionFile . dir ,
@@ -201,7 +296,8 @@ export async function captureCompactionCheckpointSnapshotAsync(params: {
201296 return null ;
202297 }
203298 const sessionId = await readSessionIdFromTranscriptHeaderAsync ( snapshotFile ) ;
204- if ( ! sessionId ) {
299+ const leafId = liveLeafId ?? ( await readSessionLeafIdFromTranscriptAsync ( snapshotFile , maxBytes ) ) ;
300+ if ( ! sessionId || ! leafId ) {
205301 try {
206302 await fs . unlink ( snapshotFile ) ;
207303 } catch {
0 commit comments