@@ -3728,16 +3728,16 @@ describe("LcmContextEngine.bootstrap", () => {
37283728 expect ( reconcileSpy ) . toHaveBeenCalledTimes ( 1 ) ;
37293729 } ) ;
37303730
3731- it ( "uses the bulk import path for initial bootstrap" , async ( ) => {
3732- const sessionFile = createSessionFilePath ( "bulk " ) ;
3731+ it ( "uses the live ingest path for initial bootstrap" , async ( ) => {
3732+ const sessionFile = createSessionFilePath ( "bootstrap-ingest-path " ) ;
37333733 const sm = SessionManager . open ( sessionFile ) ;
37343734 sm . appendMessage ( {
37353735 role : "user" ,
3736- content : [ { type : "text" , text : "bulk one" } ] ,
3736+ content : [ { type : "text" , text : "ingest one" } ] ,
37373737 } as AgentMessage ) ;
37383738 sm . appendMessage ( {
37393739 role : "assistant" ,
3740- content : [ { type : "text" , text : "bulk two" } ] ,
3740+ content : [ { type : "text" , text : "ingest two" } ] ,
37413741 } as AgentMessage ) ;
37423742
37433743 const warnLog = vi . fn ( ) ;
@@ -3753,13 +3753,185 @@ describe("LcmContextEngine.bootstrap", () => {
37533753 const singleSpy = vi . spyOn ( engine . getConversationStore ( ) , "createMessage" ) ;
37543754
37553755 const result = await engine . bootstrap ( {
3756- sessionId : "bootstrap-bulk " ,
3756+ sessionId : "bootstrap-ingest-path " ,
37573757 sessionFile,
37583758 } ) ;
37593759
37603760 expect ( result . bootstrapped ) . toBe ( true ) ;
3761- expect ( bulkSpy ) . toHaveBeenCalledTimes ( 1 ) ;
3762- expect ( singleSpy ) . not . toHaveBeenCalled ( ) ;
3761+ expect ( result . importedMessages ) . toBe ( 2 ) ;
3762+ expect ( bulkSpy ) . not . toHaveBeenCalled ( ) ;
3763+ expect ( singleSpy ) . toHaveBeenCalledTimes ( 2 ) ;
3764+ } ) ;
3765+
3766+ it ( "externalizes oversized file blocks during first-time bootstrap and still reconciles later tail messages" , async ( ) => {
3767+ await withTempHome ( async ( ) => {
3768+ const sessionFile = createSessionFilePath ( "bootstrap-large-file-parity" ) ;
3769+ const fileText = `${ "bootstrap file line\n" . repeat ( 160 ) } done` ;
3770+ writeFileSync (
3771+ sessionFile ,
3772+ `${ JSON . stringify ( {
3773+ role : "user" ,
3774+ content : `<file name="bootstrap.md" mime="text/markdown">${ fileText } </file>` ,
3775+ } ) } \n`,
3776+ "utf8" ,
3777+ ) ;
3778+
3779+ const engine = createEngineWithConfig ( { largeFileTokenThreshold : 20 } ) ;
3780+ const sessionId = "bootstrap-large-file-parity" ;
3781+ const first = await engine . bootstrap ( { sessionId, sessionFile } ) ;
3782+ expect ( first ) . toEqual ( {
3783+ bootstrapped : true ,
3784+ importedMessages : 1 ,
3785+ } ) ;
3786+
3787+ const conversation = await engine . getConversationStore ( ) . getConversationBySessionId ( sessionId ) ;
3788+ expect ( conversation ) . not . toBeNull ( ) ;
3789+
3790+ const initiallyStored = await engine
3791+ . getConversationStore ( )
3792+ . getMessages ( conversation ! . conversationId ) ;
3793+ expect ( initiallyStored ) . toHaveLength ( 1 ) ;
3794+ expect ( initiallyStored [ 0 ] . content ) . toContain ( "[LCM File: file_" ) ;
3795+ expect ( initiallyStored [ 0 ] . content ) . not . toContain ( "<file name=" ) ;
3796+ expect ( initiallyStored [ 0 ] . content ) . not . toContain ( fileText . slice ( 0 , 64 ) ) ;
3797+
3798+ const fileIdMatch = initiallyStored [ 0 ] . content . match ( / f i l e _ [ a - f 0 - 9 ] { 16 } / ) ;
3799+ expect ( fileIdMatch ) . not . toBeNull ( ) ;
3800+ const storedFile = await engine . getSummaryStore ( ) . getLargeFile ( fileIdMatch ! [ 0 ] ) ;
3801+ expect ( storedFile ) . not . toBeNull ( ) ;
3802+ expect ( storedFile ! . fileName ) . toBe ( "bootstrap.md" ) ;
3803+ expect ( readFileSync ( storedFile ! . storageUri , "utf8" ) ) . toBe ( fileText ) ;
3804+
3805+ const parts = await engine . getConversationStore ( ) . getMessageParts ( initiallyStored [ 0 ] . messageId ) ;
3806+ expect ( parts ) . toHaveLength ( 1 ) ;
3807+ expect ( parts [ 0 ] . textContent ) . toContain ( "[LCM File: file_" ) ;
3808+
3809+ appendFileSync (
3810+ sessionFile ,
3811+ `${ JSON . stringify ( {
3812+ role : "assistant" ,
3813+ content : [ { type : "text" , text : "tail after externalized bootstrap" } ] ,
3814+ } ) } \n`,
3815+ "utf8" ,
3816+ ) ;
3817+
3818+ const second = await engine . bootstrap ( { sessionId, sessionFile } ) ;
3819+ expect ( second ) . toEqual ( {
3820+ bootstrapped : true ,
3821+ importedMessages : 1 ,
3822+ reason : "reconciled missing session messages" ,
3823+ } ) ;
3824+
3825+ const afterReconcile = await engine
3826+ . getConversationStore ( )
3827+ . getMessages ( conversation ! . conversationId ) ;
3828+ expect ( afterReconcile . map ( ( message ) => message . content ) ) . toEqual ( [
3829+ initiallyStored [ 0 ] . content ,
3830+ "tail after externalized bootstrap" ,
3831+ ] ) ;
3832+ } ) ;
3833+ } ) ;
3834+
3835+ it ( "externalizes inline images during first-time bootstrap" , async ( ) => {
3836+ const largeFilesDir = mkdtempSync ( join ( tmpdir ( ) , "lossless-claw-large-files-" ) ) ;
3837+ tempDirs . push ( largeFilesDir ) ;
3838+ const sessionFile = createSessionFilePath ( "bootstrap-inline-image-parity" ) ;
3839+ const base64Image = `iVBOR${ "A" . repeat ( 600 ) } ` ;
3840+ writeFileSync (
3841+ sessionFile ,
3842+ `${ JSON . stringify ( {
3843+ role : "user" ,
3844+ content : `[media attached: bootstrap.png]\n${ base64Image } \n` ,
3845+ } ) } \n`,
3846+ "utf8" ,
3847+ ) ;
3848+
3849+ const engine = createEngineWithConfig ( {
3850+ largeFileTokenThreshold : 20 ,
3851+ largeFilesDir,
3852+ } ) ;
3853+ const sessionId = "bootstrap-inline-image-parity" ;
3854+ const result = await engine . bootstrap ( { sessionId, sessionFile } ) ;
3855+ expect ( result . bootstrapped ) . toBe ( true ) ;
3856+ expect ( result . importedMessages ) . toBe ( 1 ) ;
3857+
3858+ const conversation = await engine . getConversationStore ( ) . getConversationBySessionId ( sessionId ) ;
3859+ expect ( conversation ) . not . toBeNull ( ) ;
3860+ const messages = await engine . getConversationStore ( ) . getMessages ( conversation ! . conversationId ) ;
3861+ expect ( messages ) . toHaveLength ( 1 ) ;
3862+ expect ( messages [ 0 ] . content ) . toContain ( "[User image: bootstrap.png" ) ;
3863+ expect ( messages [ 0 ] . content ) . not . toContain ( base64Image . slice ( 0 , 32 ) ) ;
3864+
3865+ const fileIdMatch = messages [ 0 ] . content . match ( / f i l e _ [ a - f 0 - 9 ] { 16 } / ) ;
3866+ expect ( fileIdMatch ) . not . toBeNull ( ) ;
3867+ const storedFile = await engine . getSummaryStore ( ) . getLargeFile ( fileIdMatch ! [ 0 ] ) ;
3868+ expect ( storedFile ) . not . toBeNull ( ) ;
3869+ expect ( storedFile ! . mimeType ) . toBe ( "image/png" ) ;
3870+ expect ( storedFile ! . storageUri ) . toContain ( `${ largeFilesDir } /${ conversation ! . conversationId } /` ) ;
3871+ } ) ;
3872+
3873+ it ( "externalizes oversized tool results during first-time bootstrap" , async ( ) => {
3874+ await withTempHome ( async ( ) => {
3875+ const sessionFile = createSessionFilePath ( "bootstrap-tool-result-parity" ) ;
3876+ const sm = SessionManager . open ( sessionFile ) ;
3877+ const toolOutput = `${ "bootstrap tool output\n" . repeat ( 160 ) } done` ;
3878+ sm . appendMessage ( {
3879+ role : "assistant" ,
3880+ content : [
3881+ {
3882+ type : "toolCall" ,
3883+ id : "call_bootstrap_externalized" ,
3884+ name : "exec" ,
3885+ input : { cmd : "cat large.txt" } ,
3886+ } ,
3887+ ] ,
3888+ } as AgentMessage ) ;
3889+ sm . appendMessage ( {
3890+ role : "toolResult" ,
3891+ toolCallId : "call_bootstrap_externalized" ,
3892+ toolName : "exec" ,
3893+ content : [
3894+ {
3895+ type : "tool_result" ,
3896+ tool_use_id : "call_bootstrap_externalized" ,
3897+ name : "exec" ,
3898+ content : [ { type : "text" , text : toolOutput } ] ,
3899+ } ,
3900+ ] ,
3901+ } as AgentMessage ) ;
3902+
3903+ const engine = createEngineWithConfig ( { largeFileTokenThreshold : 20 } ) ;
3904+ const sessionId = "bootstrap-tool-result-parity" ;
3905+ const result = await engine . bootstrap ( { sessionId, sessionFile } ) ;
3906+ expect ( result . bootstrapped ) . toBe ( true ) ;
3907+ expect ( result . importedMessages ) . toBe ( 2 ) ;
3908+
3909+ const conversation = await engine . getConversationStore ( ) . getConversationBySessionId ( sessionId ) ;
3910+ expect ( conversation ) . not . toBeNull ( ) ;
3911+ const messages = await engine . getConversationStore ( ) . getMessages ( conversation ! . conversationId ) ;
3912+ expect ( messages ) . toHaveLength ( 2 ) ;
3913+ expect ( messages [ 1 ] . content ) . toContain ( "[LCM Tool Output: file_" ) ;
3914+ expect ( messages [ 1 ] . content ) . toContain ( "tool=exec" ) ;
3915+ expect ( messages [ 1 ] . content ) . not . toContain ( toolOutput . slice ( 0 , 64 ) ) ;
3916+
3917+ const fileIdMatch = messages [ 1 ] . content . match ( / f i l e _ [ a - f 0 - 9 ] { 16 } / ) ;
3918+ expect ( fileIdMatch ) . not . toBeNull ( ) ;
3919+ const fileId = fileIdMatch ! [ 0 ] ;
3920+ const storedFile = await engine . getSummaryStore ( ) . getLargeFile ( fileId ) ;
3921+ expect ( storedFile ) . not . toBeNull ( ) ;
3922+ expect ( storedFile ! . fileName ) . toBe ( "exec.txt" ) ;
3923+ expect ( readFileSync ( storedFile ! . storageUri , "utf8" ) ) . toBe ( toolOutput ) ;
3924+
3925+ const parts = await engine . getConversationStore ( ) . getMessageParts ( messages [ 1 ] . messageId ) ;
3926+ expect ( parts ) . toHaveLength ( 1 ) ;
3927+ const metadata = JSON . parse ( parts [ 0 ] . metadata ?? "{}" ) as Record < string , unknown > ;
3928+ expect ( metadata ) . toMatchObject ( {
3929+ externalizedFileId : fileId ,
3930+ originalByteSize : Buffer . byteLength ( toolOutput , "utf8" ) ,
3931+ toolOutputExternalized : true ,
3932+ externalizationReason : "large_tool_result" ,
3933+ } ) ;
3934+ } ) ;
37633935 } ) ;
37643936
37653937 it ( "limits first-time bootstrap imports to the newest messages within bootstrapMaxTokens" , async ( ) => {
0 commit comments