diff --git a/packages/core/src/memory/extract.test.ts b/packages/core/src/memory/extract.test.ts index 1909e6af1b6..042bd9889b3 100644 --- a/packages/core/src/memory/extract.test.ts +++ b/packages/core/src/memory/extract.test.ts @@ -59,6 +59,7 @@ describe('auto-memory extraction', () => { touchedTopics: [], touchedProjectScope: false, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); @@ -125,6 +126,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); vi.mocked(rebuildManagedAutoMemoryIndex).mockRejectedValueOnce( @@ -151,6 +153,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: true, + hasToolActivity: true, systemMessage: undefined, }); vi.mocked(rebuildManagedAutoMemoryIndex).mockResolvedValueOnce(''); @@ -180,6 +183,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user', 'project'], touchedProjectScope: true, touchedUserScope: true, + hasToolActivity: true, systemMessage: undefined, }); @@ -200,6 +204,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user'], touchedProjectScope: false, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); @@ -227,6 +232,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); @@ -284,6 +290,7 @@ describe('auto-memory extraction', () => { touchedTopics: [], touchedProjectScope: false, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); @@ -331,6 +338,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); @@ -356,6 +364,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); @@ -400,6 +409,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); @@ -489,6 +499,7 @@ describe('auto-memory extraction', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }); @@ -532,5 +543,50 @@ describe('auto-memory extraction', () => { ); expect(result.cursor.processedOffset).toBe(compressedHistory.length); }); + it('BUG #6311: should NOT advance cursor when agent makes zero tool calls (hallucination)', async () => { + vi.mocked(runAutoMemoryExtractionByAgent).mockResolvedValue({ + touchedTopics: [], + touchedProjectScope: false, + touchedUserScope: false, + hasToolActivity: false, + systemMessage: undefined, + }); + + const history = [ + { + role: 'user' as const, + parts: [{ text: 'Remember that I prefer pnpm over npm.' }], + }, + ]; + + const result = await runAutoMemoryExtract({ + projectRoot, + sessionId: 'session-1', + config: mockConfig, + history: [...history], + }); + + expect(result.cursor.processedOffset).toBe(0); + }); + it('should advance cursor on legitimate noop (agent checked memory, found nothing new)', async () => { + vi.mocked(runAutoMemoryExtractionByAgent).mockResolvedValue({ + touchedTopics: [], + touchedProjectScope: false, + touchedUserScope: false, + hasToolActivity: true, + systemMessage: undefined, + }); + + const history = [{ role: 'user' as const, parts: [{ text: 'hello' }] }]; + + const result = await runAutoMemoryExtract({ + projectRoot, + sessionId: 'session-1', + config: mockConfig, + history: [...history], + }); + + expect(result.cursor.processedOffset).toBe(1); + }); }); }); diff --git a/packages/core/src/memory/extract.ts b/packages/core/src/memory/extract.ts index a42db33dd2b..403e7c411f6 100644 --- a/packages/core/src/memory/extract.ts +++ b/packages/core/src/memory/extract.ts @@ -197,9 +197,12 @@ export async function runAutoMemoryExtract(params: { await Promise.all([projectRebuild, userRebuild]); } + const madeGenuineProgress = + agentResult.touchedTopics.length > 0 || agentResult.hasToolActivity; + const cursor: AutoMemoryExtractCursor = { sessionId: params.sessionId, - processedOffset: params.history.length, + processedOffset: madeGenuineProgress ? params.history.length : startOffset, updatedAt: now.toISOString(), }; await writeExtractCursor(params.projectRoot, cursor); diff --git a/packages/core/src/memory/extractAgent.test.ts b/packages/core/src/memory/extractAgent.test.ts index d0903bb3443..5786fd349da 100644 --- a/packages/core/src/memory/extractAgent.test.ts +++ b/packages/core/src/memory/extractAgent.test.ts @@ -67,6 +67,7 @@ describe('auto-memory extraction with agent planner', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: 'Managed auto-memory updated: user.md', }; }); diff --git a/packages/core/src/memory/extractionAgentPlanner.test.ts b/packages/core/src/memory/extractionAgentPlanner.test.ts index 37924cd76d0..93ef2df6855 100644 --- a/packages/core/src/memory/extractionAgentPlanner.test.ts +++ b/packages/core/src/memory/extractionAgentPlanner.test.ts @@ -76,6 +76,7 @@ describe('runAutoMemoryExtractionByAgent', () => { status: 'completed', finalText: '', filesTouched: ['/tmp/auto-memory/user/prefs.md'], + filesWritten: ['/tmp/auto-memory/user/prefs.md'], }); const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp'); @@ -84,6 +85,7 @@ describe('runAutoMemoryExtractionByAgent', () => { touchedTopics: ['user'], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: 'Managed auto-memory updated: user.md', }); expect(runForkedAgent).toHaveBeenCalledWith( @@ -108,6 +110,7 @@ describe('runAutoMemoryExtractionByAgent', () => { status: 'completed', finalText: '', filesTouched: [], + filesWritten: [], }); const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp'); @@ -115,6 +118,7 @@ describe('runAutoMemoryExtractionByAgent', () => { touchedTopics: [], touchedProjectScope: false, touchedUserScope: false, + hasToolActivity: false, systemMessage: undefined, }); }); @@ -176,6 +180,11 @@ describe('runAutoMemoryExtractionByAgent', () => { '/tmp/auto-memory/reference/api.md', '/tmp/some/other/file.ts', ], + filesWritten: [ + '/tmp/auto-memory/project/arch.md', + '/tmp/auto-memory/reference/api.md', + '/tmp/some/other/file.ts', + ], }); const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp'); @@ -195,6 +204,10 @@ describe('runAutoMemoryExtractionByAgent', () => { '/tmp/user-memory/user/role.md', '/tmp/user-memory/feedback/terse.md', ], + filesWritten: [ + '/tmp/user-memory/user/role.md', + '/tmp/user-memory/feedback/terse.md', + ], }); const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp'); @@ -230,6 +243,10 @@ describe('runAutoMemoryExtractionByAgent', () => { 'C:/Users/foo/.qwen/projects/proj/memory/project/release.md', 'C:/Users/foo/.qwen/memories/user/role.md', ], + filesWritten: [ + 'C:/Users/foo/.qwen/projects/proj/memory/project/release.md', + 'C:/Users/foo/.qwen/memories/user/role.md', + ], }); try { @@ -256,6 +273,10 @@ describe('runAutoMemoryExtractionByAgent', () => { '/tmp/auto-memory\\project\\arch.md', '/tmp/user-memory\\user\\role.md', ], + filesWritten: [ + '/tmp/auto-memory\\project\\arch.md', + '/tmp/user-memory\\user\\role.md', + ], }); const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp'); @@ -294,6 +315,10 @@ describe('runAutoMemoryExtractionByAgent', () => { '/tmp/user-memory/user/role.md', '/tmp/auto-memory/project/release.md', ], + filesWritten: [ + '/tmp/user-memory/user/role.md', + '/tmp/auto-memory/project/release.md', + ], }); const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp'); diff --git a/packages/core/src/memory/extractionAgentPlanner.ts b/packages/core/src/memory/extractionAgentPlanner.ts index f1c4da1b362..c821b56a465 100644 --- a/packages/core/src/memory/extractionAgentPlanner.ts +++ b/packages/core/src/memory/extractionAgentPlanner.ts @@ -61,6 +61,7 @@ export interface AutoMemoryExtractionExecutionResult { /** True when at least one file inside the user-level memory root was written/edited. */ touchedUserScope: boolean; systemMessage?: string; + hasToolActivity: boolean; } /** @@ -291,12 +292,13 @@ export async function runAutoMemoryExtractionByAgent( } const { topics, touchedProjectScope, touchedUserScope } = - touchedTopicsFromFilePaths(result.filesTouched, projectRoot); + touchedTopicsFromFilePaths(result.filesWritten ?? [], projectRoot); return { touchedTopics: topics, touchedProjectScope, touchedUserScope, + hasToolActivity: result.filesTouched.length > 0, systemMessage: topics.length > 0 ? `Managed auto-memory updated: ${topics.map((t) => `${t}.md`).join(', ')}` diff --git a/packages/core/src/memory/memoryLifecycle.integration.test.ts b/packages/core/src/memory/memoryLifecycle.integration.test.ts index 5151e08e500..6318306ab68 100644 --- a/packages/core/src/memory/memoryLifecycle.integration.test.ts +++ b/packages/core/src/memory/memoryLifecycle.integration.test.ts @@ -82,6 +82,7 @@ describe('managed auto-memory lifecycle integration', () => { touchedTopics: [topic], touchedProjectScope: true, touchedUserScope: false, + hasToolActivity: true, systemMessage: undefined, }; },