diff --git a/src/QueryEngine.ts b/src/QueryEngine.ts index c9697c768..8e48ed757 100644 --- a/src/QueryEngine.ts +++ b/src/QueryEngine.ts @@ -248,6 +248,7 @@ export class QueryEngine { } = this.config this.discoveredSkillNames.clear() + this.permissionDenials = [] setCwd(cwd) const persistSession = !isSessionPersistenceDisabled() const startTime = Date.now() diff --git a/src/cli/transports/SSETransport.ts b/src/cli/transports/SSETransport.ts index 42b90afbd..110f71957 100644 --- a/src/cli/transports/SSETransport.ts +++ b/src/cli/transports/SSETransport.ts @@ -350,6 +350,7 @@ export class SSETransport implements Transport { const reader = body.getReader() const decoder = new TextDecoder() let buffer = '' + const MAX_BUFFER_BYTES = 1024 * 1024 // 1MB — SSE frames include event/data/id prefixes try { while (true) { @@ -357,6 +358,14 @@ export class SSETransport implements Transport { if (done) break buffer += decoder.decode(value, STREAM_DECODE_OPTS) + if (buffer.length > MAX_BUFFER_BYTES) { + logForDebugging( + `SSETransport: Buffer exceeded ${MAX_BUFFER_BYTES} bytes — dropping connection`, + { level: 'error' }, + ) + logForDiagnosticsNoPII('error', 'cli_sse_buffer_overflow') + break + } const { frames, remaining } = parseSSEFrames(buffer) buffer = remaining diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index fe26e38cf..0cd46d38c 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -305,8 +305,9 @@ import { import { deserializeMessages } from '../utils/conversationRecovery.js'; import { extractReadFilesFromMessages, extractBashToolsFromMessages } from '../utils/queryHelpers.js'; import { resetMicrocompactState } from '../services/compact/microCompact.js'; -import { runPostCompactCleanup } from '../services/compact/postCompactCleanup.js'; +import { runPostCompactCleanup, registerCompactCleanup } from '../services/compact/postCompactCleanup.js'; import { + createContentReplacementState, provisionContentReplacementState, reconstructContentReplacementState, type ContentReplacementRecord, @@ -1778,6 +1779,9 @@ export function REPL({ const [contentReplacementStateRef] = useState(() => ({ current: provisionContentReplacementState(initialMessages, initialContentReplacements), })); + registerCompactCleanup(() => { + contentReplacementStateRef.current = createContentReplacementState(); + }); const [haveShownCostDialog, setHaveShownCostDialog] = useState(getGlobalConfig().hasAcknowledgedCostThreshold); const [vimMode, setVimMode] = useState('INSERT'); diff --git a/src/services/compact/postCompactCleanup.ts b/src/services/compact/postCompactCleanup.ts index b89e3a0be..b52730217 100644 --- a/src/services/compact/postCompactCleanup.ts +++ b/src/services/compact/postCompactCleanup.ts @@ -10,6 +10,17 @@ import { clearSessionMessagesCache } from '../../utils/sessionStorage.js' import { clearBetaTracingState } from '../../utils/telemetry/betaSessionTracing.js' import { resetMicrocompactState } from './microCompact.js' +/** + * Compact-scoped cleanup callbacks registered by REPL or other long-lived + * components. Called during runPostCompactCleanup() so instance-scoped state + * (e.g. contentReplacementState) is freed alongside module-level caches. + */ +const compactCleanupCallbacks: Array<() => void> = [] + +export function registerCompactCleanup(callback: () => void): void { + compactCleanupCallbacks.push(callback) +} + /** * Run cleanup of caches and tracking state after compaction. * Call this after both auto-compact and manual /compact to free memory @@ -88,4 +99,11 @@ export function runPostCompactCleanup(querySource?: QuerySource): void { }) } clearSessionMessagesCache() + for (const cb of compactCleanupCallbacks) { + try { + cb() + } catch (error) { + logError(error) + } + } } diff --git a/src/utils/log.ts b/src/utils/log.ts index 7d15b2fb3..7b85e4cf7 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -17,7 +17,7 @@ import { import { CACHE_PATHS } from './cachePaths.js' import { stripDisplayTags, stripDisplayTagsAllowEmpty } from './displayTags.js' import { isEnvTruthy } from './envUtils.js' -import { toError } from './errors.js' +import { toError, shortErrorStack } from './errors.js' import { isEssentialTrafficOnly } from './privacyLevel.js' import { jsonParse } from './slowOperations.js' @@ -175,7 +175,7 @@ export function logError(error: unknown): void { return } - const errorStr = err.stack || err.message + const errorStr = shortErrorStack(err) const errorInfo = { error: errorStr,