@@ -52,6 +52,16 @@ type QaCrablineTransportState = QaTransportState & {
5252
5353const TELEGRAM_LIFECYCLE_METHOD_RE = / \/ ( s e n d M e s s a g e | e d i t M e s s a g e T e x t | d e l e t e M e s s a g e ) $ / u;
5454
55+ function readRecorderLines ( text : string , options : { allowIncompleteTail : boolean } ) : string [ ] {
56+ // Crabline appends each JSONL record asynchronously, so a concurrent read can end mid-record.
57+ // Defer only the unterminated tail; newline-terminated malformed records must still fail parsing.
58+ const lines = text . split ( / \r ? \n / u) ;
59+ if ( options . allowIncompleteTail && ! text . endsWith ( "\n" ) ) {
60+ lines . pop ( ) ;
61+ }
62+ return lines . filter ( ( line ) => line . trim ( ) . length > 0 ) ;
63+ }
64+
5565function readTelegramLifecycleEvent ( params : {
5666 cursor : number ;
5767 event : unknown ;
@@ -217,43 +227,45 @@ function createCrablineState(params: {
217227 let recorderLineCursor = 0 ;
218228 let syncPromise : Promise < void > | null = null ;
219229
220- const syncRecorder = async ( ) => {
221- if ( syncPromise ) {
222- return await syncPromise ;
223- }
224- syncPromise = ( async ( ) => {
225- const text = await fs
226- . readFile ( params . adapter . manifest . recorderPath , "utf8" )
227- . catch ( ( error : unknown ) => {
228- if ( ( error as NodeJS . ErrnoException ) . code === "ENOENT" ) {
229- return "" ;
230- }
231- throw error ;
232- } ) ;
233- const lines = text . split ( / \r ? \n / u) . filter ( ( line ) => line . trim ( ) . length > 0 ) ;
234- for ( const line of lines . slice ( recorderLineCursor ) ) {
235- const parsed = JSON . parse ( line ) as unknown ;
236- if ( params . adapter . channel === "telegram" ) {
237- const lifecycle = readTelegramLifecycleEvent ( {
238- cursor : outboundEvents . length + 1 ,
239- event : parsed ,
240- messageByProviderId : telegramMessageByProviderId ,
241- pendingByChat : pendingTelegramMessagesByChat ,
242- } ) ;
243- if ( lifecycle ) {
244- outboundEvents . push ( lifecycle ) ;
245- }
230+ const syncRecorderSnapshot = async ( options : { allowIncompleteTail : boolean } ) => {
231+ const text = await fs
232+ . readFile ( params . adapter . manifest . recorderPath , "utf8" )
233+ . catch ( ( error : unknown ) => {
234+ if ( ( error as NodeJS . ErrnoException ) . code === "ENOENT" ) {
235+ return "" ;
246236 }
247- const outbound = params . adapter . createOutboundFromRecorderEvent ( {
237+ throw error ;
238+ } ) ;
239+ const lines = readRecorderLines ( text , options ) ;
240+ for ( const line of lines . slice ( recorderLineCursor ) ) {
241+ const parsed = JSON . parse ( line ) as unknown ;
242+ if ( params . adapter . channel === "telegram" ) {
243+ const lifecycle = readTelegramLifecycleEvent ( {
244+ cursor : outboundEvents . length + 1 ,
248245 event : parsed ,
249- targetByProviderTarget,
250- } ) as QaBusOutboundMessageInput | null ;
251- if ( outbound ) {
252- baseState . addOutboundMessage ( outbound ) ;
246+ messageByProviderId : telegramMessageByProviderId ,
247+ pendingByChat : pendingTelegramMessagesByChat ,
248+ } ) ;
249+ if ( lifecycle ) {
250+ outboundEvents . push ( lifecycle ) ;
253251 }
254252 }
255- recorderLineCursor = lines . length ;
256- } ) ( ) ;
253+ const outbound = params . adapter . createOutboundFromRecorderEvent ( {
254+ event : parsed ,
255+ targetByProviderTarget,
256+ } ) as QaBusOutboundMessageInput | null ;
257+ if ( outbound ) {
258+ baseState . addOutboundMessage ( outbound ) ;
259+ }
260+ }
261+ recorderLineCursor = lines . length ;
262+ } ;
263+
264+ const syncRecorder = async ( ) => {
265+ if ( syncPromise ) {
266+ return await syncPromise ;
267+ }
268+ syncPromise = syncRecorderSnapshot ( { allowIncompleteTail : true } ) ;
257269 try {
258270 await syncPromise ;
259271 } finally {
@@ -276,7 +288,7 @@ function createCrablineState(params: {
276288 outboundEvents . length = 0 ;
277289 recorderLineCursor = await fs
278290 . readFile ( params . adapter . manifest . recorderPath , "utf8" )
279- . then ( ( text ) => text . split ( / \r ? \n / u ) . filter ( ( line ) => line . trim ( ) . length > 0 ) . length )
291+ . then ( ( text ) => readRecorderLines ( text , { allowIncompleteTail : true } ) . length )
280292 . catch ( ( ) => 0 ) ;
281293 } ,
282294 getSnapshot : baseState . getSnapshot . bind ( baseState ) ,
@@ -313,8 +325,11 @@ function createCrablineState(params: {
313325 } ,
314326 async cleanup ( ) {
315327 clearInterval ( interval ) ;
316- await syncRecorder ( ) ;
317328 await params . adapter . close ( ) ;
329+ if ( syncPromise ) {
330+ await syncPromise ;
331+ }
332+ await syncRecorderSnapshot ( { allowIncompleteTail : false } ) ;
318333 } ,
319334 } ;
320335}
0 commit comments