Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .gemini/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"experimental": {
"extensionReloading": true,
"modelSteering": true,
"memoryManager": true
"autoMemory": true
},
"general": {
"devtools": true
Expand Down
26 changes: 13 additions & 13 deletions docs/cli/settings.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1759,13 +1759,14 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.memoryV2`** (boolean):
- **Description:** Disable the built-in save_memory tool and let the main
agent persist project context by editing markdown files directly with
edit/write_file. Routes facts across four tiers: team-shared conventions go
edit/write_file. Route facts across four tiers: team-shared conventions go
to project GEMINI.md files, project-specific personal notes go to the
per-project private memory folder (MEMORY.md as index + sibling .md files
for detail), and cross-project personal preferences go to the global
~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit
— settings, credentials, etc. remain off-limits).
- **Default:** `false`
— settings, credentials, etc. remain off-limits). Set to false to fall back
to the legacy save_memory tool.
- **Default:** `true`
- **Requires restart:** Yes

- **`experimental.autoMemory`** (boolean):
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2280,9 +2280,9 @@ const SETTINGS_SCHEMA = {
label: 'Memory v2',
category: 'Experimental',
requiresRestart: true,
default: false,
default: true,
description:
'Disable the built-in save_memory tool and let the main agent persist project context by editing markdown files directly with edit/write_file. Routes facts across four tiers: team-shared conventions go to project GEMINI.md files, project-specific personal notes go to the per-project private memory folder (MEMORY.md as index + sibling .md files for detail), and cross-project personal preferences go to the global ~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit — settings, credentials, etc. remain off-limits).',
'Disable the built-in save_memory tool and let the main agent persist project context by editing markdown files directly with edit/write_file. Route facts across four tiers: team-shared conventions go to project GEMINI.md files, project-specific personal notes go to the per-project private memory folder (MEMORY.md as index + sibling .md files for detail), and cross-project personal preferences go to the global ~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit — settings, credentials, etc. remain off-limits). Set to false to fall back to the legacy save_memory tool.',
showInDialog: true,
},
autoMemory: {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/nonInteractiveCliAgentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export async function runNonInteractive({
const { setupInitialActivityLogger } = await import(
'./utils/devtoolsService.js'
);
await setupInitialActivityLogger(config);
setupInitialActivityLogger(config);
}

const { stdout: workingStdout } = createWorkingStdio();
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3501,13 +3501,27 @@ describe('Config JIT Initialization', () => {
});

describe('isMemoryV2Enabled', () => {
it('should default to false', () => {
it('should default to true', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
};

config = new Config(params);
expect(config.isMemoryV2Enabled()).toBe(true);
});

it('should return false when experimentalMemoryV2 is explicitly false', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
experimentalMemoryV2: false,
};

config = new Config(params);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,7 +1172,7 @@ export class Config implements McpContext, AgentLoopContext {
);

this.experimentalJitContext = params.experimentalJitContext ?? true;
this.experimentalMemoryV2 = params.experimentalMemoryV2 ?? false;
this.experimentalMemoryV2 = params.experimentalMemoryV2 ?? true;
this.experimentalAutoMemory = params.experimentalAutoMemory ?? false;
this.experimentalContextManagementConfig =
params.experimentalContextManagementConfig;
Expand Down
82 changes: 61 additions & 21 deletions packages/core/src/services/chatRecordingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export async function loadConversationRecord(
): Promise<
| (ConversationRecord & {
messageCount?: number;
userMessageCount?: number;
firstUserMessage?: string;
hasUserOrAssistantMessage?: boolean;
})
Expand All @@ -128,8 +129,11 @@ export async function loadConversationRecord(
let metadata: Partial<ConversationRecord> = {};
const messagesMap = new Map<string, MessageRecord>();
const messageIds: string[] = [];
const messageKinds = new Map<
string,
{ isUser: boolean; isUserOrAssistant: boolean }
>();
let firstUserMessageStr: string | undefined;
let hasUserOrAssistant = false;

for await (const line of rl) {
if (!line.trim()) continue;
Expand All @@ -140,13 +144,14 @@ export async function loadConversationRecord(
if (options?.metadataOnly) {
const idx = messageIds.indexOf(rewindId);
if (idx !== -1) {
messageIds.splice(idx);
const removedIds = messageIds.splice(idx);
for (const removedId of removedIds) {
messageKinds.delete(removedId);
}
} else {
messageIds.length = 0;
messageKinds.clear();
}
// For metadataOnly we can't perfectly un-track hasUserOrAssistant if it was rewinded,
// but we can assume false if messageIds is empty.
if (messageIds.length === 0) hasUserOrAssistant = false;
} else {
let found = false;
const idsToDelete: string[] = [];
Expand All @@ -164,20 +169,18 @@ export async function loadConversationRecord(
}
} else if (isMessageRecord(record)) {
const id = record.id;
if (
const isUser = hasProperty(record, 'type') && record.type === 'user';
const isUserOrAssistant =
hasProperty(record, 'type') &&
(record.type === 'user' || record.type === 'gemini')
) {
hasUserOrAssistant = true;
}
(record.type === 'user' || record.type === 'gemini');
// Track message count and first user message
if (options?.metadataOnly) {
messageIds.push(id);
messageKinds.set(id, { isUser, isUserOrAssistant });
}
if (
!firstUserMessageStr &&
hasProperty(record, 'type') &&
record['type'] === 'user' &&
isUser &&
hasProperty(record, 'content') &&
record['content']
) {
Expand Down Expand Up @@ -221,6 +224,33 @@ export async function loadConversationRecord(
return await parseLegacyRecordFallback(filePath, options);
}

const metadataMessages = Array.isArray(metadata.messages)
? metadata.messages
: [];
const loadedMessages =
metadataMessages.length > 0
? metadataMessages
: Array.from(messagesMap.values());
const metadataFirstUserMessage =
metadataMessages.find((message) => message.type === 'user') ?? null;
let fallbackFirstUserMessage = firstUserMessageStr;
if (!fallbackFirstUserMessage && metadataFirstUserMessage) {
const rawContent = metadataFirstUserMessage.content;
if (Array.isArray(rawContent)) {
fallbackFirstUserMessage = rawContent
.map((part: unknown) => (isTextPart(part) ? part['text'] : ''))
.join('');
} else if (typeof rawContent === 'string') {
fallbackFirstUserMessage = rawContent;
}
}
const userMessageCount = options?.metadataOnly
? Array.from(messageKinds.values()).filter((m) => m.isUser).length
: loadedMessages.filter((m) => m.type === 'user').length;
const hasUserOrAssistant = options?.metadataOnly
? Array.from(messageKinds.values()).some((m) => m.isUserOrAssistant)
: loadedMessages.some((m) => m.type === 'user' || m.type === 'gemini');

return {
sessionId: metadata.sessionId,
projectHash: metadata.projectHash,
Expand All @@ -229,16 +259,21 @@ export async function loadConversationRecord(
summary: metadata.summary,
directories: metadata.directories,
kind: metadata.kind,
messages: Array.from(messagesMap.values()),
messages: options?.metadataOnly ? [] : loadedMessages,
messageCount: options?.metadataOnly
? messageIds.length
: messagesMap.size,
firstUserMessage: firstUserMessageStr,
hasUserOrAssistantMessage: options?.metadataOnly
? hasUserOrAssistant
: Array.from(messagesMap.values()).some(
(m) => m.type === 'user' || m.type === 'gemini',
),
? metadataMessages.length || messageIds.length
: loadedMessages.length,
userMessageCount:
options?.metadataOnly && metadataMessages.length > 0
? metadataMessages.filter((m) => m.type === 'user').length
: userMessageCount,
firstUserMessage: fallbackFirstUserMessage,
hasUserOrAssistantMessage:
options?.metadataOnly && metadataMessages.length > 0
? metadataMessages.some(
(m) => m.type === 'user' || m.type === 'gemini',
)
: hasUserOrAssistant,
};
} catch (error) {
debugLogger.error('Error loading conversation record from JSONL:', error);
Expand Down Expand Up @@ -816,6 +851,7 @@ async function parseLegacyRecordFallback(
): Promise<
| (ConversationRecord & {
messageCount?: number;
userMessageCount?: number;
firstUserMessage?: string;
hasUserOrAssistantMessage?: boolean;
})
Expand Down Expand Up @@ -849,6 +885,8 @@ async function parseLegacyRecordFallback(
...legacyRecord,
messages: [],
messageCount: legacyRecord.messages?.length || 0,
userMessageCount:
legacyRecord.messages?.filter((m) => m.type === 'user').length || 0,
firstUserMessage: fallbackFirstUserMessageStr,
hasUserOrAssistantMessage:
legacyRecord.messages?.some(
Expand All @@ -858,6 +896,8 @@ async function parseLegacyRecordFallback(
}
return {
...legacyRecord,
userMessageCount:
legacyRecord.messages?.filter((m) => m.type === 'user').length || 0,
hasUserOrAssistantMessage:
legacyRecord.messages?.some(
(m) => m.type === 'user' || m.type === 'gemini',
Expand Down
Loading
Loading