diff --git a/src/feishu/event-handler.ts b/src/feishu/event-handler.ts index 134d3ba2..01341394 100644 --- a/src/feishu/event-handler.ts +++ b/src/feishu/event-handler.ts @@ -1276,7 +1276,7 @@ async function executeClaudeTask( // 使用 repo identity(而非带随机后缀的工作区路径)确保同仓库记忆互通 const repoIdentity = getRepoIdentity(workingDir); const memoryContext = config.memory.enabled - ? await injectMemories(rawPrompt, { agentId, userId, workspaceDir: repoIdentity }) + ? await injectMemories(rawPrompt, { agentId, userId, workspaceDir: repoIdentity, chatId }) : ''; const result = await claudeExecutor.execute({ @@ -1627,7 +1627,7 @@ async function executeDirectTask( // 记忆注入(使用 repo identity 确保同仓库记忆互通) const repoIdentity = getRepoIdentity(workingDir); const memoryContext = config.memory.enabled - ? await injectMemories(rawPrompt, { agentId, userId, workspaceDir: repoIdentity }) + ? await injectMemories(rawPrompt, { agentId, userId, workspaceDir: repoIdentity, chatId }) : ''; const result = await claudeExecutor.execute({ diff --git a/src/memory/__tests__/injector.test.ts b/src/memory/__tests__/injector.test.ts index 9a754ee3..5d290f34 100644 --- a/src/memory/__tests__/injector.test.ts +++ b/src/memory/__tests__/injector.test.ts @@ -140,7 +140,7 @@ describe('formatMemories', () => { ]; const output = formatMemories(results); - expect(output).toContain('since 2026-02-15'); + expect(output).toContain('(2026-02-15'); }); it('should include date for decisions', () => { @@ -165,6 +165,32 @@ describe('formatMemories', () => { expect(output).toContain('confidence: low'); }); + it('should tag memories from other chats', () => { + const results = [ + makeResult({ type: 'fact', content: '使用 PostgreSQL' }), + ]; + // memory.chatId defaults to 'chat1', pass a different currentChatId + const output = formatMemories(results, 'chat_other'); + expect(output).toContain('来自其他会话'); + }); + + it('should not tag memories from current chat', () => { + const results = [ + makeResult({ type: 'fact', content: '使用 PostgreSQL' }), + ]; + // memory.chatId defaults to 'chat1', pass same as currentChatId + const output = formatMemories(results, 'chat1'); + expect(output).not.toContain('来自其他会话'); + }); + + it('should not tag memories when no currentChatId provided', () => { + const results = [ + makeResult({ type: 'fact', content: '使用 PostgreSQL' }), + ]; + const output = formatMemories(results); + expect(output).not.toContain('来自其他会话'); + }); + it('should not tag high-confidence memories', () => { const results = [ makeResult({ type: 'preference', content: '确定用 React', confidence: 0.9 }), diff --git a/src/memory/injector.ts b/src/memory/injector.ts index e8604688..414963a0 100644 --- a/src/memory/injector.ts +++ b/src/memory/injector.ts @@ -12,6 +12,7 @@ export interface InjectionContext { agentId: string; userId?: string; workspaceDir?: string; + chatId?: string; } /** Type display names */ @@ -54,7 +55,7 @@ export async function injectMemories( return ''; } - const fragment = formatMemories(results); + const fragment = formatMemories(results, context.chatId); logger.info( { agentId: context.agentId, userId: context.userId, count: results.length, chars: fragment.length }, 'Memories injected into system prompt', @@ -68,8 +69,9 @@ export async function injectMemories( /** * Format search results into a prompt fragment, respecting maxInjectTokens. + * @param currentChatId - When provided, memories are annotated with source context (current vs other chat). */ -export function formatMemories(results: MemorySearchResult[]): string { +export function formatMemories(results: MemorySearchResult[], currentChatId?: string): string { const maxChars = config.memory.maxInjectTokens * 3; // Group by type @@ -97,20 +99,34 @@ export function formatMemories(results: MemorySearchResult[]): string { const itemLines: string[] = []; for (const r of group) { const mem = r.memory; - let line: string; + + // Date tag: for decisions use createdAt (when the decision was made); + // for other types prefer validAt (when the fact became true) + const dateStr = (type === 'decision' + ? (mem.createdAt || mem.validAt || '') + : (mem.validAt || mem.createdAt || '') + ).split('T')[0]; + + // Source tag: annotate whether this memory is from the current chat or another + const fromOtherChat = currentChatId && mem.chatId && mem.chatId !== currentChatId; + + // Build annotation parts + const annotations: string[] = []; if (type === 'state' && mem.ttl) { - line = `- ${mem.content} (预计到 ${mem.ttl.split('T')[0]})`; - } else if (type === 'fact' && mem.validAt) { - line = `- ${mem.content} (since ${mem.validAt.split('T')[0]})`; - } else if (type === 'decision' && mem.createdAt) { - const reason = mem.supersedeReason ? `, 原因: ${mem.supersedeReason}` : ''; - line = `- ${mem.content} (${mem.createdAt.split('T')[0]}${reason})`; - } else { - const confidenceTag = mem.confidence < 0.6 ? ' (confidence: low)' : ''; - line = `- ${mem.content}${confidenceTag}`; + annotations.push(`预计到 ${mem.ttl.split('T')[0]}`); + } else if (type === 'decision' && mem.supersedeReason) { + annotations.push(dateStr, `原因: ${mem.supersedeReason}`); + } else if (dateStr) { + annotations.push(dateStr); } + if (mem.confidence < 0.6) annotations.push('confidence: low'); + if (fromOtherChat) annotations.push('来自其他会话'); + + const tag = annotations.length > 0 ? ` (${annotations.join(', ')})` : ''; + const line = `- ${mem.content}${tag}`; + if (totalChars + header.length + 1 + line.length + 1 > maxChars) { budgetExhausted = true; break;