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
4 changes: 2 additions & 2 deletions src/feishu/event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
28 changes: 27 additions & 1 deletion src/memory/__tests__/injector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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 }),
Expand Down
40 changes: 28 additions & 12 deletions src/memory/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface InjectionContext {
agentId: string;
userId?: string;
workspaceDir?: string;
chatId?: string;
}

/** Type display names */
Expand Down Expand Up @@ -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',
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Loading