mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-16 14:27:24 -07:00
passing git add packages/.git add packages/.
This commit is contained in:
@@ -20,7 +20,7 @@ describe('Context Management Fidelity E2E', () => {
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should reproduce the exact context working buffer on resume', async () => {
|
||||
it('should reproduce the exact context working buffer on resume', { timeout: 30000 }, async () => {
|
||||
// Mock responses to trigger GC (summarization)
|
||||
const snapshotResponse: FakeResponse = {
|
||||
method: 'generateContent',
|
||||
@@ -205,28 +205,35 @@ describe('Context Management Fidelity E2E', () => {
|
||||
contextBeforeExit!.length,
|
||||
);
|
||||
|
||||
for (let i = 0; i < contextBeforeExit!.length; i++) {
|
||||
expect(contextAfterResume![i].id).toBe(contextBeforeExit![i].id);
|
||||
expect(contextAfterResume![i].content).toEqual(
|
||||
contextBeforeExit![i].content,
|
||||
try {
|
||||
for (let i = 0; i < contextBeforeExit!.length; i++) {
|
||||
expect(contextAfterResume![i].id).toBe(contextBeforeExit![i].id);
|
||||
expect(contextAfterResume![i].content).toEqual(
|
||||
contextBeforeExit![i].content,
|
||||
);
|
||||
}
|
||||
|
||||
// Most importantly, synthetic IDs (like summaries) must be stable.
|
||||
const syntheticTurns = contextBeforeExit!.filter(
|
||||
(t: HistoryTurn) => t.id && t.id.length === 32,
|
||||
); // deriveStableId produces 32-char hex
|
||||
expect(syntheticTurns.length).toBeGreaterThan(0);
|
||||
|
||||
const syntheticTurnsAfter = contextAfterResume!.filter(
|
||||
(t: HistoryTurn) => t.id && t.id.length === 32,
|
||||
);
|
||||
expect(syntheticTurnsAfter.length).toBeGreaterThanOrEqual(
|
||||
syntheticTurns.length,
|
||||
);
|
||||
|
||||
// Check if the first synthetic turn is identical
|
||||
expect(syntheticTurnsAfter[0].id).toBe(syntheticTurns[0].id);
|
||||
expect(syntheticTurnsAfter[0].content).toEqual(syntheticTurns[0].content);
|
||||
} catch (e) {
|
||||
console.error('--- DEBUG LOG ---');
|
||||
console.error(fs.readFileSync(commonEnv.GEMINI_DEBUG_LOG_FILE, 'utf-8'));
|
||||
console.error('-----------------');
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Most importantly, synthetic IDs (like summaries) must be stable.
|
||||
const syntheticTurns = contextBeforeExit!.filter(
|
||||
(t: HistoryTurn) => t.id && t.id.length === 32,
|
||||
); // deriveStableId produces 32-char hex
|
||||
expect(syntheticTurns.length).toBeGreaterThan(0);
|
||||
|
||||
const syntheticTurnsAfter = contextAfterResume!.filter(
|
||||
(t: HistoryTurn) => t.id && t.id.length === 32,
|
||||
);
|
||||
expect(syntheticTurnsAfter.length).toBeGreaterThanOrEqual(
|
||||
syntheticTurns.length,
|
||||
);
|
||||
|
||||
// Check if the first synthetic turn is identical
|
||||
expect(syntheticTurnsAfter[0].id).toBe(syntheticTurns[0].id);
|
||||
expect(syntheticTurnsAfter[0].content).toEqual(syntheticTurns[0].content);
|
||||
});
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,10 @@ describe('Context Management Resume E2E', () => {
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should preserve and utilize GC snapshot boundaries when resuming a session', async () => {
|
||||
it(
|
||||
'should preserve and utilize GC snapshot boundaries when resuming a session',
|
||||
{ timeout: 30000 },
|
||||
async () => {
|
||||
const snapshotResponse: FakeResponse = {
|
||||
method: 'generateContent',
|
||||
response: {
|
||||
@@ -145,5 +148,5 @@ describe('Context Management Resume E2E', () => {
|
||||
const traces = fs.readFileSync(traceLog, 'utf-8');
|
||||
expect(traces).toContain('Hitting Synchronous Pressure Barrier');
|
||||
expect(traces).toContain('GC Triggered.');
|
||||
});
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
convertSessionToHistoryFormats,
|
||||
type SessionInfo,
|
||||
} from '../../utils/sessionUtils.js';
|
||||
import type { Part } from '@google/genai';
|
||||
|
||||
export { convertSessionToHistoryFormats };
|
||||
|
||||
@@ -27,7 +26,7 @@ export const useSessionBrowser = (
|
||||
config: Config,
|
||||
onLoadHistory: (
|
||||
uiHistory: HistoryItemWithoutId[],
|
||||
clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }>,
|
||||
clientHistory: any[],
|
||||
resumedSessionData: ResumedSessionData,
|
||||
) => Promise<void>,
|
||||
) => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type ResumedSessionData,
|
||||
convertSessionToClientHistory,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Part } from '@google/genai';
|
||||
import type { HistoryItemWithoutId } from '../types.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { convertSessionToHistoryFormats } from './useSessionBrowser.js';
|
||||
@@ -54,7 +53,7 @@ export function useSessionResume({
|
||||
const loadHistoryForResume = useCallback(
|
||||
async (
|
||||
uiHistory: HistoryItemWithoutId[],
|
||||
clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }>,
|
||||
clientHistory: any[],
|
||||
resumedData: ResumedSessionData,
|
||||
) => {
|
||||
// Wait for the client.
|
||||
|
||||
@@ -398,7 +398,17 @@ export class ContextManager {
|
||||
});
|
||||
|
||||
if (pendingRequest) {
|
||||
hardenedHistory.pop(); // Remove the pending request from the final output
|
||||
const last = hardenedHistory[hardenedHistory.length - 1];
|
||||
if (last && last.content.parts) {
|
||||
const numPartsToRemove = pendingRequest.content.parts?.length || 0;
|
||||
if (numPartsToRemove > 0 && last.content.parts.length > numPartsToRemove) {
|
||||
last.content.parts.splice(-numPartsToRemove);
|
||||
} else {
|
||||
hardenedHistory.pop();
|
||||
}
|
||||
} else {
|
||||
hardenedHistory.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const apiHistory = hardenedHistory.map((h) => h.content);
|
||||
|
||||
@@ -27,7 +27,7 @@ export function fromGraph(
|
||||
let currentTurn: { id: string; content: Content } | null = null;
|
||||
|
||||
for (const node of nodes) {
|
||||
const turnId = node.turnId || 'orphan';
|
||||
const turnId = node.turnId || 'orphan'; debugLogger.log("[ID-TRACK] fromGraph converting node:", node.id, "turnId:", turnId);
|
||||
const durableId = turnId.startsWith('turn_') ? turnId.slice(5) : turnId;
|
||||
|
||||
// Register the payload in the identity service to ensure stability
|
||||
|
||||
@@ -211,13 +211,13 @@ export class ContextGraphBuilder {
|
||||
? part.functionCall.id
|
||||
: undefined;
|
||||
|
||||
const isSnapshot = isTextPart(part) && isSnapshotState(part.text);
|
||||
|
||||
// Use stable API ID if available, otherwise anchor to the turn and index.
|
||||
const id = apiId
|
||||
? `${apiId}_${turnSalt}_${partIdx}`
|
||||
: `${turnSalt}_${partIdx}`;
|
||||
|
||||
const isSnapshot = isTextPart(part) && isSnapshotState(part.text);
|
||||
|
||||
const node: ConcreteNode = {
|
||||
id,
|
||||
timestamp: Date.now(),
|
||||
|
||||
@@ -48,7 +48,12 @@ export interface SnapshotState {
|
||||
recent_arc: string[];
|
||||
}
|
||||
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
|
||||
export function isSnapshotState(text: string): boolean {
|
||||
import('../../utils/debugLogger.js').then(({ debugLogger }) => {
|
||||
debugLogger.log('[isSnapshotState] CALLED WITH:', text.substring(0, 50));
|
||||
});
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
||||
return false;
|
||||
@@ -56,13 +61,16 @@ export function isSnapshotState(text: string): boolean {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(trimmed);
|
||||
if (!isRecord(parsed)) return false;
|
||||
return (
|
||||
Array.isArray(parsed['active_tasks']) &&
|
||||
const isSnap = Array.isArray(parsed['active_tasks']) &&
|
||||
Array.isArray(parsed['discovered_facts']) &&
|
||||
Array.isArray(parsed['constraints_and_preferences']) &&
|
||||
Array.isArray(parsed['recent_arc'])
|
||||
);
|
||||
} catch {
|
||||
Array.isArray(parsed['recent_arc']);
|
||||
if (!isSnap) {
|
||||
debugLogger.log('[isSnapshotState] FAILED FOR JSON:', JSON.stringify(parsed));
|
||||
}
|
||||
return isSnap;
|
||||
} catch (e) {
|
||||
debugLogger.log('[isSnapshotState] PARSE FAILED FOR:', trimmed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +89,9 @@ export interface BaselineSnapshotInfo {
|
||||
export function findLatestSnapshotBaseline(
|
||||
targets: readonly ConcreteNode[],
|
||||
): BaselineSnapshotInfo | undefined {
|
||||
import('../../utils/debugLogger.js').then(({ debugLogger }) => {
|
||||
debugLogger.log('[findLatestSnapshotBaseline] Targets:', targets.map(t => ({ id: t.id, type: t.type, text: (t.payload as any).text?.substring(0, 20) })));
|
||||
});
|
||||
const lastSnapshotNode = [...targets]
|
||||
.reverse()
|
||||
.find((n) => n.type === NodeType.SNAPSHOT && n.payload.text);
|
||||
|
||||
@@ -337,7 +337,7 @@ export class GeminiClient {
|
||||
}
|
||||
|
||||
async resumeChat(
|
||||
history: Content[],
|
||||
history: any[],
|
||||
resumedSessionData?: ResumedSessionData,
|
||||
): Promise<void> {
|
||||
this.chat = await this.startChat(history, resumedSessionData);
|
||||
@@ -378,7 +378,7 @@ export class GeminiClient {
|
||||
}
|
||||
|
||||
async startChat(
|
||||
extraHistory?: Content[],
|
||||
extraHistory?: any[],
|
||||
resumedSessionData?: ResumedSessionData,
|
||||
): Promise<GeminiChat> {
|
||||
this.forceFullIdeContext = true;
|
||||
@@ -394,13 +394,16 @@ export class GeminiClient {
|
||||
: await getInitialChatHistory(this.config, extraHistory);
|
||||
|
||||
try {
|
||||
import('node:fs').then((fs) => {
|
||||
fs.appendFileSync('/tmp/gemini_startChat_history.log', JSON.stringify(history, null, 2) + '\n');
|
||||
});
|
||||
const systemMemory = this.config.getSystemInstructionMemory();
|
||||
const systemInstruction = getCoreSystemPrompt(this.config, systemMemory);
|
||||
const chat = new GeminiChat(
|
||||
this.config,
|
||||
systemInstruction,
|
||||
tools,
|
||||
history,
|
||||
history as any[],
|
||||
resumedSessionData,
|
||||
async (modelId: string) => {
|
||||
this.lastUsedModelId = modelId;
|
||||
@@ -421,7 +424,7 @@ export class GeminiClient {
|
||||
await reportError(
|
||||
error,
|
||||
'Error initializing Gemini chat session.',
|
||||
history,
|
||||
history as Content[],
|
||||
'startChat',
|
||||
);
|
||||
throw new Error(`Failed to initialize chat: ${getErrorMessage(error)}`);
|
||||
@@ -665,6 +668,7 @@ export class GeminiClient {
|
||||
currentBaseUnits = baseUnits;
|
||||
|
||||
if (didApplyManagement) {
|
||||
import('node:fs').then(fs => fs.appendFileSync('/tmp/gemini_sync.log', `[client.ts] setHistory called with ${newHistory.length} items!\n`));
|
||||
// If the manager pruned history, we update the chat before continuing.
|
||||
// Note: we don't include the pendingRequest in this setHistory,
|
||||
// because Turn.run will add it normally.
|
||||
|
||||
@@ -898,6 +898,7 @@ export class ChatRecordingService {
|
||||
|
||||
// 1. Sync content and IDs
|
||||
const newMessages: MessageRecord[] = history.map((turn) => {
|
||||
import('node:fs').then(fs => fs.appendFileSync('/tmp/gemini_sync.log', `[${new Date().toISOString()}] [updateMessages] Turn ID: ${turn.id}, text: ${turn.content.parts?.[0]?.text?.substring(0, 50)}\n`));
|
||||
const existing = this.cachedConversation?.messages.find(
|
||||
(m) => m.id === turn.id,
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { type Part, type PartListUnion } from '@google/genai';
|
||||
import { type ConversationRecord } from '../services/chatRecordingService.js';
|
||||
import { partListUnionToString } from '../core/geminiRequest.js';
|
||||
import { type HistoryTurn } from '../core/agentChatHistory.js';
|
||||
|
||||
/**
|
||||
* Converts a PartListUnion into a normalized array of Part objects.
|
||||
@@ -29,8 +30,8 @@ function ensurePartArray(content: PartListUnion): Part[] {
|
||||
*/
|
||||
export function convertSessionToClientHistory(
|
||||
messages: ConversationRecord['messages'],
|
||||
): Array<{ role: 'user' | 'model'; parts: Part[] }> {
|
||||
const clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }> = [];
|
||||
): HistoryTurn[] {
|
||||
const clientHistory: HistoryTurn[] = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.type === 'info' || msg.type === 'error' || msg.type === 'warning') {
|
||||
@@ -47,8 +48,11 @@ export function convertSessionToClientHistory(
|
||||
}
|
||||
|
||||
clientHistory.push({
|
||||
role: 'user',
|
||||
parts: ensurePartArray(msg.content),
|
||||
id: msg.id,
|
||||
content: {
|
||||
role: 'user',
|
||||
parts: ensurePartArray(msg.content),
|
||||
}
|
||||
});
|
||||
} else if (msg.type === 'gemini') {
|
||||
const modelParts: Part[] = [];
|
||||
@@ -85,8 +89,11 @@ export function convertSessionToClientHistory(
|
||||
}
|
||||
|
||||
clientHistory.push({
|
||||
role: 'model',
|
||||
parts: modelParts,
|
||||
id: msg.id,
|
||||
content: {
|
||||
role: 'model',
|
||||
parts: modelParts,
|
||||
}
|
||||
});
|
||||
|
||||
const functionResponseParts: Part[] = [];
|
||||
@@ -117,8 +124,11 @@ export function convertSessionToClientHistory(
|
||||
|
||||
if (functionResponseParts.length > 0) {
|
||||
clientHistory.push({
|
||||
role: 'user',
|
||||
parts: functionResponseParts,
|
||||
id: `${msg.id}_response`,
|
||||
content: {
|
||||
role: 'user',
|
||||
parts: functionResponseParts,
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -128,8 +138,11 @@ export function convertSessionToClientHistory(
|
||||
|
||||
if (modelParts.length > 0) {
|
||||
clientHistory.push({
|
||||
role: 'model',
|
||||
parts: modelParts,
|
||||
id: msg.id,
|
||||
content: {
|
||||
role: 'model',
|
||||
parts: modelParts,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -137,4 +150,4 @@ export function convertSessionToClientHistory(
|
||||
}
|
||||
|
||||
return clientHistory;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user