@@ -45,6 +45,11 @@ interface FrontierEntry {
4545 path : BrowserAdaptiveAction [ ]
4646}
4747
48+ interface AdaptiveNetworkFailureRetention {
49+ remainingCount : number
50+ remainingBytes : number
51+ }
52+
4853export async function exploreAdaptiveBrowserStateMachine ( {
4954 page,
5055 baseUrl,
@@ -79,6 +84,10 @@ export async function exploreAdaptiveBrowserStateMachine({
7984 let revisits = 0
8085 let keyboardActions = 0
8186 let exhausted : BrowserAdaptiveExplorationResult [ "summary" ] [ "budgetExhausted" ]
87+ const networkFailureRetention : AdaptiveNetworkFailureRetention = {
88+ remainingCount : Math . min ( contract . budgets . maxErrors , contract . descriptorLimits . maxDiagnostics ) ,
89+ remainingBytes : Math . floor ( contract . budgets . maxArtifactBytes / 8 ) ,
90+ }
8291
8392 const initial = await captureAdaptiveState ( page , contract , 0 , navigationScope )
8493 appendDiagnostics ( diagnostics , initial . diagnostics , contract . descriptorLimits . maxDiagnostics )
@@ -144,7 +153,7 @@ export async function exploreAdaptiveBrowserStateMachine({
144153 const newConsoleRecords = consoleErrorRecords ( observations . consoleMessages . slice ( beforeConsole ) )
145154 const newConsoleErrors = newConsoleRecords . map ( recordMessage )
146155 const newPageErrors = errorMessages ( observations . errors . slice ( beforeErrors ) )
147- const oracleEvidence = adaptiveOracleEvidence ( newConsoleRecords , observations . network . slice ( beforeNetwork ) , newPageErrors , contract , networkPolicy )
156+ const oracleEvidence = adaptiveOracleEvidence ( newConsoleRecords , observations . network . slice ( beforeNetwork ) , newPageErrors , contract , networkPolicy , networkFailureRetention )
148157 const fingerprints = [ ...oracleEvidence . fingerprints ]
149158 const existing = states . get ( stabilized . state . digest )
150159 const newState = ! existing
@@ -189,6 +198,7 @@ export async function exploreAdaptiveBrowserStateMachine({
189198 loadingAfter : stabilized . loading ,
190199 oracleFingerprints : fingerprints ,
191200 ...( oracleEvidence . networkFailures . length > 0 ? { networkFailures : oracleEvidence . networkFailures } : { } ) ,
201+ ...( oracleEvidence . networkFailureSummary ? { networkFailureSummary : oracleEvidence . networkFailureSummary } : { } ) ,
192202 ...( accessibilityFingerprints . length > 0 ? { accessibilityFindingFingerprints : accessibilityFingerprints } : { } ) ,
193203 } ,
194204 status : actionError ? "error" : scopeRejection ? "rejected" : newState ? "ok" : "revisited" ,
@@ -569,34 +579,70 @@ function consoleErrorRecords(records: object[]): Record<string, unknown>[] {
569579 return records . map ( ( record ) => record as Record < string , unknown > ) . filter ( ( record ) => record . type === "error" || record . level === "error" ) . slice ( 0 , 100 )
570580}
571581
572- function adaptiveOracleEvidence ( consoleRecords : Record < string , unknown > [ ] , networkRecords : object [ ] , pageErrors : string [ ] , contract : BrowserAdaptiveExplorationContract , networkPolicy ?: BrowserPreviewNetworkPolicy ) : { fingerprints : string [ ] ; networkFailures : BrowserAdaptiveNetworkFailure [ ] ; errorCount : number } {
582+ function adaptiveOracleEvidence ( consoleRecords : Record < string , unknown > [ ] , networkRecords : object [ ] , pageErrors : string [ ] , contract : BrowserAdaptiveExplorationContract , networkPolicy ?: BrowserPreviewNetworkPolicy , retention ?: AdaptiveNetworkFailureRetention ) : { fingerprints : string [ ] ; networkFailures : BrowserAdaptiveNetworkFailure [ ] ; networkFailureSummary ?: BrowserAdaptiveTransition [ "observations" ] [ "networkFailureSummary" ] ; errorCount : number } {
573583 const failures = networkRecords . map ( ( record ) => record as Record < string , unknown > ) . filter ( ( record ) => record . type === "requestfailed" )
574- const networkFailures = failures . map ( ( record ) : BrowserAdaptiveNetworkFailure => {
584+ const classifiedFailures = failures . map ( ( record ) : BrowserAdaptiveNetworkFailure & { expectedBlock : boolean } => {
575585 const url = typeof record . url === "string" ? record . url : ""
576586 const decision = networkPolicy ? browserPreviewNetworkDecision ( url , networkPolicy ) : { url, urlClassification : "invalid" as const , policyDecision : "unknown" as const , policyReason : "network-policy-unavailable" }
577587 const failure = networkFailureMessage ( record )
578588 const expectedBlock = decision . policyDecision === "blocked" && / E R R _ B L O C K E D _ B Y _ C L I E N T | b l o c k e d b y c l i e n t / i. test ( failure )
579- return { ...decision , ...( failure ? { failure } : { } ) , oracleFinding : ! expectedBlock || contract . oraclePolicy . policyBlocks === "finding" }
589+ return { ...decision , ...( failure ? { failure } : { } ) , oracleFinding : ! expectedBlock || contract . oraclePolicy . policyBlocks === "finding" , expectedBlock }
580590 } )
581- const expectedBlockUrls = networkFailures . filter ( ( failure ) => ! failure . oracleFinding ) . map ( ( failure ) => failure . url )
582- const findingConsoleRecords = consoleRecords . filter ( ( record ) => {
583- if ( ! / E R R _ B L O C K E D _ B Y _ C L I E N T / i . test ( recordMessage ( record ) ) ) return true
584- const location = objectRecord ( record . location )
585- const match = typeof location . url === "string" && location . url
586- ? expectedBlockUrls . indexOf ( location . url )
587- : expectedBlockUrls . length > 0 ? 0 : - 1
588- if ( match < 0 ) return true
589- expectedBlockUrls . splice ( match , 1 )
590- return false
591+ const unmatchedFailures = new Set ( classifiedFailures . map ( ( _failure , index ) => index ) )
592+ const consoleMessages = consoleRecords . flatMap ( ( record ) => {
593+ const message = recordMessage ( record )
594+ const locationUrl = objectRecord ( record . location ) . url
595+ const match = classifiedFailures . findIndex ( ( failure , index ) => unmatchedFailures . has ( index )
596+ && ( typeof locationUrl === "string" && locationUrl ? failure . url === locationUrl && failureTokenMatches ( message , failure . failure ) : failureTokenMatches ( message , failure . failure ) ) )
597+ if ( match < 0 ) return [ message ]
598+ unmatchedFailures . delete ( match )
599+ const failure = classifiedFailures [ match ] !
600+ return failure . expectedBlock && contract . oraclePolicy . policyBlocks === "evidence" ? [ ] : [ message ]
591601 } )
592- const consoleUrls = new Set ( findingConsoleRecords . map ( ( record ) => objectRecord ( record . location ) . url ) . filter ( ( url ) : url is string => typeof url === "string" ) )
593- const networkMessages = networkFailures . filter ( ( failure ) => failure . oracleFinding && ! consoleUrls . has ( failure . url ) ) . map ( ( failure ) => stableJson ( { type : "requestfailed" , url : failure . url , failure : failure . failure , urlClassification : failure . urlClassification , policyDecision : failure . policyDecision , policyReason : failure . policyReason } ) )
594- const messages = [ ... findingConsoleRecords . map ( recordMessage ) , ... pageErrors , ... networkMessages ]
602+ const networkMessages = classifiedFailures . filter ( ( failure , index ) => unmatchedFailures . has ( index ) && failure . oracleFinding ) . map ( networkFailureOracleMessage )
603+ const messages = [ ... consoleMessages , ... pageErrors , ... networkMessages ]
604+ const networkFailureSummary = classifiedFailures . length > 0 ? boundedNetworkFailureEvidence ( classifiedFailures , contract , retention ) : undefined
595605 return {
596606 fingerprints : [ ...new Set ( messages . map ( ( message ) => browserAdaptiveDigest ( "oracle" , message ) ) ) ] . sort ( ) ,
597- networkFailures,
598- errorCount : findingConsoleRecords . length + pageErrors . length + networkMessages . length ,
607+ networkFailures : networkFailureSummary ?. failures ?? [ ] ,
608+ networkFailureSummary : networkFailureSummary ?. summary ,
609+ errorCount : consoleMessages . length + pageErrors . length + networkMessages . length ,
610+ }
611+ }
612+
613+ function boundedNetworkFailureEvidence ( failures : Array < BrowserAdaptiveNetworkFailure & { expectedBlock : boolean } > , contract : BrowserAdaptiveExplorationContract , cumulativeRetention ?: AdaptiveNetworkFailureRetention ) : { failures : BrowserAdaptiveNetworkFailure [ ] ; summary : NonNullable < BrowserAdaptiveTransition [ "observations" ] [ "networkFailureSummary" ] > } {
614+ const ordered = failures . map ( ( { expectedBlock : _expectedBlock , ...failure } ) => failure ) . sort ( ( left , right ) => stableJson ( left ) . localeCompare ( stableJson ( right ) ) )
615+ const retention = cumulativeRetention ?? {
616+ remainingCount : Math . min ( contract . budgets . maxErrors , contract . descriptorLimits . maxDiagnostics ) ,
617+ remainingBytes : Math . floor ( contract . budgets . maxArtifactBytes / 8 ) ,
618+ }
619+ const retained : BrowserAdaptiveNetworkFailure [ ] = [ ]
620+ for ( const failure of ordered . slice ( 0 , retention . remainingCount ) ) {
621+ const bytes = Buffer . byteLength ( stableJson ( failure ) ) + 1
622+ if ( bytes > retention . remainingBytes ) break
623+ retained . push ( failure )
624+ retention . remainingCount -= 1
625+ retention . remainingBytes -= bytes
599626 }
627+ return {
628+ failures : retained ,
629+ summary : {
630+ total : failures . length ,
631+ retained : retained . length ,
632+ policyBlocks : failures . filter ( ( failure ) => failure . expectedBlock ) . length ,
633+ oracleFindings : failures . filter ( ( failure ) => failure . oracleFinding ) . length ,
634+ truncated : retained . length < failures . length ,
635+ } ,
636+ }
637+ }
638+
639+ function failureTokenMatches ( message : string , failure : string | undefined ) : boolean {
640+ const token = failure ?. match ( / E R R _ [ A - Z 0 - 9 _ ] + / i) ?. [ 0 ]
641+ return Boolean ( token && message . toUpperCase ( ) . includes ( token . toUpperCase ( ) ) )
642+ }
643+
644+ function networkFailureOracleMessage ( failure : BrowserAdaptiveNetworkFailure ) : string {
645+ return stableJson ( { type : "requestfailed" , url : failure . url , failure : failure . failure , urlClassification : failure . urlClassification , policyDecision : failure . policyDecision , policyReason : failure . policyReason } )
600646}
601647
602648function networkFailureMessage ( record : Record < string , unknown > ) : string {
0 commit comments