mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
robustness(core): static checks to validate history is immutable (#21228)
This commit is contained in:
@@ -11,7 +11,9 @@ import type { Content } from '@google/genai';
|
||||
/**
|
||||
* Serializes chat history to a Markdown string.
|
||||
*/
|
||||
export function serializeHistoryToMarkdown(history: Content[]): string {
|
||||
export function serializeHistoryToMarkdown(
|
||||
history: readonly Content[],
|
||||
): string {
|
||||
return history
|
||||
.map((item) => {
|
||||
const text =
|
||||
@@ -49,7 +51,7 @@ export function serializeHistoryToMarkdown(history: Content[]): string {
|
||||
* Options for exporting chat history.
|
||||
*/
|
||||
export interface ExportHistoryOptions {
|
||||
history: Content[];
|
||||
history: readonly Content[];
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface MessageActionReturn {
|
||||
export interface LoadHistoryActionReturn<HistoryType = unknown> {
|
||||
type: 'load_history';
|
||||
history: HistoryType;
|
||||
clientHistory: Content[]; // The history for the generative client
|
||||
clientHistory: readonly Content[]; // The history for the generative client
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -255,7 +255,7 @@ export class GeminiClient {
|
||||
return this.chat !== undefined;
|
||||
}
|
||||
|
||||
getHistory(): Content[] {
|
||||
getHistory(): readonly Content[] {
|
||||
return this.getChat().getHistory();
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ export class GeminiClient {
|
||||
this.getChat().stripThoughtsFromHistory();
|
||||
}
|
||||
|
||||
setHistory(history: Content[]) {
|
||||
setHistory(history: readonly Content[]) {
|
||||
this.getChat().setHistory(history);
|
||||
this.updateTelemetryTokenCount();
|
||||
this.forceFullIdeContext = true;
|
||||
@@ -1171,7 +1171,7 @@ export class GeminiClient {
|
||||
/**
|
||||
* Masks bulky tool outputs to save context window space.
|
||||
*/
|
||||
private async tryMaskToolOutputs(history: Content[]): Promise<void> {
|
||||
private async tryMaskToolOutputs(history: readonly Content[]): Promise<void> {
|
||||
if (!this.config.getToolOutputMaskingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ export class GeminiChat {
|
||||
|
||||
private async makeApiCallAndProcessStream(
|
||||
modelConfigKey: ModelConfigKey,
|
||||
requestContents: Content[],
|
||||
requestContents: readonly Content[],
|
||||
prompt_id: string,
|
||||
abortSignal: AbortSignal,
|
||||
role: LlmRole,
|
||||
@@ -489,7 +489,7 @@ export class GeminiChat {
|
||||
let currentGenerateContentConfig: GenerateContentConfig =
|
||||
newAvailabilityConfig;
|
||||
let lastConfig: GenerateContentConfig = currentGenerateContentConfig;
|
||||
let lastContentsToUse: Content[] = requestContents;
|
||||
let lastContentsToUse: Content[] = [...requestContents];
|
||||
|
||||
const getAvailabilityContext = createAvailabilityContextProvider(
|
||||
this.config,
|
||||
@@ -528,9 +528,9 @@ export class GeminiChat {
|
||||
abortSignal,
|
||||
};
|
||||
|
||||
let contentsToUse = supportsModernFeatures(modelToUse)
|
||||
? contentsForPreviewModel
|
||||
: requestContents;
|
||||
let contentsToUse: Content[] = supportsModernFeatures(modelToUse)
|
||||
? [...contentsForPreviewModel]
|
||||
: [...requestContents];
|
||||
|
||||
const hookSystem = this.config.getHookSystem();
|
||||
if (hookSystem) {
|
||||
@@ -687,16 +687,10 @@ export class GeminiChat {
|
||||
* @return History contents alternating between user and model for the entire
|
||||
* chat session.
|
||||
*/
|
||||
getHistory(curated: boolean = false): Content[] {
|
||||
getHistory(curated: boolean = false): readonly Content[] {
|
||||
const history = curated
|
||||
? extractCuratedHistory(this.history)
|
||||
: this.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];
|
||||
}
|
||||
|
||||
@@ -714,8 +708,8 @@ export class GeminiChat {
|
||||
this.history.push(content);
|
||||
}
|
||||
|
||||
setHistory(history: Content[]): void {
|
||||
this.history = history;
|
||||
setHistory(history: readonly Content[]): void {
|
||||
this.history = [...history];
|
||||
this.lastPromptTokenCount = estimateTokenCountSync(
|
||||
this.history.flatMap((c) => c.parts || []),
|
||||
);
|
||||
@@ -742,7 +736,9 @@ export class GeminiChat {
|
||||
// To ensure our requests validate, the first function call in every model
|
||||
// turn within the active loop must have a `thoughtSignature` property.
|
||||
// If we do not do this, we will get back 400 errors from the API.
|
||||
ensureActiveLoopHasThoughtSignatures(requestContents: Content[]): Content[] {
|
||||
ensureActiveLoopHasThoughtSignatures(
|
||||
requestContents: readonly Content[],
|
||||
): readonly Content[] {
|
||||
// First, find the start of the active loop by finding the last user turn
|
||||
// with a text message, i.e. that is not a function response.
|
||||
let activeLoopStartIndex = -1;
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface LogEntry {
|
||||
}
|
||||
|
||||
export interface Checkpoint {
|
||||
history: Content[];
|
||||
history: readonly Content[];
|
||||
authType?: AuthType;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface RoutingDecision {
|
||||
*/
|
||||
export interface RoutingContext {
|
||||
/** The full history of the conversation. */
|
||||
history: Content[];
|
||||
history: readonly Content[];
|
||||
/** The immediate request parts to be processed. */
|
||||
request: PartListUnion;
|
||||
/** An abort signal to cancel an LLM call during routing. */
|
||||
|
||||
@@ -74,7 +74,9 @@ export class ContextBuilder {
|
||||
}
|
||||
|
||||
// Helper to convert Google GenAI Content[] to Safety Protocol ConversationTurn[]
|
||||
private convertHistoryToTurns(history: Content[]): ConversationTurn[] {
|
||||
private convertHistoryToTurns(
|
||||
history: readonly Content[],
|
||||
): ConversationTurn[] {
|
||||
const turns: ConversationTurn[] = [];
|
||||
let currentUserRequest: { text: string } | undefined;
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ export function modelStringToModelConfigAlias(model: string): string {
|
||||
* contain massive tool outputs (like large grep results or logs).
|
||||
*/
|
||||
async function truncateHistoryToBudget(
|
||||
history: Content[],
|
||||
history: readonly Content[],
|
||||
config: Config,
|
||||
): Promise<Content[]> {
|
||||
let functionResponseTokenCounter = 0;
|
||||
|
||||
@@ -664,7 +664,7 @@ export class ChatRecordingService {
|
||||
* Updates the conversation history based on the provided API Content array.
|
||||
* This is used to persist changes made to the history (like masking) back to disk.
|
||||
*/
|
||||
updateMessagesFromHistory(history: Content[]): void {
|
||||
updateMessagesFromHistory(history: readonly Content[]): void {
|
||||
if (!this.conversationFile) return;
|
||||
|
||||
try {
|
||||
|
||||
@@ -43,7 +43,7 @@ const EXEMPT_TOOLS = new Set([
|
||||
]);
|
||||
|
||||
export interface MaskingResult {
|
||||
newHistory: Content[];
|
||||
newHistory: readonly Content[];
|
||||
maskedCount: number;
|
||||
tokensSaved: number;
|
||||
}
|
||||
@@ -67,7 +67,10 @@ export interface MaskingResult {
|
||||
* are preserved until they collectively reach the threshold.
|
||||
*/
|
||||
export class ToolOutputMaskingService {
|
||||
async mask(history: Content[], config: Config): Promise<MaskingResult> {
|
||||
async mask(
|
||||
history: readonly Content[],
|
||||
config: Config,
|
||||
): Promise<MaskingResult> {
|
||||
const maskingConfig = await config.getToolOutputMaskingConfig();
|
||||
if (!maskingConfig.enabled || history.length === 0) {
|
||||
return { newHistory: history, maskedCount: 0, tokensSaved: 0 };
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { ToolCallRequestInfo } from '../scheduler/types.js';
|
||||
|
||||
export interface ToolCallData<HistoryType = unknown, ArgsType = unknown> {
|
||||
history?: HistoryType;
|
||||
clientHistory?: Content[];
|
||||
clientHistory?: readonly Content[];
|
||||
commitHash?: string;
|
||||
toolCall: {
|
||||
name: string;
|
||||
|
||||
@@ -226,7 +226,7 @@ export class GeminiCliSession {
|
||||
break;
|
||||
}
|
||||
|
||||
const transcript: Content[] = client.getHistory();
|
||||
const transcript: readonly Content[] = client.getHistory();
|
||||
const context: SessionContext = {
|
||||
sessionId,
|
||||
transcript,
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface AgentShell {
|
||||
|
||||
export interface SessionContext {
|
||||
sessionId: string;
|
||||
transcript: Content[];
|
||||
transcript: readonly Content[];
|
||||
cwd: string;
|
||||
timestamp: string;
|
||||
fs: AgentFilesystem;
|
||||
|
||||
Reference in New Issue
Block a user