passing git add packages/.git add packages/.

This commit is contained in:
Your Name
2026-05-15 05:32:49 +00:00
parent 26602f97fb
commit efdaf0cb6a
11 changed files with 100 additions and 53 deletions
+30 -23
View File
@@ -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);
});
+5 -2
View File
@@ -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.
+11 -1
View File
@@ -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);
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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);
+8 -4
View File
@@ -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,
);
+24 -11
View File
@@ -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;
}
}