Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-05-18 - [Terminal Input Regex Recompilation Bottleneck]
**Learning:** Compiling regular expressions in a hot loop (like `validateInput` running for every incoming websocket chunk on terminal input) causes significant CPU overhead, especially as the number of policy patterns grows.
**Action:** Always pre-compile or cache `RegExp` objects on hot execution paths. Use bounded structures (like `LRU` cache or a bounded `Map` size check) if patterns can be dynamically generated, to prevent memory leaks from caching unbounded strings.
16 changes: 15 additions & 1 deletion packages/terminal/src/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export function validateShell(
return { allowed: true };
}

// Bounded cache for compiled regular expressions to avoid recompiling on every keystroke
const MAX_REGEX_CACHE_SIZE = 100;
const regexCache = new Map<string, RegExp>();

/**
* Check if input text contains blocked patterns.
* This is a best-effort filter β€” not a security boundary.
Expand All @@ -116,7 +120,17 @@ export function validateInput(
): AccessCheckResult {
for (const pattern of policy.inputBlockPatterns) {
try {
const re = new RegExp(pattern);
let re = regexCache.get(pattern);
if (!re) {
// Prevent memory leak if dynamic policies create unbounded patterns
if (regexCache.size >= MAX_REGEX_CACHE_SIZE) {
const firstKey = regexCache.keys().next().value;
if (firstKey !== undefined) regexCache.delete(firstKey);
}
re = new RegExp(pattern);
regexCache.set(pattern, re);
}

if (re.test(input)) {
return {
allowed: false,
Expand Down