fix(ai): keep global memory always enabled

Remove the proactive-memory preference and its cross-platform runtime plumbing so memory behavior has one clear contract. Normalize away the obsolete setting and keep source type limited to provenance.
This commit is contained in:
digua committed 2026-08-24 22:35:47 +08:00
1 parent 77c4dec9e6
commit c69a927615
21 files changed
+10 -74

No files matched your search

-2
View File
@@ -86,7 +86,6 @@ export function createCliRunAgentStream(
crossChatAnalysisService?: CrossChatAnalysisService crossChatAnalysisService?: CrossChatAnalysisService
sessionAdapter?: SessionRuntimeAdapter sessionAdapter?: SessionRuntimeAdapter
memoryService?: AIMemoryService memoryService?: AIMemoryService
getAllowProactiveMemory?: () => boolean
} = {} } = {}
): (params: AgentStreamRequest, onEvent: (chunk: AgentStreamChunk) => void, abortSignal: AbortSignal) => Promise<void> { ): (params: AgentStreamRequest, onEvent: (chunk: AgentStreamChunk) => void, abortSignal: AbortSignal) => Promise<void> {
const aiDataDir = getAiDir(dbManager) const aiDataDir = getAiDir(dbManager)
@@ -161,7 +160,6 @@ export function createCliRunAgentStream(
maxToolResultTokens: resolveCrossChatToolResultTokenBudget(contextWindow), maxToolResultTokens: resolveCrossChatToolResultTokenBudget(contextWindow),
memoryService: options.memoryService, memoryService: options.memoryService,
aiChatId, aiChatId,
allowProactiveMemory: options.getAllowProactiveMemory?.() ?? true,
}) })
await runCrossChatAgent({ await runCrossChatAgent({
userMessage, userMessage,
@@ -23,7 +23,6 @@ export function createCliCrossChatTools(options: {
maxToolResultTokens: number maxToolResultTokens: number
memoryService: AIMemoryService memoryService: AIMemoryService
aiChatId: string aiChatId: string
allowProactiveMemory: boolean
}): AgentTool<any, any>[] { }): AgentTool<any, any>[] {
const context: Omit<CrossChatToolExecutionContext, 'abortSignal' | 'reportProgress'> = { const context: Omit<CrossChatToolExecutionContext, 'abortSignal' | 'reportProgress'> = {
locale: options.locale, locale: options.locale,
@@ -31,7 +30,6 @@ export function createCliCrossChatTools(options: {
analysisService: options.analysisService, analysisService: options.analysisService,
memoryService: options.memoryService, memoryService: options.memoryService,
aiChatId: options.aiChatId, aiChatId: options.aiChatId,
allowProactiveMemory: options.allowProactiveMemory,
maxToolResultTokens: options.maxToolResultTokens, maxToolResultTokens: options.maxToolResultTokens,
countTokens, countTokens,
preprocessMessagesBySession: (sessionId, messages) => preprocessMessagesBySession: (sessionId, messages) =>
-1
View File
@@ -233,7 +233,6 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
crossChatAnalysisService, crossChatAnalysisService,
sessionAdapter, sessionAdapter,
memoryService: aiMemoryService, memoryService: aiMemoryService,
getAllowProactiveMemory: () => preferencesManager.load().aiGlobalSettings.allowProactiveMemory,
}), }),
}, },
}) })
@@ -79,7 +79,6 @@ export function createElectronRunAgentStream(
analysisService: CrossChatAnalysisService analysisService: CrossChatAnalysisService
sessionAdapter: SessionRuntimeAdapter sessionAdapter: SessionRuntimeAdapter
memoryService: AIMemoryService memoryService: AIMemoryService
getAllowProactiveMemory: () => boolean
} }
): ( ): (
params: AgentStreamRequest, params: AgentStreamRequest,
@@ -156,7 +155,6 @@ export function createElectronRunAgentStream(
maxToolResultTokens: resolveCrossChatToolResultTokenBudget(resolvedContextWindow), maxToolResultTokens: resolveCrossChatToolResultTokenBudget(resolvedContextWindow),
memoryService: crossChatOptions.memoryService, memoryService: crossChatOptions.memoryService,
aiChatId, aiChatId,
allowProactiveMemory: crossChatOptions.getAllowProactiveMemory(),
}) })
await runCrossChatAgent({ await runCrossChatAgent({
userMessage, userMessage,
@@ -23,7 +23,6 @@ export function createElectronCrossChatTools(options: {
maxToolResultTokens: number maxToolResultTokens: number
memoryService: AIMemoryService memoryService: AIMemoryService
aiChatId: string aiChatId: string
allowProactiveMemory: boolean
}): AgentTool<any, any>[] { }): AgentTool<any, any>[] {
const context: Omit<CrossChatToolExecutionContext, 'abortSignal' | 'reportProgress'> = { const context: Omit<CrossChatToolExecutionContext, 'abortSignal' | 'reportProgress'> = {
locale: options.locale, locale: options.locale,
@@ -31,7 +30,6 @@ export function createElectronCrossChatTools(options: {
analysisService: options.analysisService, analysisService: options.analysisService,
memoryService: options.memoryService, memoryService: options.memoryService,
aiChatId: options.aiChatId, aiChatId: options.aiChatId,
allowProactiveMemory: options.allowProactiveMemory,
maxToolResultTokens: options.maxToolResultTokens, maxToolResultTokens: options.maxToolResultTokens,
countTokens, countTokens,
preprocessMessagesBySession: (sessionId, messages) => preprocessMessagesBySession: (sessionId, messages) =>
-1
View File
@@ -229,7 +229,6 @@ export async function startInternalServer(
analysisService: crossChatAnalysisService, analysisService: crossChatAnalysisService,
sessionAdapter, sessionAdapter,
memoryService: newAIMemoryService, memoryService: newAIMemoryService,
getAllowProactiveMemory: () => preferencesManager.load().aiGlobalSettings.allowProactiveMemory,
}), }),
executeAiTool: createExecuteElectronAiTool(newSemanticIndexService ?? undefined), executeAiTool: createExecuteElectronAiTool(newSemanticIndexService ?? undefined),
} }
@@ -195,7 +195,7 @@ test('loads thinkingLevels as empty object when field is absent in legacy prefer
// Write a legacy preferences.json that has no thinkingLevels field // Write a legacy preferences.json that has no thinkingLevels field
writeFileSync( writeFileSync(
join(systemDir, 'preferences.json'), join(systemDir, 'preferences.json'),
JSON.stringify({ aiGlobalSettings: { maxMessagesPerRequest: 500 } }) JSON.stringify({ aiGlobalSettings: { maxMessagesPerRequest: 500, allowProactiveMemory: false } })
) )
const loaded = new PreferencesManager(systemDir).load() const loaded = new PreferencesManager(systemDir).load()
@@ -204,7 +204,7 @@ test('loads thinkingLevels as empty object when field is absent in legacy prefer
// Existing fields must survive the migration // Existing fields must survive the migration
assert.equal(loaded.aiGlobalSettings.maxMessagesPerRequest, 500) assert.equal(loaded.aiGlobalSettings.maxMessagesPerRequest, 500)
assert.equal(loaded.aiGlobalSettings.chartAutoMode, 'suggest') assert.equal(loaded.aiGlobalSettings.chartAutoMode, 'suggest')
assert.equal(loaded.aiGlobalSettings.allowProactiveMemory, true) assert.equal('allowProactiveMemory' in loaded.aiGlobalSettings, false)
} finally { } finally {
rmSync(systemDir, { recursive: true, force: true }) rmSync(systemDir, { recursive: true, force: true })
} }
+3 -4
View File
@@ -49,7 +49,6 @@ const DEFAULTS: Preferences = {
sqlExportFormat: 'csv', sqlExportFormat: 'csv',
enableAutoSkill: true, enableAutoSkill: true,
chartAutoMode: 'suggest', chartAutoMode: 'suggest',
allowProactiveMemory: true,
searchContextBefore: 2, searchContextBefore: 2,
searchContextAfter: 2, searchContextAfter: 2,
}, },
@@ -152,10 +151,10 @@ export class PreferencesManager {
private normalizeAiGlobalSettings(partial: Partial<AIGlobalSettings> | undefined): AIGlobalSettings { private normalizeAiGlobalSettings(partial: Partial<AIGlobalSettings> | undefined): AIGlobalSettings {
const normalized = { ...(partial ?? {}) } as Record<string, unknown> const normalized = { ...(partial ?? {}) } as Record<string, unknown>
// Context compression became an automatic runtime capability. Ignore the // Ignore removed settings so legacy values cannot survive normalization
// legacy user preference so a previously disabled value cannot survive // and accidentally re-enable obsolete behavior after a later save.
// after the setting is removed.
delete normalized.contextCompression delete normalized.contextCompression
delete normalized.allowProactiveMemory
return { ...DEFAULTS.aiGlobalSettings, ...(normalized as Partial<AIGlobalSettings>) } return { ...DEFAULTS.aiGlobalSettings, ...(normalized as Partial<AIGlobalSettings>) }
} }
-1
View File
@@ -295,7 +295,6 @@ export interface AIGlobalSettings {
sqlExportFormat: 'csv' | 'json' sqlExportFormat: 'csv' | 'json'
enableAutoSkill: boolean enableAutoSkill: boolean
chartAutoMode: ChartAutoMode chartAutoMode: ChartAutoMode
allowProactiveMemory: boolean
searchContextBefore: number searchContextBefore: number
searchContextAfter: number searchContextAfter: number
} }
-2
View File
@@ -64,7 +64,6 @@ test('the shared Desktop and CLI Web adapter exposes and executes all thirteen c
analysisService, analysisService,
memoryService: {} as Parameters<typeof createCrossChatAgentToolAdapters>[0]['memoryService'], memoryService: {} as Parameters<typeof createCrossChatAgentToolAdapters>[0]['memoryService'],
aiChatId: 'global-chat-test', aiChatId: 'global-chat-test',
allowProactiveMemory: true,
maxToolResultTokens: 16_000, maxToolResultTokens: 16_000,
preprocessMessagesBySession: async (_sessionId, messages) => messages, preprocessMessagesBySession: async (_sessionId, messages) => messages,
preprocessSummariesBySession: async (_sessionId, summaries) => summaries, preprocessSummariesBySession: async (_sessionId, summaries) => summaries,
@@ -128,7 +127,6 @@ test('cross-chat tool adapters share entities resolved during the current Agent
analysisService, analysisService,
memoryService, memoryService,
aiChatId: 'global-chat-test', aiChatId: 'global-chat-test',
allowProactiveMemory: true,
maxToolResultTokens: 16_000, maxToolResultTokens: 16_000,
preprocessMessagesBySession: async (_sessionId, messages) => messages, preprocessMessagesBySession: async (_sessionId, messages) => messages,
preprocessSummariesBySession: async (_sessionId, summaries) => summaries, preprocessSummariesBySession: async (_sessionId, summaries) => summaries,
@@ -499,7 +499,6 @@ function createContext(overrides: Partial<CrossChatAnalysisToolService> = {}): C
analysisService: service, analysisService: service,
memoryService: {} as CrossChatToolExecutionContext['memoryService'], memoryService: {} as CrossChatToolExecutionContext['memoryService'],
aiChatId: 'global-chat-test', aiChatId: 'global-chat-test',
allowProactiveMemory: true,
preprocessMessagesBySession: (_sessionId, messages) => preprocessMessagesBySession: (_sessionId, messages) =>
messages.map((message) => ({ ...message, senderName: 'U1', content: '[redacted]' })), messages.map((message) => ({ ...message, senderName: 'U1', content: '[redacted]' })),
preprocessSummariesBySession: (_sessionId, summaries) => summaries, preprocessSummariesBySession: (_sessionId, summaries) => summaries,
@@ -101,7 +101,7 @@ function entityResolution(refs: AIEntityRef[]): CrossChatEntityResolution {
} }
function createContext( function createContext(
options: { allowProactiveMemory?: boolean; entityRefs?: AIEntityRef[]; resolvedEntityRefs?: AIEntityRef[] } = {} options: { entityRefs?: AIEntityRef[]; resolvedEntityRefs?: AIEntityRef[] } = {}
): CrossChatToolExecutionContext { ): CrossChatToolExecutionContext {
const analysisService = { const analysisService = {
resolveEntities: async (refs: AIEntityRef[]) => entityResolution(refs), resolveEntities: async (refs: AIEntityRef[]) => entityResolution(refs),
@@ -113,7 +113,6 @@ function createContext(
analysisService, analysisService,
memoryService: new FakeMemoryService(), memoryService: new FakeMemoryService(),
aiChatId: 'global-chat-1', aiChatId: 'global-chat-1',
allowProactiveMemory: options.allowProactiveMemory ?? true,
preprocessMessagesBySession: (_sessionId, messages) => messages, preprocessMessagesBySession: (_sessionId, messages) => messages,
preprocessSummariesBySession: (_sessionId, summaries) => summaries, preprocessSummariesBySession: (_sessionId, summaries) => summaries,
preprocessModelLabel: (value) => value, preprocessModelLabel: (value) => value,
@@ -185,19 +184,13 @@ describe('global memory tools', () => {
) )
}) })
it('blocks proactive AI writes when disabled but keeps explicit user writes available', async () => { it('writes durable AI-derived memories', async () => {
const context = createContext({ allowProactiveMemory: false }) const context = createContext()
await assert.rejects(
async () =>
memoryWriteTool.handler({ scope_type: 'global', content: 'AI inference', source_type: 'ai' }, context),
/disabled/i
)
const result = await memoryWriteTool.handler( const result = await memoryWriteTool.handler(
{ scope_type: 'global', content: '用户明确要求记住', source_type: 'user' }, { scope_type: 'global', content: 'AI 根据聊天证据形成的稳定结论', source_type: 'ai' },
context context
) )
assert.equal((result.data as AIMemoryEntry).sourceType, 'user') assert.equal((result.data as AIMemoryEntry).sourceType, 'ai')
}) })
it('reads and writes self memory on demand without an entity id', async () => { it('reads and writes self memory on demand without an entity id', async () => {
@@ -95,9 +95,6 @@ async function memoryWriteHandler(
await assertResolvableEntityScope(scope, context) await assertResolvableEntityScope(scope, context)
const content = requireString(params.content, 'content') const content = requireString(params.content, 'content')
const sourceType = parseSourceType(params.source_type) const sourceType = parseSourceType(params.source_type)
if (sourceType === 'ai' && !context.allowProactiveMemory) {
throw new Error('Proactive AI memory is disabled; only explicit user memory writes are allowed')
}
const id = optionalString(params.id) const id = optionalString(params.id)
let entry: AIMemoryEntry let entry: AIMemoryEntry
-1
View File
@@ -433,7 +433,6 @@ export interface CrossChatToolExecutionContext {
analysisService: CrossChatAnalysisToolService analysisService: CrossChatAnalysisToolService
memoryService: AIMemoryToolService memoryService: AIMemoryToolService
aiChatId: string aiChatId: string
allowProactiveMemory: boolean
preprocessMessagesBySession: ( preprocessMessagesBySession: (
sessionId: string, sessionId: string,
messages: CrossChatMessageSource[] messages: CrossChatMessageSource[]
@@ -1,11 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { AIMemoryEntry } from '@/services/ai/types' import type { AIMemoryEntry } from '@/services/ai/types'
import { useAIService, useDataService } from '@/services' import { useAIService, useDataService } from '@/services'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { usePromptStore } from '@/stores/prompt'
import { useSessionStore } from '@/stores/session' import { useSessionStore } from '@/stores/session'
type MemoryView = 'preferences' | 'self' | 'entities' type MemoryView = 'preferences' | 'self' | 'entities'
@@ -17,9 +15,7 @@ const { t, locale } = useI18n()
const toast = useToast() const toast = useToast()
const aiService = useAIService() const aiService = useAIService()
const dataService = useDataService() const dataService = useDataService()
const promptStore = usePromptStore()
const sessionStore = useSessionStore() const sessionStore = useSessionStore()
const { aiGlobalSettings } = storeToRefs(promptStore)
const activeView = ref<MemoryView>('preferences') const activeView = ref<MemoryView>('preferences')
const entries = ref<AIMemoryEntry[]>([]) const entries = ref<AIMemoryEntry[]>([])
@@ -43,11 +39,6 @@ const visibleEntries = computed(() =>
: entry.scopeType === 'contact' || entry.scopeType === 'group' : entry.scopeType === 'contact' || entry.scopeType === 'group'
) )
) )
const proactiveMemory = computed({
get: () => aiGlobalSettings.value.allowProactiveMemory ?? true,
set: (value: boolean) => promptStore.updateAIGlobalSettings({ allowProactiveMemory: value }),
})
function close(): void { function close(): void {
emit('update:open', false) emit('update:open', false)
} }
@@ -206,16 +197,6 @@ watch(
</header> </header>
<div class="flex min-h-0 flex-1 flex-col px-5 py-4"> <div class="flex min-h-0 flex-1 flex-col px-5 py-4">
<div class="mb-4 flex items-center justify-between rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-900/60">
<div class="min-w-0 pr-4">
<p class="text-sm text-gray-900 dark:text-gray-100">{{ t('ai.global.memory.proactive.title') }}</p>
<p class="mt-0.5 text-xs leading-5 text-gray-500 dark:text-gray-400">
{{ t('ai.global.memory.proactive.description') }}
</p>
</div>
<USwitch v-model="proactiveMemory" />
</div>
<div class="mb-3 flex items-center justify-between gap-3"> <div class="mb-3 flex items-center justify-between gap-3">
<UTabs v-model="activeView" :items="viewItems" :content="false" size="sm" class="min-w-max" /> <UTabs v-model="activeView" :items="viewItems" :content="false" size="sm" class="min-w-max" />
<span class="text-xs text-gray-400"> <span class="text-xs text-gray-400">
-4
View File
@@ -288,10 +288,6 @@
"entities": "Contacts and groups", "entities": "Contacts and groups",
"count": "{count} items", "count": "{count} items",
"empty": "No memories yet. You can ask AI to remember something in a conversation.", "empty": "No memories yet. You can ask AI to remember something in a conversation.",
"proactive": {
"title": "Allow proactive AI memory",
"description": "Let AI save long-term preferences, personal context, and relationship insights. Explicit requests to remember still work when this is off."
},
"source": { "source": {
"user": "User confirmed", "user": "User confirmed",
"ai": "AI generated" "ai": "AI generated"
-4
View File
@@ -288,10 +288,6 @@
"entities": "連絡先とグループ", "entities": "連絡先とグループ",
"count": "{count} 件", "count": "{count} 件",
"empty": "メモリはまだありません。会話で AI に覚えるよう依頼できます。", "empty": "メモリはまだありません。会話で AI に覚えるよう依頼できます。",
"proactive": {
"title": "AI による自発的な記憶を許可",
"description": "長期的に役立つ設定、本人の背景、関係性の知見を AI が保存できます。オフでも、明示的に記憶を依頼した内容は保存されます。"
},
"source": { "source": {
"user": "ユーザー確認済み", "user": "ユーザー確認済み",
"ai": "AI が生成" "ai": "AI が生成"
-4
View File
@@ -288,10 +288,6 @@
"entities": "联系人和群聊", "entities": "联系人和群聊",
"count": "{count} 条", "count": "{count} 条",
"empty": "暂无记忆,你可以在对话中让 AI 记住。", "empty": "暂无记忆,你可以在对话中让 AI 记住。",
"proactive": {
"title": "允许 AI 主动记忆",
"description": "允许 AI 保存有长期价值的偏好、个人背景和关系结论;关闭后仍会保存你明确要求记住的内容。"
},
"source": { "source": {
"user": "用户确认", "user": "用户确认",
"ai": "AI 形成" "ai": "AI 形成"
-4
View File
@@ -288,10 +288,6 @@
"entities": "聯絡人和群聊", "entities": "聯絡人和群聊",
"count": "{count} 條", "count": "{count} 條",
"empty": "暫無記憶,你可以在對話中讓 AI 記住。", "empty": "暫無記憶,你可以在對話中讓 AI 記住。",
"proactive": {
"title": "允許 AI 主動記憶",
"description": "允許 AI 儲存有長期價值的偏好、個人背景與關係結論;關閉後仍會儲存你明確要求記住的內容。"
},
"source": { "source": {
"user": "使用者確認", "user": "使用者確認",
"ai": "AI 形成" "ai": "AI 形成"
-2
View File
@@ -10,7 +10,6 @@ interface AIGlobalSettings {
sqlExportFormat: 'csv' | 'json' sqlExportFormat: 'csv' | 'json'
enableAutoSkill: boolean enableAutoSkill: boolean
chartAutoMode: ChartAutoMode chartAutoMode: ChartAutoMode
allowProactiveMemory: boolean
searchContextBefore: number searchContextBefore: number
searchContextAfter: number searchContextAfter: number
} }
@@ -30,7 +29,6 @@ export const usePromptStore = defineStore(
sqlExportFormat: 'csv', sqlExportFormat: 'csv',
enableAutoSkill: true, enableAutoSkill: true,
chartAutoMode: 'suggest', chartAutoMode: 'suggest',
allowProactiveMemory: true,
searchContextBefore: 2, searchContextBefore: 2,
searchContextAfter: 2, searchContextAfter: 2,
}) })
-1
View File
@@ -59,7 +59,6 @@ it('recalls a durable preference in a new global AI conversation before querying
analysisService, analysisService,
memoryService, memoryService,
aiChatId, aiChatId,
allowProactiveMemory: true,
preprocessMessagesBySession: (_sessionId, messages) => messages, preprocessMessagesBySession: (_sessionId, messages) => messages,
preprocessSummariesBySession: (_sessionId, summaries) => summaries, preprocessSummariesBySession: (_sessionId, summaries) => summaries,
preprocessModelLabel: (value) => value, preprocessModelLabel: (value) => value,