Compare commits

..

3 Commits

Author SHA1 Message Date
jacob314 ed74c13aed Fix code colorizer ansi escape bug. 2026-03-05 12:37:00 -08:00
jacob314 3ece48ebc0 Fix so shell calls are formatted nicely as bash code blocks 2026-03-05 09:54:18 -08:00
Darren Sander 3d4956aa57 perf(core): fix OOM crash in long-running sessions (#19608)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-03-05 04:23:07 +00:00
15 changed files with 303 additions and 22 deletions
@@ -0,0 +1,88 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="428" viewBox="0 0 920 428">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="428" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#f9e2af" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#f9e2af" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</text>
<text x="891" y="19" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="36" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="53" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
<text x="45" y="53" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">run_shell_command</text>
<text x="207" y="53" fill="#6c7086" textLength="666" lengthAdjust="spacingAndGlyphs">cat &lt;&lt; &apos;EOF&apos; &gt; foo.txtRoses are red,Violets are blue,The CLI is fast,And h</text>
<text x="873" y="53" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="53" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="70" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="87" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="87" fill="#00cdcd" textLength="27" lengthAdjust="spacingAndGlyphs">cat</text>
<text x="45" y="87" fill="#e5e5e5" textLength="36" lengthAdjust="spacingAndGlyphs"> &lt;&lt; </text>
<text x="81" y="87" fill="#cdcd00" textLength="45" lengthAdjust="spacingAndGlyphs">&apos;EOF&apos;</text>
<text x="126" y="87" fill="#e5e5e5" textLength="90" lengthAdjust="spacingAndGlyphs"> &gt; foo.txt</text>
<text x="891" y="87" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="104" fill="#e5e5e5" textLength="126" lengthAdjust="spacingAndGlyphs">Roses are red,</text>
<text x="891" y="104" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#e5e5e5" textLength="153" lengthAdjust="spacingAndGlyphs">Violets are blue,</text>
<text x="891" y="121" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="138" fill="#e5e5e5" textLength="144" lengthAdjust="spacingAndGlyphs">The CLI is fast,</text>
<text x="891" y="138" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#e5e5e5" textLength="144" lengthAdjust="spacingAndGlyphs">And helpful too.</text>
<text x="891" y="155" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="172" fill="#e5e5e5" textLength="144" lengthAdjust="spacingAndGlyphs">End of the poem.</text>
<text x="891" y="172" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="189" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs">EOF</text>
<text x="891" y="189" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="206" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="206" fill="#cdcd00" textLength="342" lengthAdjust="spacingAndGlyphs">&quot;Poem successfully written to foo.txt&quot;</text>
<text x="891" y="206" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="223" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">Note: </text>
<text x="72" y="240" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs">Command contains redirection which can be undesirable. </text>
<text x="891" y="240" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#333333" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">Tip: </text>
<text x="72" y="257" fill="#333333" textLength="576" lengthAdjust="spacingAndGlyphs">Toggle auto-edit (Shift+Tab) to allow redirection in the future.</text>
<text x="891" y="257" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="274" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Allow execution of: &apos;cat, heredoc (&lt;&lt;), redirection (&gt;), echo&apos;? </text>
<text x="891" y="274" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="306" width="9" height="17" fill="#001a00" />
<text x="18" y="308" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="306" width="9" height="17" fill="#001a00" />
<rect x="36" y="306" width="18" height="17" fill="#001a00" />
<text x="36" y="308" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<rect x="54" y="306" width="9" height="17" fill="#001a00" />
<rect x="63" y="306" width="90" height="17" fill="#001a00" />
<text x="63" y="308" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
<rect x="153" y="306" width="171" height="17" fill="#001a00" />
<text x="891" y="308" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="325" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> 2. Allow for this session </text>
<text x="891" y="325" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="342" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> 3. Allow for all future sessions </text>
<text x="891" y="342" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> 4. No, suggest changes (esc) </text>
<text x="891" y="359" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="376" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#f9e2af" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

@@ -90,6 +90,33 @@ exports[`ToolConfirmationQueue > renders ExitPlanMode tool confirmation with Suc
"
`;
exports[`ToolConfirmationQueue > renders a multiline shell command with syntax highlighting and redirection warning (SVG snapshot) 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
│ │
│ ? run_shell_command cat << 'EOF' > foo.txtRoses are red,Violets are blue,The CLI is fast,And h… │
│ │
│ cat << 'EOF' > foo.txt │
│ Roses are red, │
│ Violets are blue, │
│ The CLI is fast, │
│ And helpful too. │
│ End of the poem. │
│ EOF │
│ echo "Poem successfully written to foo.txt" │
│ │
│ Note: Command contains redirection which can be undesirable. │
│ Tip: Toggle auto-edit (Shift+Tab) to allow redirection in the future. │
│ Allow execution of: 'cat, heredoc (<<), redirection (>), echo'? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. Allow for all future sessions │
│ 4. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`ToolConfirmationQueue > renders expansion hint when content is long and constrained 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
@@ -240,6 +240,37 @@ describe('ToolConfirmationMessage', () => {
unmount();
});
it('should render multiline shell scripts with correct newlines and syntax highlighting (SVG snapshot)', async () => {
const confirmationDetails: SerializableConfirmationDetails = {
type: 'exec',
title: 'Confirm Multiline Script',
command: 'echo "hello"\nfor i in 1 2 3; do\n echo $i\ndone',
rootCommand: 'echo',
rootCommands: ['echo'],
};
const result = renderWithProviders(
<ToolConfirmationMessage
callId="test-call-id"
confirmationDetails={confirmationDetails}
config={mockConfig}
getPreferredEditor={vi.fn()}
availableTerminalHeight={30}
terminalWidth={80}
/>,
);
await result.waitUntilReady();
const output = result.lastFrame();
expect(output).toContain('echo "hello"');
expect(output).toContain('for i in 1 2 3; do');
expect(output).toContain('echo $i');
expect(output).toContain('done');
await expect(result).toMatchSvgSnapshot();
result.unmount();
});
describe('with folder trust', () => {
const editConfirmationDetails: SerializableConfirmationDetails = {
type: 'edit',
@@ -40,6 +40,7 @@ import {
import { AskUserDialog } from '../AskUserDialog.js';
import { ExitPlanModeDialog } from '../ExitPlanModeDialog.js';
import { WarningMessage } from './WarningMessage.js';
import { colorizeCode } from '../../utils/CodeColorizer.js';
import {
getDeceptiveUrlDetails,
toUnicodeUrl,
@@ -548,9 +549,19 @@ export const ToolConfirmationMessage: React.FC<
>
<Box flexDirection="column">
{commandsToDisplay.map((cmd, idx) => (
<Text key={idx} color={theme.text.link}>
{sanitizeForDisplay(cmd)}
</Text>
<Box
key={idx}
flexDirection="column"
paddingBottom={idx < commandsToDisplay.length - 1 ? 1 : 0}
>
{colorizeCode({
code: cmd,
language: 'bash',
maxWidth: Math.max(terminalWidth, 1),
settings,
hideLineNumbers: true,
})}
</Box>
))}
</Box>
</MaxSizedBox>
@@ -634,6 +645,7 @@ export const ToolConfirmationMessage: React.FC<
mcpToolDetailsText,
expandDetailsHintKey,
getPreferredEditor,
settings,
]);
const bodyOverflowDirection: 'top' | 'bottom' =
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="173" viewBox="0 0 920 173">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="173" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="2" fill="#cdcd00" textLength="63" lengthAdjust="spacingAndGlyphs">&quot;hello&quot;</text>
<text x="0" y="19" fill="#0000ee" textLength="27" lengthAdjust="spacingAndGlyphs">for</text>
<text x="27" y="19" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> i </text>
<text x="54" y="19" fill="#0000ee" textLength="18" lengthAdjust="spacingAndGlyphs">in</text>
<text x="72" y="19" fill="#e5e5e5" textLength="72" lengthAdjust="spacingAndGlyphs"> 1 2 3; </text>
<text x="144" y="19" fill="#0000ee" textLength="18" lengthAdjust="spacingAndGlyphs">do</text>
<text x="18" y="36" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="36" fill="#cd00cd" textLength="18" lengthAdjust="spacingAndGlyphs">$i</text>
<text x="0" y="53" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">done</text>
<text x="0" y="70" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Allow execution of: &apos;echo&apos;? </text>
<rect x="0" y="102" width="9" height="17" fill="#001a00" />
<text x="0" y="104" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="9" y="102" width="9" height="17" fill="#001a00" />
<rect x="18" y="102" width="18" height="17" fill="#001a00" />
<text x="18" y="104" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<rect x="36" y="102" width="9" height="17" fill="#001a00" />
<rect x="45" y="102" width="90" height="17" fill="#001a00" />
<text x="45" y="104" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
<rect x="135" y="102" width="135" height="17" fill="#001a00" />
<text x="0" y="121" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> 2. Allow for this session </text>
<text x="0" y="138" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> 3. No, suggest changes (esc) </text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -2,7 +2,9 @@
exports[`ToolConfirmationMessage > should display multiple commands for exec type when provided 1`] = `
"echo "hello"
ls -la
whoami
Allow execution of 3 commands?
@@ -35,6 +37,19 @@ Do you want to proceed?
"
`;
exports[`ToolConfirmationMessage > should render multiline shell scripts with correct newlines and syntax highlighting (SVG snapshot) 1`] = `
"echo "hello"
for i in 1 2 3; do
echo $i
done
Allow execution of: 'echo'?
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > should strip BiDi characters from MCP tool and server names 1`] = `
"MCP Server: testserver
Tool: testtool
@@ -1799,7 +1799,6 @@ export const useGeminiStream = (
addItem,
registerBackgroundShell,
consumeUserHint,
config,
isLowErrorVerbosity,
maybeAddSuppressedToolErrorNote,
maybeAddLowVerbosityFailureNote,
@@ -50,4 +50,36 @@ describe('colorizeCode', () => {
expect(lastFrame()).toMatch(/line 1\s*\n\s*\n\s*line 3/);
unmount();
});
it('does not let colors from ansi escape codes leak into colorized code', async () => {
const code = 'line 1\n\x1b[41mline 2 with red background\x1b[0m\nline 3';
const settings = new LoadedSettings(
{ path: '', settings: {}, originalSettings: {} },
{ path: '', settings: {}, originalSettings: {} },
{
path: '',
settings: { ui: { useAlternateBuffer: true, showLineNumbers: false } },
originalSettings: {
ui: { useAlternateBuffer: true, showLineNumbers: false },
},
},
{ path: '', settings: {}, originalSettings: {} },
true,
[],
);
const result = colorizeCode({
code,
language: 'javascript',
maxWidth: 80,
settings,
hideLineNumbers: true,
});
const renderResult = renderWithProviders(<>{result}</>);
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
});
});
+7 -5
View File
@@ -14,6 +14,7 @@ import type {
ElementContent,
RootContent,
} from 'hast';
import stripAnsi from 'strip-ansi';
import { themeManager } from '../themes/theme-manager.js';
import type { Theme } from '../themes/theme.js';
import {
@@ -98,16 +99,17 @@ function highlightAndRenderLine(
theme: Theme,
): React.ReactNode {
try {
const strippedLine = stripAnsi(line);
const getHighlightedLine = () =>
!language || !lowlight.registered(language)
? lowlight.highlightAuto(line)
: lowlight.highlight(language, line);
? lowlight.highlightAuto(strippedLine)
: lowlight.highlight(language, strippedLine);
const renderedNode = renderHastNode(getHighlightedLine(), theme, undefined);
return renderedNode !== null ? renderedNode : line;
return renderedNode !== null ? renderedNode : strippedLine;
} catch (_error) {
return line;
return stripAnsi(line);
}
}
@@ -238,7 +240,7 @@ export function colorizeCode({
<Text color={activeTheme.defaultColor}>{`${index + 1}`}</Text>
</Box>
)}
<Text color={activeTheme.colors.Gray}>{line}</Text>
<Text color={activeTheme.colors.Gray}>{stripAnsi(line)}</Text>
</Box>
));
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">line </text>
<text x="45" y="2" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">1</text>
<text x="0" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">line </text>
<text x="45" y="19" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">2</text>
<text x="63" y="19" fill="#89b4fa" textLength="36" lengthAdjust="spacingAndGlyphs">with</text>
<text x="99" y="19" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> red background </text>
<text x="0" y="36" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">line </text>
<text x="45" y="36" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">3</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`colorizeCode > does not let colors from ansi escape codes leak into colorized code 1`] = `
"line 1
line 2 with red background
line 3"
`;
+7 -3
View File
@@ -691,9 +691,13 @@ export class GeminiChat {
const history = curated
? extractCuratedHistory(this.history)
: this.history;
// Deep copy the history to avoid mutating the history outside of the
// chat session.
return structuredClone(history);
// Return a shallow copy of the array to prevent callers from mutating
// the internal history array (push/pop/splice). Content objects are
// shared references — callers MUST NOT mutate them in place.
// This replaces a prior structuredClone() which deep-copied the entire
// conversation on every call, causing O(n) memory pressure per turn
// that compounded into OOM crashes in long-running sessions.
return [...history];
}
/**
+9 -4
View File
@@ -241,6 +241,7 @@ export class Turn {
readonly pendingToolCalls: ToolCallRequestInfo[] = [];
private debugResponses: GenerateContentResponse[] = [];
private pendingCitations = new Set<string>();
private cachedResponseText: string | undefined = undefined;
finishReason: FinishReason | undefined = undefined;
constructor(
@@ -432,11 +433,15 @@ export class Turn {
/**
* Get the concatenated response text from all responses in this turn.
* This extracts and joins all text content from the model's responses.
* The result is cached since this is called multiple times per turn.
*/
getResponseText(): string {
return this.debugResponses
.map((response) => getResponseText(response))
.filter((text): text is string => text !== null)
.join(' ');
if (this.cachedResponseText === undefined) {
this.cachedResponseText = this.debugResponses
.map((response) => getResponseText(response))
.filter((text): text is string => text !== null)
.join(' ');
}
return this.cachedResponseText;
}
}
+19 -5
View File
@@ -232,6 +232,7 @@ type EventBacklogItem = {
export class CoreEventEmitter extends EventEmitter<CoreEvents> {
private _eventBacklog: EventBacklogItem[] = [];
private _backlogHead = 0;
private static readonly MAX_BACKLOG_SIZE = 10000;
constructor() {
@@ -243,8 +244,17 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
...args: CoreEvents[K]
): void {
if (this.listenerCount(event) === 0) {
if (this._eventBacklog.length >= CoreEventEmitter.MAX_BACKLOG_SIZE) {
this._eventBacklog.shift();
const backlogSize = this._eventBacklog.length - this._backlogHead;
if (backlogSize >= CoreEventEmitter.MAX_BACKLOG_SIZE) {
// Evict oldest entry. Use a head pointer instead of shift() to avoid
// O(n) array reindexing on every eviction at capacity.
(this._eventBacklog as unknown[])[this._backlogHead] = undefined;
this._backlogHead++;
// Compact once dead entries exceed half capacity to bound memory
if (this._backlogHead >= CoreEventEmitter.MAX_BACKLOG_SIZE / 2) {
this._eventBacklog = this._eventBacklog.slice(this._backlogHead);
this._backlogHead = 0;
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
this._eventBacklog.push({ event, args } as EventBacklogItem);
@@ -391,9 +401,13 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
* subscribes.
*/
drainBacklogs(): void {
const backlog = [...this._eventBacklog];
this._eventBacklog.length = 0; // Clear in-place
for (const item of backlog) {
const backlog = this._eventBacklog;
const head = this._backlogHead;
this._eventBacklog = [];
this._backlogHead = 0;
for (let i = head; i < backlog.length; i++) {
const item = backlog[i];
if (item === undefined) continue;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(this.emit as (event: keyof CoreEvents, ...args: unknown[]) => boolean)(
item.event,
@@ -87,7 +87,6 @@ export async function checkNextSpeaker(
lastComprehensiveMessage.parts &&
lastComprehensiveMessage.parts.length === 0
) {
lastComprehensiveMessage.parts.push({ text: '' });
return {
reasoning:
'The last message was a filler model message with no content (nothing for user to act on), model should speak next.',