Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/core/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ describe('Gemini Client (client.ts)', () => {
getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
getUseModelRouter: vi.fn().mockReturnValue(false),
getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
getTargetDir: vi.fn().mockReturnValue('/test/project/root'),
getCwd: vi.fn().mockReturnValue('/test/project/root'),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1764,7 +1764,7 @@ export class GeminiClient {
this.getHistoryShallow(),
lastCompletionTimestamp,
this.config.getClearContextOnIdle(),
opts,
{ ...opts, projectRoot: this.config.getTargetDir() },
);
if (!mcResult.meta) {
return false;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core/geminiChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1775,7 +1775,7 @@ export class GeminiChat {
this.history,
null,
this.config.getClearContextOnIdle(),
{ force: true },
{ force: true, projectRoot: this.config.getTargetDir() },
);
const mcMeta = mcResult.meta;

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/services/memoryPressureMonitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ function createMockConfig(
}
: overrides.geminiClient;
return {
getTargetDir: () => '/project',
getFileReadCache: () =>
({
clear: vi.fn(),
Expand Down
19 changes: 12 additions & 7 deletions packages/core/src/services/memoryPressureMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,13 +716,18 @@ export class MemoryPressureMonitor extends EventEmitter {
const chat = client.getChat();
const history = chat.getHistoryShallow?.() ?? chat.getHistory();
const settings = this.coreConfig.getClearContextOnIdle();
const result = microcompactHistory(history, Date.now() - 1, {
...settings,
toolResultsThresholdMinutes:
(settings.toolResultsThresholdMinutes ?? 0) < 0
? settings.toolResultsThresholdMinutes
: 0,
});
const result = microcompactHistory(
history,
Date.now() - 1,
{
...settings,
toolResultsThresholdMinutes:
(settings.toolResultsThresholdMinutes ?? 0) < 0
? settings.toolResultsThresholdMinutes
: 0,
},
{ projectRoot: this.coreConfig.getTargetDir() },
);
if (result.meta) {
chat.setHistory(result.history);
// Explicitly clear fileReadCache here instead of relying on
Expand Down
130 changes: 130 additions & 0 deletions packages/core/src/services/microcompaction/microcompact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
*/

import { describe, expect, it, afterEach } from 'vitest';
import path from 'node:path';
import type { Content } from '@google/genai';
import type { ClearContextOnIdleSettings } from '../../config/config.js';
import {
getAutoMemoryRoot,
getUserAutoMemoryRoot,
} from '../../memory/paths.js';

import {
evaluateTimeBasedTrigger,
Expand Down Expand Up @@ -1269,6 +1274,131 @@ describe('microcompactHistory evictedReadPaths (issue #4239)', () => {
});
});

describe('microcompactHistory managed memory reads (issue #6713)', () => {
const projectRoot = '/project';
const managedMemoryPath = path.join(
getAutoMemoryRoot(projectRoot),
'user',
'preferences.md',
);

function fileCall(id: string, filePath: string): Content {
return {
role: 'model',
parts: [
{
functionCall: {
id,
name: 'read_file',
args: { file_path: filePath },
},
},
],
};
}

function fileResult(id: string, output: string): Content {
return {
role: 'user',
parts: [
{ functionResponse: { id, name: 'read_file', response: { output } } },
],
};
}

it('keeps managed memory reads during idle compaction', () => {
const history: Content[] = [
fileCall('memory', managedMemoryPath),
fileResult('memory', 'managed memory content'),
fileCall('ordinary', '/project/old.ts'),
fileResult('ordinary', 'ordinary content '.repeat(50)),
fileCall('recent', '/project/recent.ts'),
fileResult('recent', 'recent content'),
];

const result = microcompactHistory(
history,
Date.now() - 2 * 60 * 60 * 1000,
{
toolResultsThresholdMinutes: 5,
toolResultsNumToKeep: 1,
},
{ projectRoot },
);

expect(
result.history[1]?.parts?.[0]?.functionResponse?.response?.['output'],
).toBe('managed memory content');
expect(
result.history[3]?.parts?.[0]?.functionResponse?.response?.['output'],
).toBe(MICROCOMPACT_CLEARED_MESSAGE);
expect(result.meta?.toolsCleared).toBe(1);
});

it('keeps managed memory reads during size compaction', () => {
const history: Content[] = [
fileCall('memory', managedMemoryPath),
fileResult('memory', 'managed memory content '.repeat(50)),
fileCall('ordinary', '/project/old.ts'),
fileResult('ordinary', 'ordinary content '.repeat(50)),
fileCall('recent', '/project/recent.ts'),
fileResult('recent', 'recent content'),
];

const result = microcompactHistory(
history,
Date.now(),
{
toolResultsThresholdMinutes: 60,
toolResultsNumToKeep: 1,
toolResultsTotalCharsThreshold: 20,
},
{ sizeOnly: true, projectRoot },
);

expect(
result.history[1]?.parts?.[0]?.functionResponse?.response?.['output'],
).toBe('managed memory content '.repeat(50));
expect(
result.history[3]?.parts?.[0]?.functionResponse?.response?.['output'],
).toBe(MICROCOMPACT_CLEARED_MESSAGE);
expect(result.meta?.toolsCleared).toBe(1);
});

it('keeps user-level managed memory reads during idle compaction', () => {
const userMemoryPath = path.join(
getUserAutoMemoryRoot(),
'user',
'preferences.md',
);
const history: Content[] = [
fileCall('memory', userMemoryPath),
fileResult('memory', 'user memory content'),
fileCall('ordinary', '/project/old.ts'),
fileResult('ordinary', 'ordinary content '.repeat(50)),
fileCall('recent', '/project/recent.ts'),
fileResult('recent', 'recent content'),
];

const result = microcompactHistory(
history,
Date.now() - 2 * 60 * 60 * 1000,
{
toolResultsThresholdMinutes: 5,
toolResultsNumToKeep: 1,
},
{ projectRoot },
);

expect(
result.history[1]?.parts?.[0]?.functionResponse?.response?.['output'],
).toBe('user memory content');
expect(
result.history[3]?.parts?.[0]?.functionResponse?.response?.['output'],
).toBe(MICROCOMPACT_CLEARED_MESSAGE);
});
});

describe('microcompactHistory — force option', () => {
afterEach(clearEnv);

Expand Down
35 changes: 31 additions & 4 deletions packages/core/src/services/microcompaction/microcompact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Content, Part } from '@google/genai';

import type { ClearContextOnIdleSettings } from '../../config/config.js';
import { DEFAULT_TOOL_RESULTS_TOTAL_CHARS_THRESHOLD } from '../../config/clearContextDefaults.js';
import { isAnyAutoMemPath } from '../../memory/paths.js';
import { sanitizeMimeForPlaceholder } from '../compactionInputSlimming.js';
import { ToolNames } from '../../tools/tool-names.js';

Expand Down Expand Up @@ -149,17 +150,37 @@ function hasNestedMedia(part: Part): boolean {
* `toolResultsNumToKeep: 1` keeps 1 tool result AND 1 media item, not
* 1 entry total across the combined list.
*/
function collectCompactablePartRefs(history: Content[]): CollectedRefs {
function isManagedMemoryRead(
part: Part,
callIdToFilePath: Map<string, string[]>,
projectRoot: string | undefined,
): boolean {
if (!projectRoot || part.functionResponse?.name !== ToolNames.READ_FILE) {
return false;
}
const paths = getFilePathsForResponse(part, callIdToFilePath);
return paths?.length === 1 && isAnyAutoMemPath(paths[0]!, projectRoot);
}

function collectCompactablePartRefs(
history: Content[],
projectRoot?: string,
): CollectedRefs {
const tool: PartRef[] = [];
const media: PartRef[] = [];
const nestedMedia: PartRef[] = [];
const callIdToFilePath = buildCallIdToFilePath(history);
for (let ci = 0; ci < history.length; ci++) {
const content = history[ci]!;
if (content.role !== 'user' || !content.parts) continue;
for (let pi = 0; pi < content.parts.length; pi++) {
const part = content.parts[pi]!;
const fnName = part.functionResponse?.name;
if (fnName && COMPACTABLE_TOOLS.has(fnName)) {
if (
fnName &&
COMPACTABLE_TOOLS.has(fnName) &&
!isManagedMemoryRead(part, callIdToFilePath, projectRoot)
) {
tool.push({ contentIndex: ci, partIndex: pi, kind: 'tool' });
} else if (part.functionResponse && hasNestedMedia(part)) {
// Non-compactable tool result with media attached — clear only
Expand Down Expand Up @@ -347,6 +368,7 @@ function planSizeBasedClearing(
settings: ClearContextOnIdleSettings,
keepRecent: number,
pendingContent: Content | Content[] | undefined,
projectRoot?: string,
): SizeClearPlan | null {
const threshold = getToolResultsTotalCharsThreshold(settings);
if (!Number.isFinite(threshold) || threshold < 0) {
Expand All @@ -356,7 +378,7 @@ function planSizeBasedClearing(
const pending = normalizePendingContent(pendingContent);
const virtualHistory =
pending.length > 0 ? [...history, ...pending] : history;
const { tool } = collectCompactablePartRefs(virtualHistory);
const { tool } = collectCompactablePartRefs(virtualHistory, projectRoot);
const charsByRef = new Map<string, number>();
let totalChars = 0;
let pendingChars = 0;
Expand Down Expand Up @@ -412,6 +434,7 @@ export interface MicrocompactOptions {
force?: boolean;
sizeOnly?: boolean;
pendingContent?: Content | Content[];
projectRoot?: string;
}

export interface MicrocompactMeta {
Expand Down Expand Up @@ -494,7 +517,10 @@ export function microcompactHistory(
}

if (triggerReason === 'force' || triggerReason === 'idle') {
({ tool, media, nestedMedia } = collectCompactablePartRefs(history));
({ tool, media, nestedMedia } = collectCompactablePartRefs(
history,
opts?.projectRoot,
));
// Each kind gets its own keepRecent budget: setting
// `toolResultsNumToKeep: 1` keeps 1 of each, not 1 total. This
// matches what users typically expect when they configure the
Expand All @@ -514,6 +540,7 @@ export function microcompactHistory(
settings,
keepRecent,
pending,
opts?.projectRoot,
);
if (!sizePlan) {
return { history };
Expand Down
Loading