1+ import { sleep } from '@sim/utils/helpers'
12import { isRecordLike } from '@sim/utils/object'
23import type { ReadFileTextResponse } from 'sim/embed'
34import { listCatalogTools } from '@/lib/catalog/application/list-tools'
45import { readBlockCatalog } from '@/lib/catalog/application/read-block-catalog'
6+ import { compileLinearRegex , isPlainText , literalRegex } from '@/lib/core/security/linear-regex'
57import { enginePrincipal } from '@/lib/mothership/agent-cli/engine-principal'
68import {
79 type AgentCliEngine ,
@@ -46,6 +48,9 @@ const CATALOG_ALL = 100_000
4648const MAX_FILES = 300
4749const FILE_READ_CONCURRENCY = 5
4850const MAX_BYTES_PER_FILE = 262_144
51+ /** Bound scanning after materialization without charging backend read latency. */
52+ const MAX_SCAN_TIME_MS = 5_000
53+ const SCAN_YIELD_INTERVAL_MS = 10
4954/** `workflow:<uuid>` — a prefixed form no world or resource ever prints as its path. */
5055const PREFIX_SELECTOR = / ^ \w + : /
5156const PLATFORM_SCOPES : ReadonlySet < Scope > = new Set < Scope > ( [ 'blocks' , 'tools' ] )
@@ -416,13 +421,12 @@ async function materializeWithin(
416421}
417422
418423function compilePattern ( raw : string , ignoreCase : boolean ) : ( line : string ) => boolean {
419- try {
420- const regex = new RegExp ( raw , ignoreCase ? 'i' : '' )
421- return ( line ) => regex . test ( line )
422- } catch {
423- const needle = ignoreCase ? raw . toLowerCase ( ) : raw
424- return ( line ) => ( ignoreCase ? line . toLowerCase ( ) : line ) . includes ( needle )
425- }
424+ const regex = isPlainText ( raw )
425+ ? literalRegex ( raw , { ignoreCase } )
426+ : compileLinearRegex ( raw , { ignoreCase } )
427+ if ( regex ) return ( line ) => regex . test ( line )
428+ const needle = ignoreCase ? raw . toLowerCase ( ) : raw
429+ return ( line ) => ( ignoreCase ? line . toLowerCase ( ) : line ) . includes ( needle )
426430}
427431
428432function clip ( line : string ) : string {
@@ -570,11 +574,27 @@ export const universalGrepCommand: AgentCliEngine = {
570574 let total = 0
571575 let shownMatches = 0
572576 const perScope = new Map < Scope , number > ( )
577+ const scanStartedAt = performance . now ( )
578+ let lastYieldAt = scanStartedAt
579+ const checkScan = ( ) => {
580+ runtime . signal ?. throwIfAborted ( )
581+ const now = performance . now ( )
582+ if ( now - scanStartedAt >= MAX_SCAN_TIME_MS ) {
583+ throw new Error (
584+ 'Search incomplete: scanning exceeded the time budget. Narrow with --scope or --in.'
585+ )
586+ }
587+ return now
588+ }
573589 for ( const resource of candidates ) {
574590 if ( resource . text === null ) continue
575591 const lines = resource . text . split ( '\n' )
576592 const selected = new Set < number > ( )
577593 for ( let i = 0 ; i < lines . length ; i ++ ) {
594+ if ( checkScan ( ) - lastYieldAt >= SCAN_YIELD_INTERVAL_MS ) {
595+ await sleep ( 0 )
596+ lastYieldAt = checkScan ( )
597+ }
578598 if ( ! matches ( lines [ i ] ) ) continue
579599 total ++
580600 perScope . set ( resource . scope , ( perScope . get ( resource . scope ) ?? 0 ) + 1 )
@@ -595,6 +615,7 @@ export const universalGrepCommand: AgentCliEngine = {
595615 if ( matches ( lines [ i ] ) ) shownMatches ++
596616 }
597617 }
618+ checkScan ( )
598619
599620 if ( countOnly ) {
600621 const breakdown = [ ...perScope . entries ( ) ] . map ( ( [ s , n ] ) => `${ s } =${ n } ` ) . join ( ' ' )
0 commit comments