@@ -8,7 +8,11 @@ const command = process.argv[2];
88const scratchRoot = process . env . KITCHEN_SINK_TMP_DIR || os . tmpdir ( ) ;
99
1010const LOG_SCAN_CHUNK_BYTES = 64 * 1024 ;
11+ const LOG_SCAN_FINDING_CONTEXT_CHARS = 2048 ;
12+ const LOG_SCAN_MAX_FILES = 5000 ;
1113const LOG_SCAN_MAX_FINDINGS = 100 ;
14+ const LOG_SCAN_MAX_LINE_CHARS = 16 * 1024 ;
15+ const LOG_SCAN_SEGMENT_OVERLAP_CHARS = 256 ;
1216
1317const readJson = ( file ) => JSON . parse ( fs . readFileSync ( file , "utf8" ) ) ;
1418const scratchFile = ( name ) => path . join ( scratchRoot , name ) ;
@@ -46,67 +50,109 @@ function scanTextFileLines(file, onLine) {
4650 const fd = fs . openSync ( file , "r" ) ;
4751 try {
4852 const buffer = Buffer . alloc ( LOG_SCAN_CHUNK_BYTES ) ;
49- let carry = "" ;
53+ let currentLine = "" ;
5054 let lineNumber = 1 ;
55+ const emitLine = ( line , info = { } ) => onLine ( line , lineNumber , info ) ;
56+ const appendLineText = ( text , complete ) => {
57+ currentLine += text ;
58+ while ( currentLine . length > LOG_SCAN_MAX_LINE_CHARS ) {
59+ const segment = currentLine . slice ( 0 , LOG_SCAN_MAX_LINE_CHARS ) ;
60+ currentLine = currentLine . slice ( LOG_SCAN_MAX_LINE_CHARS - LOG_SCAN_SEGMENT_OVERLAP_CHARS ) ;
61+ if ( emitLine ( segment , { truncated : true } ) === false ) {
62+ return false ;
63+ }
64+ }
65+ if ( complete ) {
66+ if ( emitLine ( currentLine ) === false ) {
67+ return false ;
68+ }
69+ currentLine = "" ;
70+ lineNumber += 1 ;
71+ }
72+ return true ;
73+ } ;
74+
5175 while ( true ) {
5276 const bytesRead = fs . readSync ( fd , buffer , 0 , buffer . length , null ) ;
5377 if ( bytesRead <= 0 ) {
5478 break ;
5579 }
56- const text = carry + buffer . subarray ( 0 , bytesRead ) . toString ( "utf8" ) ;
80+ const text = buffer . subarray ( 0 , bytesRead ) . toString ( "utf8" ) ;
5781 const lines = text . split ( / \r ? \n / u) ;
58- carry = lines . pop ( ) ?? "" ;
59- for ( const line of lines ) {
60- if ( onLine ( line , lineNumber ) === false ) {
82+ for ( let index = 0 ; index < lines . length - 1 ; index += 1 ) {
83+ if ( appendLineText ( lines [ index ] , true ) === false ) {
6184 return ;
6285 }
63- lineNumber += 1 ;
86+ }
87+ if ( appendLineText ( lines . at ( - 1 ) ?? "" , false ) === false ) {
88+ return ;
6489 }
6590 }
66- if ( carry . length > 0 ) {
67- onLine ( carry , lineNumber ) ;
91+ if ( currentLine . length > 0 ) {
92+ onLine ( currentLine , lineNumber ) ;
6893 }
6994 } finally {
7095 fs . closeSync ( fd ) ;
7196 }
7297}
7398
74- function scanLogs ( ) {
75- if ( ! process . env . KITCHEN_SINK_TMP_DIR ) {
76- throw new Error ( "KITCHEN_SINK_TMP_DIR is required for kitchen-sink log scans" ) ;
99+ function formatFindingLine ( line , pattern , info = { } ) {
100+ const matchIndex = Math . max ( 0 , line . search ( pattern ) ) ;
101+ const halfWindow = Math . floor ( LOG_SCAN_FINDING_CONTEXT_CHARS / 2 ) ;
102+ const start = Math . max ( 0 , matchIndex - halfWindow ) ;
103+ const end = Math . min ( line . length , start + LOG_SCAN_FINDING_CONTEXT_CHARS ) ;
104+ const prefix = start > 0 ? "... " : "" ;
105+ const suffix = end < line . length || info . truncated ? " ..." : "" ;
106+ return `${ prefix } ${ line . slice ( start , end ) } ${ suffix } ` ;
107+ }
108+
109+ function shouldScanLogFile ( entry ) {
110+ if ( ! ( / \. (?: l o g | j s o n l ) $ / u. test ( entry ) || / o p e n c l a w - k i t c h e n - s i n k - / u. test ( path . basename ( entry ) ) ) ) {
111+ return false ;
77112 }
78- const roots = [ scratchRoot , path . join ( process . env . HOME , ".openclaw" ) ] ;
79- const files = [ ] ;
80- const visit = ( entry ) => {
81- if ( ! fs . existsSync ( entry ) ) {
82- return ;
83- }
84- const stat = fs . lstatSync ( entry ) ;
85- if ( stat . isSymbolicLink ( ) ) {
86- return ;
87- }
88- if ( stat . isDirectory ( ) ) {
89- for ( const child of fs . readdirSync ( entry ) . toSorted ( ) ) {
90- visit ( path . join ( entry , child ) ) ;
113+ return ! normalizedPath ( entry ) . includes ( "/.npm/_logs/" ) ;
114+ }
115+
116+ function scanLogFiles ( roots , onFile ) {
117+ let scannedFiles = 0 ;
118+ for ( const root of roots ) {
119+ const pending = [ root ] ;
120+ while ( pending . length > 0 ) {
121+ const entry = pending . pop ( ) ;
122+ if ( ! entry || ! fs . existsSync ( entry ) ) {
123+ continue ;
91124 }
92- return ;
93- }
94- if ( / \. (?: l o g | j s o n l ) $ / u. test ( entry ) || / o p e n c l a w - k i t c h e n - s i n k - / u. test ( path . basename ( entry ) ) ) {
95- if ( normalizedPath ( entry ) . includes ( "/.npm/_logs/" ) ) {
96- return ;
125+ const stat = fs . lstatSync ( entry ) ;
126+ if ( stat . isSymbolicLink ( ) ) {
127+ continue ;
128+ }
129+ if ( stat . isDirectory ( ) ) {
130+ const children = fs . readdirSync ( entry ) . toSorted ( ( left , right ) => right . localeCompare ( left ) ) ;
131+ for ( const child of children ) {
132+ pending . push ( path . join ( entry , child ) ) ;
133+ }
134+ continue ;
135+ }
136+ if ( ! shouldScanLogFile ( entry ) ) {
137+ continue ;
138+ }
139+ scannedFiles += 1 ;
140+ if ( scannedFiles > LOG_SCAN_MAX_FILES ) {
141+ throw new Error ( `kitchen-sink log scan exceeded ${ LOG_SCAN_MAX_FILES } candidate files` ) ;
142+ }
143+ if ( onFile ( entry , scannedFiles ) === false ) {
144+ return scannedFiles ;
97145 }
98- files . push ( entry ) ;
99146 }
100- } ;
101- for ( const root of roots ) {
102- visit ( root ) ;
103- }
104- if ( files . length === 0 ) {
105- throw new Error (
106- "kitchen-sink log scan found no files under the isolated scratch root or OpenClaw home" ,
107- ) ;
108147 }
148+ return scannedFiles ;
149+ }
109150
151+ function scanLogs ( ) {
152+ if ( ! process . env . KITCHEN_SINK_TMP_DIR ) {
153+ throw new Error ( "KITCHEN_SINK_TMP_DIR is required for kitchen-sink log scans" ) ;
154+ }
155+ const roots = [ scratchRoot , path . join ( process . env . HOME , ".openclaw" ) ] ;
110156 const deny = [
111157 / \b u n c a u g h t e x c e p t i o n \b / iu,
112158 / \b u n h a n d l e d r e j e c t i o n \b / iu,
@@ -122,29 +168,36 @@ function scanLogs() {
122168 ] ;
123169 const findings = [ ] ;
124170 let omittedFindings = false ;
125- for ( const file of files ) {
126- scanTextFileLines ( file , ( line , lineNumber ) => {
171+ const scannedFiles = scanLogFiles ( roots , ( file ) => {
172+ scanTextFileLines ( file , ( line , lineNumber , info ) => {
127173 if ( allow . some ( ( pattern ) => pattern . test ( line ) ) ) {
128174 return true ;
129175 }
130- if ( deny . some ( ( pattern ) => pattern . test ( line ) ) ) {
176+ const matchedPattern = deny . find ( ( pattern ) => pattern . test ( line ) ) ;
177+ if ( matchedPattern ) {
131178 if ( findings . length >= LOG_SCAN_MAX_FINDINGS ) {
132179 omittedFindings = true ;
133180 return false ;
134181 }
135- findings . push ( `${ file } :${ lineNumber } : ${ line } ` ) ;
182+ findings . push ( `${ file } :${ lineNumber } : ${ formatFindingLine ( line , matchedPattern , info ) } ` ) ;
136183 }
137184 return true ;
138185 } ) ;
139186 if ( omittedFindings ) {
140- break ;
187+ return false ;
141188 }
189+ return true ;
190+ } ) ;
191+ if ( scannedFiles === 0 ) {
192+ throw new Error (
193+ "kitchen-sink log scan found no files under the isolated scratch root or OpenClaw home" ,
194+ ) ;
142195 }
143196 if ( findings . length > 0 ) {
144197 const suffix = omittedFindings ? "\n... additional findings omitted" : "" ;
145198 throw new Error ( `unexpected error-like log lines:\n${ findings . join ( "\n" ) } ${ suffix } ` ) ;
146199 }
147- console . log ( `log scan passed (${ files . length } file(s))` ) ;
200+ console . log ( `log scan passed (${ scannedFiles } file(s))` ) ;
148201}
149202
150203function readConfig ( ) {
0 commit comments