From 521b5a4f95d4f24f7abee53be70021d9c1d086cd Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Mon, 22 Jun 2026 11:03:24 +0800 Subject: [PATCH 1/3] fix(memory): harden memory followups --- docs/issues/pr1794-memory-hardening/plan.md | 38 ++ docs/issues/pr1794-memory-hardening/spec.md | 41 ++ docs/issues/pr1794-memory-hardening/tasks.md | 16 + .../presenter/agentRuntimePresenter/index.ts | 5 - .../databaseSecurityPresenter/index.ts | 87 ++++ .../presenter/memoryPresenter/extraction.ts | 22 +- src/main/presenter/memoryPresenter/index.ts | 66 ++- src/main/presenter/sqlitePresenter/index.ts | 11 +- .../sqlitePresenter/tables/agentMemory.ts | 141 +++++-- .../tables/deepchatTapeSearchProjection.ts | 74 +++- .../settings/components/MemoryConfigPanel.vue | 394 ++++++++++-------- .../components/MemoryManagerPanel.vue | 69 ++- .../settings/components/MemorySettings.vue | 37 +- src/renderer/src/i18n/da-DK/settings.json | 15 +- src/renderer/src/i18n/de-DE/settings.json | 15 +- src/renderer/src/i18n/en-US/settings.json | 15 +- src/renderer/src/i18n/es-ES/settings.json | 15 +- src/renderer/src/i18n/fa-IR/settings.json | 15 +- src/renderer/src/i18n/fr-FR/settings.json | 15 +- src/renderer/src/i18n/he-IL/settings.json | 15 +- src/renderer/src/i18n/id-ID/settings.json | 15 +- src/renderer/src/i18n/it-IT/settings.json | 15 +- src/renderer/src/i18n/ja-JP/settings.json | 15 +- src/renderer/src/i18n/ko-KR/settings.json | 15 +- src/renderer/src/i18n/ms-MY/settings.json | 15 +- src/renderer/src/i18n/pl-PL/settings.json | 15 +- src/renderer/src/i18n/pt-BR/settings.json | 15 +- src/renderer/src/i18n/ru-RU/settings.json | 15 +- src/renderer/src/i18n/tr-TR/settings.json | 15 +- src/renderer/src/i18n/vi-VN/settings.json | 15 +- src/renderer/src/i18n/zh-CN/settings.json | 15 +- src/renderer/src/i18n/zh-HK/settings.json | 15 +- src/renderer/src/i18n/zh-TW/settings.json | 15 +- test/main/presenter/memoryExtraction.test.ts | 43 +- 34 files changed, 1024 insertions(+), 320 deletions(-) create mode 100644 docs/issues/pr1794-memory-hardening/plan.md create mode 100644 docs/issues/pr1794-memory-hardening/spec.md create mode 100644 docs/issues/pr1794-memory-hardening/tasks.md diff --git a/docs/issues/pr1794-memory-hardening/plan.md b/docs/issues/pr1794-memory-hardening/plan.md new file mode 100644 index 000000000..39a4389cd --- /dev/null +++ b/docs/issues/pr1794-memory-hardening/plan.md @@ -0,0 +1,38 @@ +# Plan + +## Core memory runtime + +1. Change extraction parsing to return a discriminated result so parse errors can propagate as `ok:false` from `extractAndStore()`. +2. Filter memory extraction spans to user-visible text only; exclude assistant reasoning fields. +3. Add a current-embedding fingerprint guard and/or queue wait before reindex reset to prevent stale drains from writing old vectors. +4. Gate `restoreMemory()` with `canWriteAgentMemory()` and delete vectors when rows are archived/forgotten. +5. Update memory FTS query construction to tokenize multi-word queries while retaining safe quoting. + +## Database and FTS + +1. Extend database security copy to preserve non-shadow indexes and triggers, or run a schema repair/rebuild pass after copy. +2. Add FTS meta/version/tokenizer tracking to agent memory FTS and rebuild when capability differs. +3. Make tape search FTS replacement/deletion stable by using a deterministic rowid or external-content style rebuild/delete path. +4. Make clear/reset paths tolerate missing FTS/meta tables. +5. Seed schema version for newly created databases or otherwise avoid running historical migrations against freshly-created latest schemas. + +## Renderer UI + +1. Add top-level MemorySettings error/finally handling. +2. Add request-id guards to MemoryManagerPanel refresh. +3. Surface search errors distinctly from empty results. +4. Rename default destructive memory operations to archive/restore semantics; keep permanent delete as an explicit dangerous action if still exposed. +5. Move/label advanced retrieval tuning to reduce accidental misuse. + +## Tests + +- Main tests for extraction parse failure cursor behavior and reasoning exclusion. +- Main tests for reindex/drain stale guard and restore disabled behavior. +- SQLite tests for FTS tokenizer rebuild and stale token replacement. +- Database security migration tests for indexes/triggers if existing harness supports it. +- Renderer tests for MemorySettings error state and stale refresh/search behavior. + +## Compatibility + +- Existing rows remain valid. FTS rebuilds are derived from canonical SQLite rows/projection. +- Archived rows remain available for restore; sidecar vectors are treated as cache and can be regenerated. diff --git a/docs/issues/pr1794-memory-hardening/spec.md b/docs/issues/pr1794-memory-hardening/spec.md new file mode 100644 index 000000000..6e1d2735d --- /dev/null +++ b/docs/issues/pr1794-memory-hardening/spec.md @@ -0,0 +1,41 @@ +# PR1794 Memory Hardening + +## User need + +PR #1794 adds a broad long-term memory system. Before merge, the implementation must avoid silent memory loss, memory contamination, data/index corruption, confusing destructive UI behavior, and obvious reliability regressions. + +## Goal + +Fix the must-fix and medium-high priority review findings for PR #1794 while preserving the overall tape-native memory design. + +## Acceptance criteria + +- Memory extraction does not advance session memory cursors when the LLM extraction output cannot be parsed as a valid result. +- Assistant reasoning/internal thinking fields are excluded from memory extraction spans. +- Embedding reindex/reset cannot be raced by an older in-flight embedding drain writing stale vectors. +- Disabling memory consistently prevents restore/write paths from scheduling new embeddings, unless explicitly documented as read-only management. +- Archive/forget removes stale vectors from the sidecar or otherwise compacts them so archived rows do not bloat vector recall. +- Memory keyword search supports tokenized multi-word queries, not only exact phrase queries. +- SQLCipher/encryption copy preserves or rebuilds indexes/triggers required by memory/tape tables. +- FTS indexes have a version/tokenizer rebuild path so runtime capability changes do not leave stale FTS schemas. +- Tape search FTS replacement/deletion does not leave stale tokens for replaced entries. +- Memory settings UI recovers from load errors, avoids stale refresh overwrites, and makes archive vs permanent delete semantics clear. +- Relevant unit tests cover the fixed behaviors. + +## Constraints + +- Keep changes focused on PR #1794 memory hardening; avoid unrelated refactors. +- Preserve backward compatibility with existing local SQLite databases. +- Do not introduce new runtime dependencies. +- Main-process DB operations remain synchronous where existing SQLite presenter patterns require it. +- UI strings must use i18n keys. + +## Non-goals + +- Redesigning the entire memory architecture. +- Changing the public PR feature scope beyond hardening and safety fixes. +- Shipping a complete vector compaction scheduler if immediate vector deletion on archive is sufficient. + +## Open questions + +None. diff --git a/docs/issues/pr1794-memory-hardening/tasks.md b/docs/issues/pr1794-memory-hardening/tasks.md new file mode 100644 index 000000000..84af6b236 --- /dev/null +++ b/docs/issues/pr1794-memory-hardening/tasks.md @@ -0,0 +1,16 @@ +# Tasks + +- [x] Core: make extraction parse failures retryable. +- [x] Core: exclude assistant reasoning from extraction spans. +- [x] Core: prevent stale embedding drains during reindex/reset. +- [x] Core: gate restore and clean vector sidecar on archive/forget. +- [x] Core: improve memory FTS tokenized search. +- [x] DB: preserve/rebuild indexes and triggers during encrypted copy. +- [x] DB: version/rebuild agent memory FTS tokenizer. +- [x] DB: stabilize tape search FTS replacement/deletion. +- [x] DB: harden clear/reset and new DB schema version initialization. +- [x] UI: add MemorySettings error handling. +- [x] UI: guard MemoryManagerPanel refresh and search errors. +- [x] UI: clarify archive vs permanent delete and advanced settings. +- [x] Tests: update memory extraction tests for `parseMemoryCandidates` union return. +- [ ] Validation: `pnpm run format`, `pnpm run i18n`, `pnpm run lint`, and `pnpm run typecheck` passed under Node v26 warning; `pnpm test` was stopped per user instruction after memoryPresenter failures surfaced. diff --git a/src/main/presenter/agentRuntimePresenter/index.ts b/src/main/presenter/agentRuntimePresenter/index.ts index 3d49d6ab2..bc84d1ef0 100644 --- a/src/main/presenter/agentRuntimePresenter/index.ts +++ b/src/main/presenter/agentRuntimePresenter/index.ts @@ -1992,13 +1992,8 @@ export class AgentRuntimePresenter implements IAgentImplementation { const b = block as { type?: string content?: unknown - reasoning_content?: unknown - text?: unknown } if (b?.type === 'content' && typeof b.content === 'string') return b.content - if (b?.type === 'reasoning_content' && typeof b.content === 'string') return b.content - if (typeof b?.reasoning_content === 'string') return b.reasoning_content - if (b?.type === 'reasoning' && typeof b.text === 'string') return b.text return '' }) .filter(Boolean) diff --git a/src/main/presenter/databaseSecurityPresenter/index.ts b/src/main/presenter/databaseSecurityPresenter/index.ts index 576018761..adcb9a35f 100644 --- a/src/main/presenter/databaseSecurityPresenter/index.ts +++ b/src/main/presenter/databaseSecurityPresenter/index.ts @@ -63,6 +63,22 @@ type SqliteSchemaRow = { const quoteIdentifier = (value: string): string => `"${value.replace(/"/g, '""')}"` const getMigrationLockPath = (dbPath: string): string => path.resolve(dbPath) +const FTS_SCHEMA_OBJECT_NAME_PATTERN = /(^|_)fts($|_)/i +const FTS_TRIGGER_NAME_PATTERN = /_(ai|ad|au)$/i + +function isFtsMaintenanceSchemaObject(row: SqliteSchemaRow & { tbl_name?: string }): boolean { + const sql = row.sql.toLowerCase() + return ( + FTS_SCHEMA_OBJECT_NAME_PATTERN.test(row.name) || + (row.tbl_name ? FTS_SCHEMA_OBJECT_NAME_PATTERN.test(row.tbl_name) : false) || + /\b[a-z0-9_]+_fts\b/i.test(row.sql) || + /\busing\s+fts[345]?\b/i.test(row.sql) || + (row.type === 'trigger' && + FTS_TRIGGER_NAME_PATTERN.test(row.name) && + sql.includes('insert into')) + ) +} + export class DatabaseSecurityPresenter { private readonly store: ElectronStore<{ metadata: DatabaseSecurityMetadata }> private readonly dbPath: string @@ -361,6 +377,7 @@ export class DatabaseSecurityPresenter { ) } + this.copySchemaObjects(db) this.copySqliteSequence(db) db.exec('COMMIT') } catch (error) { @@ -369,6 +386,54 @@ export class DatabaseSecurityPresenter { } } + private copySchemaObjects(db: Database.Database): void { + for (const object of this.listMigratableSchemaObjects(db)) { + db.exec(this.qualifyCreateSchemaObjectSql(object)) + } + } + + private listMigratableSchemaObjects(db: Database.Database): SqliteSchemaRow[] { + const virtualTableNames = new Set( + ( + db + .prepare( + `SELECT name, sql FROM sqlite_master + WHERE type = 'table' + AND sql IS NOT NULL + AND name NOT LIKE 'sqlite_%'` + ) + .all() as SqliteSchemaRow[] + ) + .filter((row) => /^CREATE\s+VIRTUAL\s+TABLE\s+/i.test(row.sql)) + .map((row) => row.name) + ) + const rows = db + .prepare( + `SELECT type, name, tbl_name, sql FROM sqlite_master + WHERE type IN ('index', 'trigger', 'view') + AND sql IS NOT NULL + AND name NOT LIKE 'sqlite_%' + ORDER BY CASE type WHEN 'index' THEN 0 WHEN 'trigger' THEN 1 ELSE 2 END, name ASC` + ) + .all() as Array + return rows.filter((row) => { + if (shouldExcludeFromSqliteCopy(row.name)) return false + if (row.tbl_name && shouldExcludeFromSqliteCopy(row.tbl_name)) return false + if (isFtsMaintenanceSchemaObject(row)) return false + for (const virtualTableName of virtualTableNames) { + if ( + row.name === virtualTableName || + row.name.startsWith(`${virtualTableName}_`) || + row.tbl_name === virtualTableName || + row.sql.includes(virtualTableName) + ) { + return false + } + } + return true + }) + } + private listMigratableTables(db: Database.Database): SqliteSchemaRow[] { const rows = db .prepare( @@ -404,6 +469,28 @@ export class DatabaseSecurityPresenter { ) } + private qualifyCreateSchemaObjectSql(row: SqliteSchemaRow): string { + if (row.type === 'index') { + return row.sql.replace( + /^CREATE\s+(UNIQUE\s+)?INDEX\s+(IF\s+NOT\s+EXISTS\s+)?/i, + (_match, unique: string | undefined, ifNotExists: string | undefined) => + `CREATE ${unique ?? ''}INDEX ${ifNotExists ?? ''}${MIGRATION_TARGET_SCHEMA}.` + ) + } + if (row.type === 'trigger') { + return row.sql.replace( + /^CREATE\s+(TEMP\s+|TEMPORARY\s+)?TRIGGER\s+(IF\s+NOT\s+EXISTS\s+)?/i, + (_match, temp: string | undefined, ifNotExists: string | undefined) => + `CREATE ${temp ?? ''}TRIGGER ${ifNotExists ?? ''}${MIGRATION_TARGET_SCHEMA}.` + ) + } + return row.sql.replace( + /^CREATE\s+(TEMP\s+|TEMPORARY\s+)?VIEW\s+(IF\s+NOT\s+EXISTS\s+)?/i, + (_match, temp: string | undefined, ifNotExists: string | undefined) => + `CREATE ${temp ?? ''}VIEW ${ifNotExists ?? ''}${MIGRATION_TARGET_SCHEMA}.` + ) + } + private copySqliteSequence(db: Database.Database): void { const sourceSequence = db .prepare("SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'") diff --git a/src/main/presenter/memoryPresenter/extraction.ts b/src/main/presenter/memoryPresenter/extraction.ts index 4c86839a6..0e45cc1f0 100644 --- a/src/main/presenter/memoryPresenter/extraction.ts +++ b/src/main/presenter/memoryPresenter/extraction.ts @@ -53,19 +53,27 @@ export function buildExtractionPrompt(spanText: string): string { ].join('\n') } -// Tolerant parse: code fences, surrounding noise, and missing fields all degrade to []. -export function parseMemoryCandidates(raw: string): MemoryCandidate[] { - if (!raw) return [] +export type MemoryCandidateParseResult = + | { ok: true; candidates: MemoryCandidate[] } + | { + ok: false + reason: 'empty-response' | 'missing-json-array' | 'invalid-json' | 'non-array' + } + +// Tolerant per-entry parse: surrounding noise and malformed entries are ignored, but malformed +// top-level model output is reported so callers can retry instead of advancing durable cursors. +export function parseMemoryCandidates(raw: string): MemoryCandidateParseResult { + if (typeof raw !== 'string' || !raw.trim()) return { ok: false, reason: 'empty-response' } const jsonText = extractJsonArray(raw) - if (!jsonText) return [] + if (!jsonText) return { ok: false, reason: 'missing-json-array' } let parsed: unknown try { parsed = JSON.parse(jsonText) } catch { - return [] + return { ok: false, reason: 'invalid-json' } } - if (!Array.isArray(parsed)) return [] + if (!Array.isArray(parsed)) return { ok: false, reason: 'non-array' } const candidates: MemoryCandidate[] = [] for (const entry of parsed) { @@ -78,7 +86,7 @@ export function parseMemoryCandidates(raw: string): MemoryCandidate[] { candidates.push({ kind, content, importance }) if (candidates.length >= MAX_CANDIDATES) break } - return candidates + return { ok: true, candidates } } function clampImportance(value: unknown): number { diff --git a/src/main/presenter/memoryPresenter/index.ts b/src/main/presenter/memoryPresenter/index.ts index 673cda20a..c31777b90 100644 --- a/src/main/presenter/memoryPresenter/index.ts +++ b/src/main/presenter/memoryPresenter/index.ts @@ -477,6 +477,15 @@ export class MemoryPresenter implements MemoryRuntimePort { this.isPendingEmbeddableRow(agentId, this.deps.repository.getById(record.memoryId)) ) if (!live.length) return { written: new Set(), usable: true } + const currentEmbedding = this.deps.resolveAgentConfig(agentId)?.memoryEmbedding + if ( + !currentEmbedding?.providerId || + !currentEmbedding?.modelId || + embeddingFingerprint(currentEmbedding.providerId, currentEmbedding.modelId) !== + embeddingFingerprint(embedding.providerId, embedding.modelId) + ) { + return { written: new Set(), usable: true } + } const store = await this.openVectorStoreLocked( agentId, { providerId: embedding.providerId, modelId: embedding.modelId }, @@ -492,6 +501,17 @@ export class MemoryPresenter implements MemoryRuntimePort { if (!this.canContinueAgentMemoryTask(agentId)) return const fingerprint = embeddingFingerprint(embedding.providerId, embedding.modelId) + const currentEmbedding = this.deps.resolveAgentConfig(agentId)?.memoryEmbedding + const currentFingerprint = + currentEmbedding?.providerId && currentEmbedding?.modelId + ? embeddingFingerprint(currentEmbedding.providerId, currentEmbedding.modelId) + : null + if (currentFingerprint !== fingerprint) { + logger.info( + `[Memory] embedding config changed during drain for ${agentId}; discarding stale vectors` + ) + return + } for (const record of records) { if (outcome.written.has(record.memoryId)) { this.deps.repository.updatePendingEmbeddingStatus(agentId, record.memoryId, 'embedded', { @@ -541,6 +561,11 @@ export class MemoryPresenter implements MemoryRuntimePort { // `force` rebuilds an unusable on-disk store even with nothing to re-queue (the foreign file is // itself what blocks recovery); otherwise skip the reset when there is no stale work. if (!requeued && !force) return + // Wait for a drain that captured the previous embedding config before resetting the sidecar, + // otherwise stale vectors can be written into the freshly reset store. + const inFlightDrain = this.embeddingDrains.get(agentId) + if (inFlightDrain) await inFlightDrain.catch(() => undefined) + if (!this.canContinueAgentMemoryTask(agentId)) return // Drop the stale-dimension store under the per-agent lock; the next embed rebuilds it. await this.runExclusiveForAgent(agentId, async () => { if (!this.canContinueAgentMemoryTask(agentId)) return @@ -627,7 +652,12 @@ export class MemoryPresenter implements MemoryRuntimePort { ) // Teardown may have begun during the extraction await; stop before any candidate processing. if (!this.canWriteAgentMemory(input.agentId)) return { ok: true, createdIds: [] } - const candidates = parseMemoryCandidates(response) + const parsed = parseMemoryCandidates(response) + if (!parsed.ok) { + logger.warn(`[Memory] extraction parse failed: ${parsed.reason}`) + return { ok: false } + } + const candidates = parsed.candidates const options: WriteMemoriesOptions = { agentId: input.agentId, sourceSession: input.sourceSession ?? null, @@ -1136,7 +1166,7 @@ export class MemoryPresenter implements MemoryRuntimePort { restoreMemory(agentId: string, memoryId: string): boolean { if (this.disposed) return false this.assertSafeAgentId(agentId) - if (!this.isManagedAgent(agentId)) return false + if (!this.canWriteAgentMemory(agentId)) return false const row = this.deps.repository.getById(memoryId) if (!row || row.agent_id !== agentId || row.status !== 'archived') return false this.deps.repository.updateStatus(memoryId, 'pending_embedding') @@ -1151,11 +1181,13 @@ export class MemoryPresenter implements MemoryRuntimePort { async forgetMemory(agentId: string, memoryId: string): Promise { if (this.disposed) return false this.assertSafeAgentId(agentId) - if (!this.isManagedAgent(agentId)) return false + if (!this.canWriteAgentMemory(agentId)) return false const row = this.deps.repository.getById(memoryId) if (!row || row.agent_id !== agentId) return false if (row.status === 'archived') return true this.deps.repository.archive(row.id, Date.now()) + await this.deleteVectorsForMemoryIds(agentId, [memoryId]) + if (this.disposed) return true this.syncWorkingMemoryAfterMutation(agentId) this.emitChanged(agentId, 'extract') return true @@ -2051,18 +2083,7 @@ export class MemoryPresenter implements MemoryRuntimePort { if (row.kind !== 'working') { this.syncWorkingMemoryAfterMutation(agentId) } - // Run the vector delete under the per-agent lock so dispose() awaits it (via vectorStoreLocks) - // before closing the sidecar — otherwise a teardown landing mid-DELETE could close the DuckDB - // connection while the statement runs. If teardown already began, skip it: the authoritative row - // is gone and an orphaned vector is harmless (recall skips matches whose SQLite row is missing). - await this.runExclusiveForAgent(agentId, async () => { - if (this.disposed) return - const store = await this.vectorStoreForAgent(agentId) - if (!store) return - await store.deleteByMemoryIds([memoryId]).catch((error) => { - logger.warn(`[Memory] vector delete failed: ${String(error)}`) - }) - }) + await this.deleteVectorsForMemoryIds(agentId, [memoryId]) if (this.disposed) return true this.emitChanged(agentId, 'delete') return true @@ -2119,6 +2140,21 @@ export class MemoryPresenter implements MemoryRuntimePort { if (resetError) throw resetError } + private async deleteVectorsForMemoryIds(agentId: string, memoryIds: string[]): Promise { + if (!memoryIds.length) return + // Run vector deletes under the per-agent lock so dispose() awaits them (via vectorStoreLocks) + // before closing the sidecar. If teardown already began, skip it: SQLite status is authoritative + // and recall ignores rows that are archived/deleted. + await this.runExclusiveForAgent(agentId, async () => { + if (this.disposed) return + const store = await this.vectorStoreForAgent(agentId) + if (!store) return + await store.deleteByMemoryIds(memoryIds).catch((error) => { + logger.warn(`[Memory] vector delete failed: ${String(error)}`) + }) + }) + } + private async settleDeletedAgentInFlight(agentId: string): Promise { const reindexing = this.reindexing.get(agentId) const backfilling = this.backfilling.get(agentId) diff --git a/src/main/presenter/sqlitePresenter/index.ts b/src/main/presenter/sqlitePresenter/index.ts index bfbfc61c9..85d690c88 100644 --- a/src/main/presenter/sqlitePresenter/index.ts +++ b/src/main/presenter/sqlitePresenter/index.ts @@ -240,6 +240,7 @@ export class SQLitePresenter implements ISQLitePresenter { private dbPath: string private password?: string private destructiveInitializationRetryCount = 0 + private databaseFileExistedBeforeOpen = false constructor(dbPath: string, password?: string) { this.dbPath = dbPath @@ -302,6 +303,7 @@ export class SQLitePresenter implements ISQLitePresenter { } private initializeDatabase(): void { + this.databaseFileExistedBeforeOpen = fs.existsSync(this.dbPath) this.db = openSQLiteDatabase(this.dbPath, this.password) this.db.prepare('SELECT 1').get() this.initTables() @@ -496,6 +498,14 @@ export class SQLitePresenter implements ISQLitePresenter { return Math.max(maxVersion, tableMaxVersion) }, 0) + if (!this.databaseFileExistedBeforeOpen && this.currentVersion === 0 && latestVersion > 0) { + this.db + .prepare('INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)') + .run(latestVersion, Date.now()) + this.currentVersion = latestVersion + return + } + // 只迁移未执行的版本 tables.forEach((table) => { for (let version = this.currentVersion + 1; version <= latestVersion; version++) { @@ -575,7 +585,6 @@ export class SQLitePresenter implements ISQLitePresenter { DELETE FROM deepchat_tape_entries; DELETE FROM deepchat_tape_search_projection; DELETE FROM deepchat_tape_search_projection_meta; - DELETE FROM deepchat_tape_search_fts_meta; DELETE FROM deepchat_sessions; DELETE FROM new_session_active_skills; DELETE FROM new_session_disabled_agent_tools; diff --git a/src/main/presenter/sqlitePresenter/tables/agentMemory.ts b/src/main/presenter/sqlitePresenter/tables/agentMemory.ts index 43ad8e09b..5609d7e74 100644 --- a/src/main/presenter/sqlitePresenter/tables/agentMemory.ts +++ b/src/main/presenter/sqlitePresenter/tables/agentMemory.ts @@ -78,6 +78,9 @@ export interface AgentMemoryListOptions { // persona lifecycle column; v35 adds conflict linkage. const AGENT_MEMORY_SCHEMA_VERSION = 35 +const AGENT_MEMORY_FTS_META_KEY = 'agent_memory_fts' +const AGENT_MEMORY_FTS_META_VERSION = 1 + type FtsCapability = { available: boolean; tokenizer: 'trigram' | 'unicode61' } const AGENT_MEMORY_INDEX_SQL = ` @@ -90,6 +93,14 @@ const AGENT_MEMORY_INDEX_SQL = ` WHERE provenance_key IS NOT NULL; ` +function tokenizeSearchQuery(query: string): string[] { + return query + .trim() + .split(/\s+/u) + .map((term) => term.trim()) + .filter(Boolean) +} + function escapeLikePattern(value: string): string { return value.replace(/[\\%_]/g, (character) => `\\${character}`) } @@ -210,6 +221,34 @@ export class AgentMemoryTable extends BaseTable { return !!row } + private readFtsMeta(): { schema_version: number; tokenizer: string } | undefined { + return this.db + .prepare('SELECT schema_version, tokenizer FROM agent_memory_fts_meta WHERE key = ?') + .get(AGENT_MEMORY_FTS_META_KEY) as { schema_version: number; tokenizer: string } | undefined + } + + private writeFtsMeta(tokenizer: string): void { + this.db + .prepare( + `INSERT INTO agent_memory_fts_meta (key, schema_version, tokenizer, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + schema_version = excluded.schema_version, + tokenizer = excluded.tokenizer, + updated_at = excluded.updated_at` + ) + .run(AGENT_MEMORY_FTS_META_KEY, AGENT_MEMORY_FTS_META_VERSION, tokenizer, Date.now()) + } + + private dropFtsIndex(): void { + this.db.exec(` + DROP TRIGGER IF EXISTS agent_memory_fts_ai; + DROP TRIGGER IF EXISTS agent_memory_fts_ad; + DROP TRIGGER IF EXISTS agent_memory_fts_au; + DROP TABLE IF EXISTS agent_memory_fts; + `) + } + // Creates the external-content FTS5 mirror of agent_memory and the triggers that keep it in // sync, then backfills existing rows the first time it is built. Idempotent and a no-op when // FTS5 is unavailable (search falls back to LIKE). superseded rows stay in the index and are @@ -220,37 +259,63 @@ export class AgentMemoryTable extends BaseTable { this.ftsReady = false return } - const alreadyBuilt = this.ftsTableExists() - this.db.exec(` - CREATE VIRTUAL TABLE IF NOT EXISTS agent_memory_fts USING fts5( - content, - agent_id UNINDEXED, - content='agent_memory', - content_rowid='rowid', - tokenize='${capability.tokenizer}' - ); - CREATE TRIGGER IF NOT EXISTS agent_memory_fts_ai AFTER INSERT ON agent_memory BEGIN - INSERT INTO agent_memory_fts(rowid, content, agent_id) - VALUES (new.rowid, new.content, new.agent_id); - END; - CREATE TRIGGER IF NOT EXISTS agent_memory_fts_ad AFTER DELETE ON agent_memory BEGIN - INSERT INTO agent_memory_fts(agent_memory_fts, rowid, content, agent_id) - VALUES ('delete', old.rowid, old.content, old.agent_id); - END; - CREATE TRIGGER IF NOT EXISTS agent_memory_fts_au AFTER UPDATE OF content ON agent_memory BEGIN - INSERT INTO agent_memory_fts(agent_memory_fts, rowid, content, agent_id) - VALUES ('delete', old.rowid, old.content, old.agent_id); - INSERT INTO agent_memory_fts(rowid, content, agent_id) - VALUES (new.rowid, new.content, new.agent_id); - END; - `) - if (!alreadyBuilt) { - this.db.exec( - `INSERT INTO agent_memory_fts(rowid, content, agent_id) - SELECT rowid, content, agent_id FROM agent_memory;` - ) + try { + this.db.transaction(() => { + this.db.exec(` + CREATE TABLE IF NOT EXISTS agent_memory_fts_meta ( + key TEXT PRIMARY KEY, + schema_version INTEGER NOT NULL, + tokenizer TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); + `) + const meta = this.readFtsMeta() + const alreadyBuilt = this.ftsTableExists() + if ( + alreadyBuilt && + (!meta || + meta.schema_version !== AGENT_MEMORY_FTS_META_VERSION || + meta.tokenizer !== capability.tokenizer) + ) { + this.dropFtsIndex() + } + const shouldBackfill = !this.ftsTableExists() + this.db.exec(` + CREATE VIRTUAL TABLE IF NOT EXISTS agent_memory_fts USING fts5( + content, + agent_id UNINDEXED, + content='agent_memory', + content_rowid='rowid', + tokenize='${capability.tokenizer}' + ); + CREATE TRIGGER IF NOT EXISTS agent_memory_fts_ai AFTER INSERT ON agent_memory BEGIN + INSERT INTO agent_memory_fts(rowid, content, agent_id) + VALUES (new.rowid, new.content, new.agent_id); + END; + CREATE TRIGGER IF NOT EXISTS agent_memory_fts_ad AFTER DELETE ON agent_memory BEGIN + INSERT INTO agent_memory_fts(agent_memory_fts, rowid, content, agent_id) + VALUES ('delete', old.rowid, old.content, old.agent_id); + END; + CREATE TRIGGER IF NOT EXISTS agent_memory_fts_au AFTER UPDATE OF content ON agent_memory BEGIN + INSERT INTO agent_memory_fts(agent_memory_fts, rowid, content, agent_id) + VALUES ('delete', old.rowid, old.content, old.agent_id); + INSERT INTO agent_memory_fts(rowid, content, agent_id) + VALUES (new.rowid, new.content, new.agent_id); + END; + `) + if (shouldBackfill) { + this.db.exec( + `INSERT INTO agent_memory_fts(rowid, content, agent_id) + SELECT rowid, content, agent_id FROM agent_memory;` + ) + } + this.writeFtsMeta(capability.tokenizer) + })() + this.ftsReady = true + } catch { + this.dropFtsIndex() + this.ftsReady = false } - this.ftsReady = true } insert(input: AgentMemoryInsertInput): AgentMemoryRow { @@ -472,8 +537,11 @@ export class AgentMemoryTable extends BaseTable { } private searchFts(agentId: string, normalized: string, limit: number): AgentMemoryRow[] { - // Quote the whole query as a phrase so FTS5 operators in user text can never break the MATCH. - const match = `"${normalized.replace(/"/g, '""')}"` + const terms = tokenizeSearchQuery(normalized) + if (!terms.length) return [] + // Quote each token so user text cannot inject FTS5 operators; join with AND so multi-word + // searches match memories containing all terms rather than requiring one exact phrase. + const match = terms.map((term) => `"${term.replace(/"/g, '""')}"`).join(' AND ') try { return this.db .prepare( @@ -496,7 +564,10 @@ export class AgentMemoryTable extends BaseTable { } private searchLike(agentId: string, normalized: string, limit: number): AgentMemoryRow[] { - const pattern = `%${escapeLikePattern(normalized)}%` + const terms = tokenizeSearchQuery(normalized) + if (!terms.length) return [] + const clauses = terms.map(() => "content LIKE ? ESCAPE '\\'") + const params = terms.map((term) => `%${escapeLikePattern(term)}%`) return this.db .prepare( `SELECT * FROM agent_memory @@ -505,11 +576,11 @@ export class AgentMemoryTable extends BaseTable { AND status != 'archived' AND status != 'conflicted' AND kind != 'working' - AND content LIKE ? ESCAPE '\\' + AND ${clauses.join(' AND ')} ORDER BY importance DESC, created_at DESC LIMIT ?` ) - .all(agentId, pattern, limit) as AgentMemoryRow[] + .all(agentId, ...params, limit) as AgentMemoryRow[] } listPendingEmbedding(limit: number = 50, agentId?: string): AgentMemoryRow[] { diff --git a/src/main/presenter/sqlitePresenter/tables/deepchatTapeSearchProjection.ts b/src/main/presenter/sqlitePresenter/tables/deepchatTapeSearchProjection.ts index 554a5d072..ed4f05cd7 100644 --- a/src/main/presenter/sqlitePresenter/tables/deepchatTapeSearchProjection.ts +++ b/src/main/presenter/sqlitePresenter/tables/deepchatTapeSearchProjection.ts @@ -226,20 +226,24 @@ export class DeepChatTapeSearchProjectionTable extends BaseTable { ): void { try { this.db.transaction(() => { + if (this.ftsReady) { + this.db + .prepare('DELETE FROM deepchat_tape_search_fts WHERE session_id = ?') + .run(sessionId) + this.db + .prepare('DELETE FROM deepchat_tape_search_fts_meta WHERE session_id = ?') + .run(sessionId) + } else { + this.clearSessionFtsForBaseWrite(sessionId) + } this.db .prepare('DELETE FROM deepchat_tape_search_projection WHERE session_id = ?') .run(sessionId) this.db .prepare('DELETE FROM deepchat_tape_search_projection_meta WHERE session_id = ?') .run(sessionId) - if (!this.ftsReady) { - this.clearSessionFtsForBaseWrite(sessionId) - } this.insertProjectionRows(rows) if (this.ftsReady) { - this.db - .prepare('DELETE FROM deepchat_tape_search_fts WHERE session_id = ?') - .run(sessionId) this.insertFtsRows(rows) this.upsertFtsMeta(sessionId, projectionVersion, maxEntryId) } @@ -403,7 +407,9 @@ export class DeepChatTapeSearchProjectionTable extends BaseTable { clearAll(): void { this.db.prepare('DELETE FROM deepchat_tape_search_projection').run() this.db.prepare('DELETE FROM deepchat_tape_search_projection_meta').run() - this.db.prepare('DELETE FROM deepchat_tape_search_fts_meta').run() + if (this.ftsMetaTableExists()) { + this.db.prepare('DELETE FROM deepchat_tape_search_fts_meta').run() + } this.clearFts() } @@ -499,17 +505,48 @@ export class DeepChatTapeSearchProjectionTable extends BaseTable { return Boolean(row) } + private ftsMetaTableExists(): boolean { + const row = this.db + .prepare( + `SELECT name + FROM sqlite_master + WHERE type = 'table' AND name = 'deepchat_tape_search_fts_meta' + LIMIT 1` + ) + .get() as { name: string } | undefined + return Boolean(row) + } + private clearSessionFtsForBaseWrite(sessionId: string): void { - this.db.prepare('DELETE FROM deepchat_tape_search_fts_meta WHERE session_id = ?').run(sessionId) + if (this.ftsMetaTableExists()) { + this.db + .prepare('DELETE FROM deepchat_tape_search_fts_meta WHERE session_id = ?') + .run(sessionId) + } if (this.ftsTableExists()) { this.db.prepare('DELETE FROM deepchat_tape_search_fts WHERE session_id = ?').run(sessionId) } } + private getProjectionRowId(sessionId: string, entryId: number): number { + const row = this.db + .prepare( + `SELECT rowid + FROM deepchat_tape_search_projection + WHERE session_id = ? AND entry_id = ?` + ) + .get(sessionId, entryId) as { rowid: number } | undefined + if (!row) { + throw new Error(`Missing tape search projection row for ${sessionId}:${entryId}`) + } + return row.rowid + } + private insertFtsRows(rows: DeepChatTapeSearchProjectionInput[]): void { if (!rows.length) return const insertFts = this.db.prepare( `INSERT INTO deepchat_tape_search_fts ( + rowid, search_text, name, session_id, @@ -522,13 +559,14 @@ export class DeepChatTapeSearchProjectionTable extends BaseTable { refs_json, created_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) for (const row of rows) { this.db .prepare('DELETE FROM deepchat_tape_search_fts WHERE session_id = ? AND entry_id = ?') .run(row.sessionId, row.entryId) insertFts.run( + this.getProjectionRowId(row.sessionId, row.entryId), row.searchText, row.name ?? '', row.sessionId, @@ -601,12 +639,18 @@ export class DeepChatTapeSearchProjectionTable extends BaseTable { dropFtsForTesting(): void { this.db.exec('DROP TABLE IF EXISTS deepchat_tape_search_fts') - this.db.prepare('DELETE FROM deepchat_tape_search_fts_meta').run() + if (this.ftsMetaTableExists()) { + this.db.prepare('DELETE FROM deepchat_tape_search_fts_meta').run() + } this.ftsReady = false } private deleteSessionFts(sessionId: string): void { - this.db.prepare('DELETE FROM deepchat_tape_search_fts_meta WHERE session_id = ?').run(sessionId) + if (this.ftsMetaTableExists()) { + this.db + .prepare('DELETE FROM deepchat_tape_search_fts_meta WHERE session_id = ?') + .run(sessionId) + } if (!this.ftsTableExists()) return try { this.db.prepare('DELETE FROM deepchat_tape_search_fts WHERE session_id = ?').run(sessionId) @@ -616,7 +660,9 @@ export class DeepChatTapeSearchProjectionTable extends BaseTable { } private clearFts(): void { - this.db.prepare('DELETE FROM deepchat_tape_search_fts_meta').run() + if (this.ftsMetaTableExists()) { + this.db.prepare('DELETE FROM deepchat_tape_search_fts_meta').run() + } if (!this.ftsTableExists()) return try { this.db.prepare('DELETE FROM deepchat_tape_search_fts').run() @@ -658,9 +704,7 @@ export class DeepChatTapeSearchProjectionTable extends BaseTable { bm25(deepchat_tape_search_fts) AS score FROM deepchat_tape_search_fts INNER JOIN deepchat_tape_search_projection AS projection - ON projection.session_id = deepchat_tape_search_fts.session_id - AND projection.entry_id = CAST(deepchat_tape_search_fts.entry_id AS INTEGER) - AND projection.search_text = deepchat_tape_search_fts.search_text + ON projection.rowid = deepchat_tape_search_fts.rowid WHERE ${whereClauses.join(' AND ')} ORDER BY score ASC, projection.entry_id DESC LIMIT ?` diff --git a/src/renderer/settings/components/MemoryConfigPanel.vue b/src/renderer/settings/components/MemoryConfigPanel.vue index 085aae0f0..52994155d 100644 --- a/src/renderer/settings/components/MemoryConfigPanel.vue +++ b/src/renderer/settings/components/MemoryConfigPanel.vue @@ -95,178 +95,239 @@

-
-
- {{ t('settings.memory.config.extractionModel') }} -
- - - + +
+
+
+ {{ t('settings.memory.config.extractionModel') }} +
+ + + + + +
+
+ {{ t('settings.memory.config.extractionModel') }} +
+ +
+ - {{ modelLabel(form.memoryExtractionModel) }} -
- - - - -
-
- {{ t('settings.memory.config.extractionModel') }} -
- + + +

+ {{ t('settings.memory.config.extractionModelHint') }} +

+
+ +
+
+ {{ t('settings.memory.config.injectionBudget') }}
- - - -

- {{ t('settings.memory.config.extractionModelHint') }} -

-
+

+ {{ + t('settings.memory.config.injectionBudgetHint', { default: DEFAULTS.budget }) + }} + {{ t('settings.memory.config.inheritedHint') }} +

+
-
-
- {{ t('settings.memory.config.injectionBudget') }} -
- -

- {{ t('settings.memory.config.injectionBudgetHint', { default: DEFAULTS.budget }) }} -

-
- +
+
+
+
+ {{ t('settings.memory.config.retrievalTitle') }} +
+

+ {{ t('settings.memory.config.retrievalHint') }} +

+
+ +
-
-
-
-
- {{ t('settings.memory.config.retrievalTitle') }} -
-

- {{ t('settings.memory.config.retrievalHint') }} -

+
+ + + + +
- - - -
- - - - @@ -352,6 +413,7 @@ const saving = ref(false) const saveError = ref(null) const embeddingOpen = ref(false) const extractionOpen = ref(false) +const advancedOpen = ref(false) const originalConfig = ref({}) const resolvedConfig = ref(null) diff --git a/src/renderer/settings/components/MemoryManagerPanel.vue b/src/renderer/settings/components/MemoryManagerPanel.vue index fe1f77401..7ce713e4c 100644 --- a/src/renderer/settings/components/MemoryManagerPanel.vue +++ b/src/renderer/settings/components/MemoryManagerPanel.vue @@ -132,13 +132,16 @@
-
+
+

+ {{ searchError }} +

@@ -259,15 +262,37 @@ > - + + + + + + + + {{ t('settings.deepchatAgents.memoryManager.deleteConfirmTitle') }} + + + {{ t('settings.deepchatAgents.memoryManager.deleteConfirmBody') }} + + + + {{ t('common.cancel') }} + + {{ t('settings.deepchatAgents.memoryManager.deletePermanent') }} + + + +
@@ -639,6 +664,7 @@ let searchTimer: ReturnType | null = null // Monotonic dispatch id: only the latest search may write results, so a late response from a // superseded query (or a switched-away agent) cannot clobber the current one. let searchRequestId = 0 +let refreshRequestId = 0 const conflicts = ref([]) const personaVersions = ref([]) const personaDrafts = ref([]) @@ -647,6 +673,7 @@ const viewManifests = ref([]) const status = ref(null) const sourceSpanOpen = ref(false) const sourceSpan = ref(null) +const searchError = ref(null) const hasEmbeddingConfigured = computed(() => props.hasEmbeddingConfigured === true) // Only gates the write surface when the caller explicitly reports memory disabled; existing rows @@ -679,6 +706,8 @@ async function refreshActivity(agentId: string): Promise { async function refresh(): Promise { if (!props.agentId) return const agentId = props.agentId + refreshRequestId += 1 + const requestId = refreshRequestId loading.value = true error.value = null try { @@ -689,12 +718,12 @@ async function refresh(): Promise { memoryClient.listPersonaDrafts(agentId), memoryClient.getStatus(agentId) ]) + if (requestId !== refreshRequestId || props.agentId !== agentId) return memories.value = list conflicts.value = conflictPairs personaVersions.value = versions personaDrafts.value = drafts status.value = currentStatus - loading.value = false // Reconcile the search cache with server truth so a mutation that reloads memories does not // leave a stale (or already-deleted) row showing in search mode. if (searchActive.value) { @@ -703,8 +732,10 @@ async function refresh(): Promise { } void refreshActivity(agentId) } catch (e) { + if (requestId !== refreshRequestId || props.agentId !== agentId) return error.value = e instanceof Error ? e.message : String(e) - loading.value = false + } finally { + if (requestId === refreshRequestId && props.agentId === agentId) loading.value = false } } @@ -723,11 +754,16 @@ function isCurrentSearch(agentId: string, query: string, requestId: number): boo async function runSearch(agentId: string, query: string, requestId: number): Promise { searching.value = true + searchError.value = null try { const results = await memoryClient.search(agentId, query) if (isCurrentSearch(agentId, query, requestId)) searchResults.value = results - } catch { - if (isCurrentSearch(agentId, query, requestId)) searchResults.value = [] + } catch (e) { + if (isCurrentSearch(agentId, query, requestId)) { + searchResults.value = [] + searchError.value = + e instanceof Error ? e.message : t('settings.deepchatAgents.memoryManager.searchFailed') + } } finally { if (isCurrentSearch(agentId, query, requestId)) searching.value = false } @@ -739,6 +775,7 @@ function resetSearch(): void { searchRequestId += 1 searchQuery.value = '' searchResults.value = [] + searchError.value = null searching.value = false } @@ -751,6 +788,7 @@ watch(searchQuery, (value) => { const requestId = searchRequestId if (!query) { searchResults.value = [] + searchError.value = null searching.value = false return } @@ -852,8 +890,7 @@ async function handleDelete(memoryId: string): Promise { try { const ok = await memoryClient.remove(props.agentId, memoryId) if (!ok) return notifyActionFailed() - memories.value = memories.value.filter((memory) => memory.id !== memoryId) - searchResults.value = searchResults.value.filter((memory) => memory.id !== memoryId) + await refresh() } catch (e) { notifyActionFailed(e) } diff --git a/src/renderer/settings/components/MemorySettings.vue b/src/renderer/settings/components/MemorySettings.vue index 27960da82..a675bb02c 100644 --- a/src/renderer/settings/components/MemorySettings.vue +++ b/src/renderer/settings/components/MemorySettings.vue @@ -9,6 +9,16 @@ {{ t('common.loading') }}
+
+
{{ loadError }}
+ +
+
('config') const resolvedSelected = ref(null) const resolvedAgentId = ref('') +const loadError = ref(null) // Resolved config describes the selected agent only once its own resolve has landed. Mid-switch the // manager panel remounts on the new agentId immediately, so these flags must not leak the previous @@ -165,9 +177,24 @@ function onSelect(value: unknown): void { void router.replace({ query: { ...route.query, agentId: id } }) } +async function reload(preferred?: string | null): Promise { + loading.value = true + loadError.value = null + try { + await loadAgents(preferred ?? selectedAgentId.value) + await loadResolved() + } catch (e) { + agents.value = [] + resolvedSelected.value = null + resolvedAgentId.value = '' + loadError.value = e instanceof Error ? e.message : String(e) + } finally { + loading.value = false + } +} + async function onSaved(): Promise { - await loadAgents(selectedAgentId.value) - await loadResolved() + await reload(selectedAgentId.value) } watch(selectedAgentId, loadResolved) @@ -180,10 +207,8 @@ watch( } ) -onMounted(async () => { +onMounted(() => { const fromQuery = typeof route.query.agentId === 'string' ? route.query.agentId : null - await loadAgents(fromQuery) - await loadResolved() - loading.value = false + void reload(fromQuery) }) diff --git a/src/renderer/src/i18n/da-DK/settings.json b/src/renderer/src/i18n/da-DK/settings.json index 53b996e45..7004c80d1 100644 --- a/src/renderer/src/i18n/da-DK/settings.json +++ b/src/renderer/src/i18n/da-DK/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "Tilføjet, men er i konflikt med et eksisterende minde — løs det i listen.", "addDuplicate": "Et lignende minde findes allerede; intet blev tilføjet.", "addDisabledHint": "Hukommelse er slået fra for denne assistent. Aktivér den i Konfiguration, før du tilføjer minder.", - "addSkipped": "Intet minde blev tilføjet." + "addSkipped": "Intet minde blev tilføjet.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Personaudvikling (eksperimentel)", "personaEvolutionDescription": "Lad refleksion foreslå opdaterede selvmodeller som kladder til din godkendelse. Uafhængig af hukommelse; slået fra som standard.", @@ -2422,7 +2426,14 @@ "similarityThreshold": "Lighedstærskel", "weightSimilarity": "Vægt · lighed", "weightRecency": "Vægt · aktualitet", - "weightImportance": "Vægt · vigtighed" + "weightImportance": "Vægt · vigtighed", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/de-DE/settings.json b/src/renderer/src/i18n/de-DE/settings.json index 4a6f4d33d..5050713dd 100644 --- a/src/renderer/src/i18n/de-DE/settings.json +++ b/src/renderer/src/i18n/de-DE/settings.json @@ -221,7 +221,11 @@ "addConflict": "Hinzugefügt, steht aber im Konflikt mit einer vorhandenen Erinnerung – bitte in der Liste klären.", "addDuplicate": "Eine ähnliche Erinnerung existiert bereits; nichts wurde hinzugefügt.", "addDisabledHint": "Das Gedächtnis ist für diesen Assistenten deaktiviert. Aktivieren Sie es zuerst in der Konfiguration, um Erinnerungen hinzuzufügen.", - "addSkipped": "Es wurde keine Erinnerung hinzugefügt." + "addSkipped": "Es wurde keine Erinnerung hinzugefügt.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Persona-Entwicklung (experimentell)", "personaEvolutionDescription": "Lassen Sie die Reflexion aktualisierte Selbstmodelle als Entwürfe zur Freigabe vorschlagen. Unabhängig vom Speicher; standardmäßig deaktiviert.", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Ähnlichkeitsschwelle", "weightSimilarity": "Gewicht · Ähnlichkeit", "weightRecency": "Gewicht · Aktualität", - "weightImportance": "Gewicht · Wichtigkeit" + "weightImportance": "Gewicht · Wichtigkeit", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index ff37a7e78..26e6041d3 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -218,7 +218,11 @@ "addConflict": "Added, but it conflicts with an existing memory — resolve it in the list.", "addDuplicate": "A similar memory already exists; nothing was added.", "addDisabledHint": "Memory is off for this assistant. Enable it in Config before adding memories.", - "addSkipped": "No memory was added." + "addSkipped": "No memory was added.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "compactionThreshold": "Trigger threshold", "compactionRetainPairs": "Retain recent pairs", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Similarity threshold", "weightSimilarity": "Weight · similarity", "weightRecency": "Weight · recency", - "weightImportance": "Weight · importance" + "weightImportance": "Weight · importance", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/es-ES/settings.json b/src/renderer/src/i18n/es-ES/settings.json index ba93a0f70..1000d60b6 100644 --- a/src/renderer/src/i18n/es-ES/settings.json +++ b/src/renderer/src/i18n/es-ES/settings.json @@ -221,7 +221,11 @@ "addConflict": "Añadida, pero entra en conflicto con una memoria existente; resuélvelo en la lista.", "addDuplicate": "Ya existe una memoria similar; no se añadió nada.", "addDisabledHint": "La memoria está desactivada para este asistente. Actívala en Configuración antes de añadir memorias.", - "addSkipped": "No se añadió ninguna memoria." + "addSkipped": "No se añadió ninguna memoria.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Evolución de la persona (experimental)", "personaEvolutionDescription": "Permite que la reflexión proponga modelos de sí mismo actualizados como borradores para tu aprobación. Independiente de la memoria; desactivado por defecto.", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Umbral de similitud", "weightSimilarity": "Peso · similitud", "weightRecency": "Peso · actualidad", - "weightImportance": "Peso · importancia" + "weightImportance": "Peso · importancia", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 319c47972..1382fd18f 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "افزوده شد، اما با یک حافظهٔ موجود تداخل دارد — آن را در فهرست حل کنید.", "addDuplicate": "حافظهٔ مشابهی از قبل وجود دارد؛ چیزی افزوده نشد.", "addDisabledHint": "حافظه برای این دستیار غیرفعال است. پیش از افزودن حافظه، آن را در «پیکربندی» فعال کنید.", - "addSkipped": "هیچ حافظه‌ای افزوده نشد." + "addSkipped": "هیچ حافظه‌ای افزوده نشد.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "تکامل پرسونا (آزمایشی)", "personaEvolutionDescription": "اجازه دهید بازتاب، مدل‌های خودِ به‌روزشده را به‌صورت پیش‌نویس برای تأیید شما پیشنهاد دهد. مستقل از حافظه؛ به‌طور پیش‌فرض خاموش.", @@ -2422,7 +2426,14 @@ "similarityThreshold": "آستانهٔ شباهت", "weightSimilarity": "وزن · شباهت", "weightRecency": "وزن · تازگی", - "weightImportance": "وزن · اهمیت" + "weightImportance": "وزن · اهمیت", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index b493abc41..99ca11df9 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "Ajoutée, mais en conflit avec une mémoire existante — résolvez-le dans la liste.", "addDuplicate": "Une mémoire similaire existe déjà ; rien n'a été ajouté.", "addDisabledHint": "La mémoire est désactivée pour cet assistant. Activez-la dans la configuration avant d'ajouter des mémoires.", - "addSkipped": "Aucune mémoire n'a été ajoutée." + "addSkipped": "Aucune mémoire n'a été ajoutée.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Évolution de la persona (expérimental)", "personaEvolutionDescription": "Laissez la réflexion proposer des modèles de soi mis à jour sous forme de brouillons à approuver. Indépendant de la mémoire ; désactivé par défaut.", @@ -2422,7 +2426,14 @@ "similarityThreshold": "Seuil de similarité", "weightSimilarity": "Poids · similarité", "weightRecency": "Poids · récence", - "weightImportance": "Poids · importance" + "weightImportance": "Poids · importance", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/he-IL/settings.json b/src/renderer/src/i18n/he-IL/settings.json index 896e58c27..bb0f60a39 100644 --- a/src/renderer/src/i18n/he-IL/settings.json +++ b/src/renderer/src/i18n/he-IL/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "נוסף, אך מתנגש עם זיכרון קיים — פתור זאת ברשימה.", "addDuplicate": "כבר קיים זיכרון דומה; לא נוסף דבר.", "addDisabledHint": "הזיכרון כבוי עבור עוזר זה. הפעל אותו בהגדרות לפני הוספת זיכרונות.", - "addSkipped": "לא נוסף זיכרון." + "addSkipped": "לא נוסף זיכרון.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "התפתחות פרסונה (ניסיוני)", "personaEvolutionDescription": "אפשר לרפלקציה להציע מודלים עצמיים מעודכנים כטיוטות לאישורך. בלתי תלוי בזיכרון; כבוי כברירת מחדל.", @@ -2422,7 +2426,14 @@ "similarityThreshold": "סף דמיון", "weightSimilarity": "משקל · דמיון", "weightRecency": "משקל · עדכניות", - "weightImportance": "משקל · חשיבות" + "weightImportance": "משקל · חשיבות", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/id-ID/settings.json b/src/renderer/src/i18n/id-ID/settings.json index 0ccf58249..bd024376d 100644 --- a/src/renderer/src/i18n/id-ID/settings.json +++ b/src/renderer/src/i18n/id-ID/settings.json @@ -221,7 +221,11 @@ "addConflict": "Ditambahkan, tetapi bertentangan dengan memori yang ada — selesaikan di daftar.", "addDuplicate": "Memori serupa sudah ada; tidak ada yang ditambahkan.", "addDisabledHint": "Memori dinonaktifkan untuk asisten ini. Aktifkan di Konfigurasi sebelum menambahkan memori.", - "addSkipped": "Tidak ada memori yang ditambahkan." + "addSkipped": "Tidak ada memori yang ditambahkan.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Evolusi persona (eksperimental)", "personaEvolutionDescription": "Biarkan refleksi mengusulkan model diri yang diperbarui sebagai draf untuk persetujuan Anda. Independen dari memori; nonaktif secara default.", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Ambang kemiripan", "weightSimilarity": "Bobot · kemiripan", "weightRecency": "Bobot · kebaruan", - "weightImportance": "Bobot · kepentingan" + "weightImportance": "Bobot · kepentingan", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/it-IT/settings.json b/src/renderer/src/i18n/it-IT/settings.json index 57fea8d70..c10f988db 100644 --- a/src/renderer/src/i18n/it-IT/settings.json +++ b/src/renderer/src/i18n/it-IT/settings.json @@ -221,7 +221,11 @@ "addConflict": "Aggiunta, ma è in conflitto con una memoria esistente: risolvilo nell'elenco.", "addDuplicate": "Esiste già una memoria simile; non è stato aggiunto nulla.", "addDisabledHint": "La memoria è disattivata per questo assistente. Attivala nella configurazione prima di aggiungere memorie.", - "addSkipped": "Nessuna memoria è stata aggiunta." + "addSkipped": "Nessuna memoria è stata aggiunta.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Evoluzione della persona (sperimentale)", "personaEvolutionDescription": "Lascia che la riflessione proponga modelli di sé aggiornati come bozze da approvare. Indipendente dalla memoria; disattivata per impostazione predefinita.", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Soglia di similarità", "weightSimilarity": "Peso · similarità", "weightRecency": "Peso · recency", - "weightImportance": "Peso · importanza" + "weightImportance": "Peso · importanza", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 78eba6c11..6f6d78b29 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "追加しましたが、既存のメモリと競合しています。リストで解決してください。", "addDuplicate": "類似のメモリが既に存在するため、追加されませんでした。", "addDisabledHint": "このアシスタントのメモリは無効です。メモリを追加する前に「設定」で有効にしてください。", - "addSkipped": "メモリは追加されませんでした。" + "addSkipped": "メモリは追加されませんでした。", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "人格進化(実験的)", "personaEvolutionDescription": "リフレクションが更新後の自己モデルを下書きとして提案し、あなたの承認を待ちます。メモリとは独立で、既定では無効です。", @@ -2422,7 +2426,14 @@ "similarityThreshold": "類似度しきい値", "weightSimilarity": "重み · 類似度", "weightRecency": "重み · 新しさ", - "weightImportance": "重み · 重要度" + "weightImportance": "重み · 重要度", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index 99e175f73..c4ab4cb55 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "추가했지만 기존 메모리와 충돌합니다. 목록에서 해결하세요.", "addDuplicate": "유사한 메모리가 이미 있어 추가하지 않았습니다.", "addDisabledHint": "이 어시스턴트의 메모리가 꺼져 있습니다. 메모리를 추가하려면 먼저 구성에서 활성화하세요.", - "addSkipped": "메모리가 추가되지 않았습니다." + "addSkipped": "메모리가 추가되지 않았습니다.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "페르소나 진화 (실험적)", "personaEvolutionDescription": "리플렉션이 업데이트된 자기 모델을 초안으로 제안하여 승인을 받도록 합니다. 메모리와 독립적이며 기본값은 꺼짐입니다.", @@ -2422,7 +2426,14 @@ "similarityThreshold": "유사도 임계값", "weightSimilarity": "가중치 · 유사도", "weightRecency": "가중치 · 최신성", - "weightImportance": "가중치 · 중요도" + "weightImportance": "가중치 · 중요도", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/ms-MY/settings.json b/src/renderer/src/i18n/ms-MY/settings.json index e27cc5278..df42f1ffc 100644 --- a/src/renderer/src/i18n/ms-MY/settings.json +++ b/src/renderer/src/i18n/ms-MY/settings.json @@ -221,7 +221,11 @@ "addConflict": "Ditambah, tetapi bercanggah dengan ingatan sedia ada — selesaikannya dalam senarai.", "addDuplicate": "Ingatan serupa sudah wujud; tiada apa-apa ditambah.", "addDisabledHint": "Ingatan dimatikan untuk pembantu ini. Aktifkannya dalam Konfigurasi sebelum menambah ingatan.", - "addSkipped": "Tiada ingatan ditambah." + "addSkipped": "Tiada ingatan ditambah.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Evolusi persona (eksperimental)", "personaEvolutionDescription": "Benarkan refleksi mencadangkan model diri yang dikemas kini sebagai draf untuk kelulusan anda. Bebas daripada memori; dimatikan secara lalai.", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Ambang persamaan", "weightSimilarity": "Berat · persamaan", "weightRecency": "Berat · kebaharuan", - "weightImportance": "Berat · kepentingan" + "weightImportance": "Berat · kepentingan", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/pl-PL/settings.json b/src/renderer/src/i18n/pl-PL/settings.json index 413867ef7..8d103f135 100644 --- a/src/renderer/src/i18n/pl-PL/settings.json +++ b/src/renderer/src/i18n/pl-PL/settings.json @@ -221,7 +221,11 @@ "addConflict": "Dodano, ale jest sprzeczne z istniejącym wspomnieniem — rozwiąż to na liście.", "addDuplicate": "Podobne wspomnienie już istnieje; nic nie dodano.", "addDisabledHint": "Pamięć jest wyłączona dla tego asystenta. Włącz ją w Konfiguracji, zanim dodasz wspomnienia.", - "addSkipped": "Nie dodano żadnego wspomnienia." + "addSkipped": "Nie dodano żadnego wspomnienia.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Ewolucja persony (eksperymentalna)", "personaEvolutionDescription": "Pozwól, aby refleksja proponowała zaktualizowane modele siebie jako wersje robocze do zatwierdzenia. Niezależna od pamięci; domyślnie wyłączona.", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Próg podobieństwa", "weightSimilarity": "Waga · podobieństwo", "weightRecency": "Waga · aktualność", - "weightImportance": "Waga · ważność" + "weightImportance": "Waga · ważność", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json index d1f069528..df1bb3c1e 100644 --- a/src/renderer/src/i18n/pt-BR/settings.json +++ b/src/renderer/src/i18n/pt-BR/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "Adicionada, mas entra em conflito com uma memória existente — resolva na lista.", "addDuplicate": "Já existe uma memória semelhante; nada foi adicionado.", "addDisabledHint": "A memória está desativada para este assistente. Ative-a na Configuração antes de adicionar memórias.", - "addSkipped": "Nenhuma memória foi adicionada." + "addSkipped": "Nenhuma memória foi adicionada.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Evolução da persona (experimental)", "personaEvolutionDescription": "Permita que a reflexão proponha modelos de si atualizados como rascunhos para sua aprovação. Independente da memória; desativado por padrão.", @@ -2422,7 +2426,14 @@ "similarityThreshold": "Limiar de similaridade", "weightSimilarity": "Peso · similaridade", "weightRecency": "Peso · recência", - "weightImportance": "Peso · importância" + "weightImportance": "Peso · importância", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index ec49dbc3d..1cbe8b15a 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "Добавлено, но конфликтует с существующим воспоминанием — разрешите это в списке.", "addDuplicate": "Похожее воспоминание уже есть; ничего не добавлено.", "addDisabledHint": "Память отключена для этого ассистента. Включите её в настройках, прежде чем добавлять воспоминания.", - "addSkipped": "Воспоминание не добавлено." + "addSkipped": "Воспоминание не добавлено.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Эволюция персоны (эксперимент)", "personaEvolutionDescription": "Позвольте рефлексии предлагать обновлённые модели себя в виде черновиков для вашего одобрения. Независимо от памяти; по умолчанию выключено.", @@ -2422,7 +2426,14 @@ "similarityThreshold": "Порог сходства", "weightSimilarity": "Вес · сходство", "weightRecency": "Вес · давность", - "weightImportance": "Вес · важность" + "weightImportance": "Вес · важность", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/tr-TR/settings.json b/src/renderer/src/i18n/tr-TR/settings.json index 943d24f0e..3b97ab37d 100644 --- a/src/renderer/src/i18n/tr-TR/settings.json +++ b/src/renderer/src/i18n/tr-TR/settings.json @@ -221,7 +221,11 @@ "addConflict": "Eklendi ancak mevcut bir anıyla çakışıyor — listede çözün.", "addDuplicate": "Benzer bir anı zaten var; hiçbir şey eklenmedi.", "addDisabledHint": "Bu asistan için bellek kapalı. Anı eklemeden önce Yapılandırma'dan etkinleştirin.", - "addSkipped": "Hiçbir anı eklenmedi." + "addSkipped": "Hiçbir anı eklenmedi.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Kişilik evrimi (deneysel)", "personaEvolutionDescription": "Yansımanın güncellenmiş benlik modellerini onayınız için taslak olarak önermesine izin verin. Bellekten bağımsızdır; varsayılan olarak kapalıdır.", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Benzerlik eşiği", "weightSimilarity": "Ağırlık · benzerlik", "weightRecency": "Ağırlık · güncellik", - "weightImportance": "Ağırlık · önem" + "weightImportance": "Ağırlık · önem", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/vi-VN/settings.json b/src/renderer/src/i18n/vi-VN/settings.json index 3602ca5a7..c1afb03d5 100644 --- a/src/renderer/src/i18n/vi-VN/settings.json +++ b/src/renderer/src/i18n/vi-VN/settings.json @@ -221,7 +221,11 @@ "addConflict": "Đã thêm, nhưng xung đột với một ký ức hiện có — hãy giải quyết trong danh sách.", "addDuplicate": "Đã có một ký ức tương tự; không có gì được thêm.", "addDisabledHint": "Bộ nhớ đã tắt cho trợ lý này. Hãy bật trong phần Cấu hình trước khi thêm ký ức.", - "addSkipped": "Không có ký ức nào được thêm." + "addSkipped": "Không có ký ức nào được thêm.", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "Tiến hóa tính cách (thử nghiệm)", "personaEvolutionDescription": "Cho phép quá trình phản tư đề xuất các mô hình bản thân cập nhật dưới dạng bản nháp để bạn phê duyệt. Độc lập với bộ nhớ; mặc định tắt.", @@ -2413,7 +2417,14 @@ "similarityThreshold": "Ngưỡng tương đồng", "weightSimilarity": "Trọng số · tương đồng", "weightRecency": "Trọng số · gần đây", - "weightImportance": "Trọng số · quan trọng" + "weightImportance": "Trọng số · quan trọng", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 08602c354..5695ba6dd 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -218,7 +218,11 @@ "addConflict": "已添加,但与现有记忆冲突,请在列表中裁决。", "addDuplicate": "已记住类似内容,未重复添加。", "addDisabledHint": "该助手的记忆已关闭,请先在「配置」中启用后再添加记忆。", - "addSkipped": "未添加记忆。" + "addSkipped": "未添加记忆。", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "compactionThreshold": "触发阈值", "compactionRetainPairs": "保留最近消息对", @@ -2413,7 +2417,14 @@ "similarityThreshold": "相似度阈值", "weightSimilarity": "权重 · 相似度", "weightRecency": "权重 · 时近度", - "weightImportance": "权重 · 重要度" + "weightImportance": "权重 · 重要度", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index 9a00687c2..ad1d88a87 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "已新增,但與現有記憶衝突,請在清單中裁決。", "addDuplicate": "已記住類似內容,未重複新增。", "addDisabledHint": "此助手的記憶已關閉,請先在「配置」中啟用後再新增記憶。", - "addSkipped": "未新增記憶。" + "addSkipped": "未新增記憶。", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "人格演化(實驗)", "personaEvolutionDescription": "讓反思以草稿形式提出更新後的自我模型,交由你審核。獨立於記憶開關,預設關閉。", @@ -2422,7 +2426,14 @@ "similarityThreshold": "相似度閾值", "weightSimilarity": "權重 · 相似度", "weightRecency": "權重 · 時近度", - "weightImportance": "權重 · 重要度" + "weightImportance": "權重 · 重要度", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 0a19a3642..76815dd31 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -2290,7 +2290,11 @@ "addConflict": "已新增,但與現有記憶衝突,請在清單中裁決。", "addDuplicate": "已記住類似內容,未重複新增。", "addDisabledHint": "此助手的記憶已關閉,請先在「配置」中啟用後再新增記憶。", - "addSkipped": "未新增記憶。" + "addSkipped": "未新增記憶。", + "searchFailed": "Search failed. Try again.", + "deletePermanent": "Delete permanently", + "deleteConfirmTitle": "Delete this memory permanently?", + "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." }, "personaEvolutionTitle": "人格演化(實驗)", "personaEvolutionDescription": "讓反思以草稿形式提出更新後的自我模型,交由你審核。獨立於記憶開關,預設關閉。", @@ -2422,7 +2426,14 @@ "similarityThreshold": "相似度閾值", "weightSimilarity": "權重 · 相似度", "weightRecency": "權重 · 時近度", - "weightImportance": "權重 · 重要度" + "weightImportance": "權重 · 重要度", + "advancedTitle": "Advanced memory settings", + "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", + "inheritedHint": "Empty uses inherited/default value.", + "topKHint": "Range 1-100. Default: {default}.", + "rrfKHint": "Range 1-1000. Default: {default}.", + "similarityThresholdHint": "Range 0-1. Default: {default}.", + "weightHint": "Non-negative ranking weight. Default: {default}." } } } diff --git a/test/main/presenter/memoryExtraction.test.ts b/test/main/presenter/memoryExtraction.test.ts index 9e49726d5..47105e5b4 100644 --- a/test/main/presenter/memoryExtraction.test.ts +++ b/test/main/presenter/memoryExtraction.test.ts @@ -33,43 +33,58 @@ describe('parseMemoryCandidates', () => { const out = parseMemoryCandidates( '[{"kind":"semantic","content":"user likes redis","importance":0.8}]' ) - expect(out).toEqual([{ kind: 'semantic', content: 'user likes redis', importance: 0.8 }]) + expect(out).toEqual({ + ok: true, + candidates: [{ kind: 'semantic', content: 'user likes redis', importance: 0.8 }] + }) }) it('parses JSON inside ```json fences with surrounding prose', () => { const raw = 'Here you go:\n```json\n[{"kind":"episodic","content":"shipped v1"}]\n```\nDone.' const out = parseMemoryCandidates(raw) - expect(out).toHaveLength(1) - expect(out[0]).toMatchObject({ kind: 'episodic', content: 'shipped v1' }) - expect(out[0].importance).toBe(0.5) // default + expect(out.ok).toBe(true) + if (!out.ok) throw new Error('expected parse to succeed') + expect(out.candidates).toHaveLength(1) + expect(out.candidates[0]).toMatchObject({ kind: 'episodic', content: 'shipped v1' }) + expect(out.candidates[0].importance).toBe(0.5) // default }) it('defaults kind to semantic and clamps importance', () => { const out = parseMemoryCandidates( '[{"content":"x","importance":5},{"content":"y","importance":-2}]' ) - expect(out[0]).toMatchObject({ kind: 'semantic', importance: 1 }) - expect(out[1]).toMatchObject({ kind: 'semantic', importance: 0 }) + expect(out.ok).toBe(true) + if (!out.ok) throw new Error('expected parse to succeed') + expect(out.candidates[0]).toMatchObject({ kind: 'semantic', importance: 1 }) + expect(out.candidates[1]).toMatchObject({ kind: 'semantic', importance: 0 }) }) it('drops entries without content', () => { const out = parseMemoryCandidates('[{"kind":"semantic"},{"content":" "},{"content":"ok"}]') - expect(out).toHaveLength(1) - expect(out[0].content).toBe('ok') + expect(out.ok).toBe(true) + if (!out.ok) throw new Error('expected parse to succeed') + expect(out.candidates).toHaveLength(1) + expect(out.candidates[0].content).toBe('ok') }) - it('returns [] for empty / non-array / garbage', () => { - expect(parseMemoryCandidates('')).toEqual([]) - expect(parseMemoryCandidates('not json')).toEqual([]) - expect(parseMemoryCandidates('{"content":"x"}')).toEqual([]) - expect(parseMemoryCandidates('[broken')).toEqual([]) + it('returns parse failures for empty / non-array / garbage', () => { + expect(parseMemoryCandidates('')).toEqual({ ok: false, reason: 'empty-response' }) + expect(parseMemoryCandidates('not json')).toEqual({ ok: false, reason: 'missing-json-array' }) + expect(parseMemoryCandidates('{"content":"x"}')).toEqual({ + ok: false, + reason: 'missing-json-array' + }) + expect(parseMemoryCandidates('[broken')).toEqual({ ok: false, reason: 'missing-json-array' }) }) it('caps at 8 candidates', () => { const many = JSON.stringify( Array.from({ length: 20 }, (_, i) => ({ kind: 'semantic', content: `c${i}` })) ) - expect(parseMemoryCandidates(many)).toHaveLength(8) + const out = parseMemoryCandidates(many) + expect(out.ok).toBe(true) + if (!out.ok) throw new Error('expected parse to succeed') + expect(out.candidates).toHaveLength(8) }) }) From aff73fcc445007004c7c5b86caa96f9533b580c1 Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Mon, 22 Jun 2026 11:31:10 +0800 Subject: [PATCH 2/3] chore: update --- docs/issues/pr1794-memory-hardening/plan.md | 1 + docs/issues/pr1794-memory-hardening/spec.md | 2 +- docs/issues/pr1794-memory-hardening/tasks.md | 2 +- src/main/routes/index.ts | 5 ----- src/renderer/src/i18n/da-DK/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/de-DE/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/es-ES/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/fa-IR/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/fr-FR/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/he-IL/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/id-ID/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/it-IT/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/ja-JP/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/ko-KR/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/ms-MY/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/pl-PL/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/pt-BR/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/ru-RU/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/tr-TR/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/vi-VN/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/zh-CN/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/zh-HK/settings.json | 22 ++++++++++---------- src/renderer/src/i18n/zh-TW/settings.json | 22 ++++++++++---------- test/main/routes/memoryDto.test.ts | 3 ++- 24 files changed, 214 insertions(+), 217 deletions(-) diff --git a/docs/issues/pr1794-memory-hardening/plan.md b/docs/issues/pr1794-memory-hardening/plan.md index 39a4389cd..9cb196a6e 100644 --- a/docs/issues/pr1794-memory-hardening/plan.md +++ b/docs/issues/pr1794-memory-hardening/plan.md @@ -23,6 +23,7 @@ 3. Surface search errors distinctly from empty results. 4. Rename default destructive memory operations to archive/restore semantics; keep permanent delete as an explicit dangerous action if still exposed. 5. Move/label advanced retrieval tuning to reduce accidental misuse. +6. Localize all newly added memory management and advanced settings copy in every supported locale, preserving interpolation placeholders. ## Tests diff --git a/docs/issues/pr1794-memory-hardening/spec.md b/docs/issues/pr1794-memory-hardening/spec.md index 6e1d2735d..932b169e0 100644 --- a/docs/issues/pr1794-memory-hardening/spec.md +++ b/docs/issues/pr1794-memory-hardening/spec.md @@ -28,7 +28,7 @@ Fix the must-fix and medium-high priority review findings for PR #1794 while pre - Preserve backward compatibility with existing local SQLite databases. - Do not introduce new runtime dependencies. - Main-process DB operations remain synchronous where existing SQLite presenter patterns require it. -- UI strings must use i18n keys. +- UI strings must use i18n keys and newly added translations must be localized for each supported locale rather than copied from English. ## Non-goals diff --git a/docs/issues/pr1794-memory-hardening/tasks.md b/docs/issues/pr1794-memory-hardening/tasks.md index 84af6b236..bd516e276 100644 --- a/docs/issues/pr1794-memory-hardening/tasks.md +++ b/docs/issues/pr1794-memory-hardening/tasks.md @@ -11,6 +11,6 @@ - [x] DB: harden clear/reset and new DB schema version initialization. - [x] UI: add MemorySettings error handling. - [x] UI: guard MemoryManagerPanel refresh and search errors. -- [x] UI: clarify archive vs permanent delete and advanced settings. +- [x] UI: localize new memory management and advanced settings strings for all supported locales. - [x] Tests: update memory extraction tests for `parseMemoryCandidates` union return. - [ ] Validation: `pnpm run format`, `pnpm run i18n`, `pnpm run lint`, and `pnpm run typecheck` passed under Node v26 warning; `pnpm test` was stopped per user instruction after memoryPresenter failures surfaced. diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts index 586804ba2..130819672 100644 --- a/src/main/routes/index.ts +++ b/src/main/routes/index.ts @@ -422,13 +422,8 @@ export function formatMemorySourceRecordContent(record: ChatMessageRecord): stri const b = block as { type?: string content?: unknown - reasoning_content?: unknown - text?: unknown } if (b?.type === 'content' && typeof b.content === 'string') return b.content - if (b?.type === 'reasoning_content' && typeof b.content === 'string') return b.content - if (typeof b?.reasoning_content === 'string') return b.reasoning_content - if (b?.type === 'reasoning' && typeof b.text === 'string') return b.text return '' } if (Array.isArray(parsed)) { diff --git a/src/renderer/src/i18n/da-DK/settings.json b/src/renderer/src/i18n/da-DK/settings.json index 7004c80d1..54c4e4020 100644 --- a/src/renderer/src/i18n/da-DK/settings.json +++ b/src/renderer/src/i18n/da-DK/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "Et lignende minde findes allerede; intet blev tilføjet.", "addDisabledHint": "Hukommelse er slået fra for denne assistent. Aktivér den i Konfiguration, før du tilføjer minder.", "addSkipped": "Intet minde blev tilføjet.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Søgningen mislykkedes. Prøv igen.", + "deletePermanent": "Slet permanent", + "deleteConfirmTitle": "Slet denne hukommelse permanent?", + "deleteConfirmBody": "Dette fjerner hukommelsen i stedet for at arkivere den. Handlingen kan ikke fortrydes." }, "personaEvolutionTitle": "Personaudvikling (eksperimentel)", "personaEvolutionDescription": "Lad refleksion foreslå opdaterede selvmodeller som kladder til din godkendelse. Uafhængig af hukommelse; slået fra som standard.", @@ -2427,13 +2427,13 @@ "weightSimilarity": "Vægt · lighed", "weightRecency": "Vægt · aktualitet", "weightImportance": "Vægt · vigtighed", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Avancerede hukommelsesindstillinger", + "advancedHint": "Juster udtrækning, promptbudget og rangering ved genfinding. Lad felter være tomme for at arve standardværdier.", + "inheritedHint": "Tomt felt bruger arvet/standardværdi.", + "topKHint": "Interval 1-100. Standard: {default}.", + "rrfKHint": "Interval 1-1000. Standard: {default}.", + "similarityThresholdHint": "Interval 0-1. Standard: {default}.", + "weightHint": "Ikke-negativ rangeringsvægt. Standard: {default}." } } } diff --git a/src/renderer/src/i18n/de-DE/settings.json b/src/renderer/src/i18n/de-DE/settings.json index 5050713dd..6d94ff024 100644 --- a/src/renderer/src/i18n/de-DE/settings.json +++ b/src/renderer/src/i18n/de-DE/settings.json @@ -222,10 +222,10 @@ "addDuplicate": "Eine ähnliche Erinnerung existiert bereits; nichts wurde hinzugefügt.", "addDisabledHint": "Das Gedächtnis ist für diesen Assistenten deaktiviert. Aktivieren Sie es zuerst in der Konfiguration, um Erinnerungen hinzuzufügen.", "addSkipped": "Es wurde keine Erinnerung hinzugefügt.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Suche fehlgeschlagen. Bitte erneut versuchen.", + "deletePermanent": "Endgültig löschen", + "deleteConfirmTitle": "Diese Erinnerung endgültig löschen?", + "deleteConfirmBody": "Dadurch wird die Erinnerung entfernt statt archiviert. Dies kann nicht rückgängig gemacht werden." }, "personaEvolutionTitle": "Persona-Entwicklung (experimentell)", "personaEvolutionDescription": "Lassen Sie die Reflexion aktualisierte Selbstmodelle als Entwürfe zur Freigabe vorschlagen. Unabhängig vom Speicher; standardmäßig deaktiviert.", @@ -2418,13 +2418,13 @@ "weightSimilarity": "Gewicht · Ähnlichkeit", "weightRecency": "Gewicht · Aktualität", "weightImportance": "Gewicht · Wichtigkeit", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Erweiterte Speichereinstellungen", + "advancedHint": "Passe Extraktion, Prompt-Budget und Abruf-Ranking an. Leere Felder übernehmen die Standardwerte.", + "inheritedHint": "Leer verwendet den geerbten/Standardwert.", + "topKHint": "Bereich 1-100. Standard: {default}.", + "rrfKHint": "Bereich 1-1000. Standard: {default}.", + "similarityThresholdHint": "Bereich 0-1. Standard: {default}.", + "weightHint": "Nicht-negative Ranking-Gewichtung. Standard: {default}." } } } diff --git a/src/renderer/src/i18n/es-ES/settings.json b/src/renderer/src/i18n/es-ES/settings.json index 1000d60b6..388f7b434 100644 --- a/src/renderer/src/i18n/es-ES/settings.json +++ b/src/renderer/src/i18n/es-ES/settings.json @@ -222,10 +222,10 @@ "addDuplicate": "Ya existe una memoria similar; no se añadió nada.", "addDisabledHint": "La memoria está desactivada para este asistente. Actívala en Configuración antes de añadir memorias.", "addSkipped": "No se añadió ninguna memoria.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "La búsqueda falló. Inténtalo de nuevo.", + "deletePermanent": "Eliminar permanentemente", + "deleteConfirmTitle": "¿Eliminar esta memoria permanentemente?", + "deleteConfirmBody": "Esto elimina la memoria en lugar de archivarla. Esta acción no se puede deshacer." }, "personaEvolutionTitle": "Evolución de la persona (experimental)", "personaEvolutionDescription": "Permite que la reflexión proponga modelos de sí mismo actualizados como borradores para tu aprobación. Independiente de la memoria; desactivado por defecto.", @@ -2418,13 +2418,13 @@ "weightSimilarity": "Peso · similitud", "weightRecency": "Peso · actualidad", "weightImportance": "Peso · importancia", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Configuración avanzada de memoria", + "advancedHint": "Ajusta la extracción, el presupuesto de prompt y la clasificación de recuperación. Deja los campos vacíos para heredar los valores predeterminados.", + "inheritedHint": "Vacío usa el valor heredado/predeterminado.", + "topKHint": "Rango 1-100. Predeterminado: {default}.", + "rrfKHint": "Rango 1-1000. Predeterminado: {default}.", + "similarityThresholdHint": "Rango 0-1. Predeterminado: {default}.", + "weightHint": "Peso de clasificación no negativo. Predeterminado: {default}." } } } diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 1382fd18f..0e9ef1837 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "حافظهٔ مشابهی از قبل وجود دارد؛ چیزی افزوده نشد.", "addDisabledHint": "حافظه برای این دستیار غیرفعال است. پیش از افزودن حافظه، آن را در «پیکربندی» فعال کنید.", "addSkipped": "هیچ حافظه‌ای افزوده نشد.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "جستجو ناموفق بود. دوباره تلاش کنید.", + "deletePermanent": "حذف دائمی", + "deleteConfirmTitle": "این حافظه برای همیشه حذف شود؟", + "deleteConfirmBody": "این کار حافظه را به‌جای بایگانی، حذف می‌کند. قابل بازگشت نیست." }, "personaEvolutionTitle": "تکامل پرسونا (آزمایشی)", "personaEvolutionDescription": "اجازه دهید بازتاب، مدل‌های خودِ به‌روزشده را به‌صورت پیش‌نویس برای تأیید شما پیشنهاد دهد. مستقل از حافظه؛ به‌طور پیش‌فرض خاموش.", @@ -2427,13 +2427,13 @@ "weightSimilarity": "وزن · شباهت", "weightRecency": "وزن · تازگی", "weightImportance": "وزن · اهمیت", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "تنظیمات پیشرفته حافظه", + "advancedHint": "استخراج، بودجه پرامپت و رتبه‌بندی بازیابی را تنظیم کنید. برای به‌ارث‌بردن پیش‌فرض‌ها، فیلدها را خالی بگذارید.", + "inheritedHint": "خالی یعنی استفاده از مقدار ارث‌بری‌شده/پیش‌فرض.", + "topKHint": "بازه 1-100. پیش‌فرض: {default}.", + "rrfKHint": "بازه 1-1000. پیش‌فرض: {default}.", + "similarityThresholdHint": "بازه 0-1. پیش‌فرض: {default}.", + "weightHint": "وزن رتبه‌بندی نامنفی. پیش‌فرض: {default}." } } } diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index 99ca11df9..3757137ee 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "Une mémoire similaire existe déjà ; rien n'a été ajouté.", "addDisabledHint": "La mémoire est désactivée pour cet assistant. Activez-la dans la configuration avant d'ajouter des mémoires.", "addSkipped": "Aucune mémoire n'a été ajoutée.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "La recherche a échoué. Réessayez.", + "deletePermanent": "Supprimer définitivement", + "deleteConfirmTitle": "Supprimer définitivement cette mémoire ?", + "deleteConfirmBody": "Cela supprime la mémoire au lieu de l’archiver. Cette action est irréversible." }, "personaEvolutionTitle": "Évolution de la persona (expérimental)", "personaEvolutionDescription": "Laissez la réflexion proposer des modèles de soi mis à jour sous forme de brouillons à approuver. Indépendant de la mémoire ; désactivé par défaut.", @@ -2427,13 +2427,13 @@ "weightSimilarity": "Poids · similarité", "weightRecency": "Poids · récence", "weightImportance": "Poids · importance", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Paramètres avancés de la mémoire", + "advancedHint": "Ajustez l’extraction, le budget de prompt et le classement de récupération. Laissez les champs vides pour hériter des valeurs par défaut.", + "inheritedHint": "Vide utilise la valeur héritée/par défaut.", + "topKHint": "Plage 1-100. Par défaut : {default}.", + "rrfKHint": "Plage 1-1000. Par défaut : {default}.", + "similarityThresholdHint": "Plage 0-1. Par défaut : {default}.", + "weightHint": "Poids de classement non négatif. Par défaut : {default}." } } } diff --git a/src/renderer/src/i18n/he-IL/settings.json b/src/renderer/src/i18n/he-IL/settings.json index bb0f60a39..9477167b8 100644 --- a/src/renderer/src/i18n/he-IL/settings.json +++ b/src/renderer/src/i18n/he-IL/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "כבר קיים זיכרון דומה; לא נוסף דבר.", "addDisabledHint": "הזיכרון כבוי עבור עוזר זה. הפעל אותו בהגדרות לפני הוספת זיכרונות.", "addSkipped": "לא נוסף זיכרון.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "החיפוש נכשל. נסה שוב.", + "deletePermanent": "מחיקה לצמיתות", + "deleteConfirmTitle": "למחוק את הזיכרון הזה לצמיתות?", + "deleteConfirmBody": "פעולה זו מסירה את הזיכרון במקום לארכב אותו. לא ניתן לבטל אותה." }, "personaEvolutionTitle": "התפתחות פרסונה (ניסיוני)", "personaEvolutionDescription": "אפשר לרפלקציה להציע מודלים עצמיים מעודכנים כטיוטות לאישורך. בלתי תלוי בזיכרון; כבוי כברירת מחדל.", @@ -2427,13 +2427,13 @@ "weightSimilarity": "משקל · דמיון", "weightRecency": "משקל · עדכניות", "weightImportance": "משקל · חשיבות", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "הגדרות זיכרון מתקדמות", + "advancedHint": "כוונן חילוץ, תקציב פרומפט ודירוג אחזור. השאר שדות ריקים כדי לרשת ברירות מחדל.", + "inheritedHint": "שדה ריק משתמש בערך בירושה/ברירת מחדל.", + "topKHint": "טווח 1-100. ברירת מחדל: {default}.", + "rrfKHint": "טווח 1-1000. ברירת מחדל: {default}.", + "similarityThresholdHint": "טווח 0-1. ברירת מחדל: {default}.", + "weightHint": "משקל דירוג לא שלילי. ברירת מחדל: {default}." } } } diff --git a/src/renderer/src/i18n/id-ID/settings.json b/src/renderer/src/i18n/id-ID/settings.json index bd024376d..72e6d0cd6 100644 --- a/src/renderer/src/i18n/id-ID/settings.json +++ b/src/renderer/src/i18n/id-ID/settings.json @@ -222,10 +222,10 @@ "addDuplicate": "Memori serupa sudah ada; tidak ada yang ditambahkan.", "addDisabledHint": "Memori dinonaktifkan untuk asisten ini. Aktifkan di Konfigurasi sebelum menambahkan memori.", "addSkipped": "Tidak ada memori yang ditambahkan.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Pencarian gagal. Coba lagi.", + "deletePermanent": "Hapus permanen", + "deleteConfirmTitle": "Hapus memori ini secara permanen?", + "deleteConfirmBody": "Ini menghapus memori alih-alih mengarsipkannya. Tindakan ini tidak dapat dibatalkan." }, "personaEvolutionTitle": "Evolusi persona (eksperimental)", "personaEvolutionDescription": "Biarkan refleksi mengusulkan model diri yang diperbarui sebagai draf untuk persetujuan Anda. Independen dari memori; nonaktif secara default.", @@ -2418,13 +2418,13 @@ "weightSimilarity": "Bobot · kemiripan", "weightRecency": "Bobot · kebaruan", "weightImportance": "Bobot · kepentingan", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Pengaturan memori lanjutan", + "advancedHint": "Sesuaikan ekstraksi, anggaran prompt, dan peringkat pengambilan. Kosongkan kolom untuk mewarisi nilai default.", + "inheritedHint": "Kosong berarti menggunakan nilai turunan/default.", + "topKHint": "Rentang 1-100. Default: {default}.", + "rrfKHint": "Rentang 1-1000. Default: {default}.", + "similarityThresholdHint": "Rentang 0-1. Default: {default}.", + "weightHint": "Bobot peringkat non-negatif. Default: {default}." } } } diff --git a/src/renderer/src/i18n/it-IT/settings.json b/src/renderer/src/i18n/it-IT/settings.json index c10f988db..595ec0ff0 100644 --- a/src/renderer/src/i18n/it-IT/settings.json +++ b/src/renderer/src/i18n/it-IT/settings.json @@ -222,10 +222,10 @@ "addDuplicate": "Esiste già una memoria simile; non è stato aggiunto nulla.", "addDisabledHint": "La memoria è disattivata per questo assistente. Attivala nella configurazione prima di aggiungere memorie.", "addSkipped": "Nessuna memoria è stata aggiunta.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Ricerca non riuscita. Riprova.", + "deletePermanent": "Elimina definitivamente", + "deleteConfirmTitle": "Eliminare definitivamente questa memoria?", + "deleteConfirmBody": "Questo rimuove la memoria invece di archiviarla. L’azione non può essere annullata." }, "personaEvolutionTitle": "Evoluzione della persona (sperimentale)", "personaEvolutionDescription": "Lascia che la riflessione proponga modelli di sé aggiornati come bozze da approvare. Indipendente dalla memoria; disattivata per impostazione predefinita.", @@ -2418,13 +2418,13 @@ "weightSimilarity": "Peso · similarità", "weightRecency": "Peso · recency", "weightImportance": "Peso · importanza", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Impostazioni avanzate della memoria", + "advancedHint": "Regola estrazione, budget del prompt e ranking del recupero. Lascia i campi vuoti per ereditare i valori predefiniti.", + "inheritedHint": "Vuoto usa il valore ereditato/predefinito.", + "topKHint": "Intervallo 1-100. Predefinito: {default}.", + "rrfKHint": "Intervallo 1-1000. Predefinito: {default}.", + "similarityThresholdHint": "Intervallo 0-1. Predefinito: {default}.", + "weightHint": "Peso di ranking non negativo. Predefinito: {default}." } } } diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 6f6d78b29..1e06b7ab6 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "類似のメモリが既に存在するため、追加されませんでした。", "addDisabledHint": "このアシスタントのメモリは無効です。メモリを追加する前に「設定」で有効にしてください。", "addSkipped": "メモリは追加されませんでした。", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "検索に失敗しました。もう一度お試しください。", + "deletePermanent": "完全に削除", + "deleteConfirmTitle": "このメモリを完全に削除しますか?", + "deleteConfirmBody": "メモリをアーカイブせずに削除します。この操作は元に戻せません。" }, "personaEvolutionTitle": "人格進化(実験的)", "personaEvolutionDescription": "リフレクションが更新後の自己モデルを下書きとして提案し、あなたの承認を待ちます。メモリとは独立で、既定では無効です。", @@ -2427,13 +2427,13 @@ "weightSimilarity": "重み · 類似度", "weightRecency": "重み · 新しさ", "weightImportance": "重み · 重要度", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "高度なメモリ設定", + "advancedHint": "抽出、プロンプト予算、検索ランキングを調整します。空欄にするとデフォルトを継承します。", + "inheritedHint": "空欄の場合は継承値/デフォルト値を使用します。", + "topKHint": "範囲 1-100。デフォルト: {default}。", + "rrfKHint": "範囲 1-1000。デフォルト: {default}。", + "similarityThresholdHint": "範囲 0-1。デフォルト: {default}。", + "weightHint": "負でないランキング重み。デフォルト: {default}。" } } } diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index c4ab4cb55..a458aed0f 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "유사한 메모리가 이미 있어 추가하지 않았습니다.", "addDisabledHint": "이 어시스턴트의 메모리가 꺼져 있습니다. 메모리를 추가하려면 먼저 구성에서 활성화하세요.", "addSkipped": "메모리가 추가되지 않았습니다.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "검색에 실패했습니다. 다시 시도하세요.", + "deletePermanent": "영구 삭제", + "deleteConfirmTitle": "이 메모리를 영구 삭제할까요?", + "deleteConfirmBody": "메모리를 보관하지 않고 제거합니다. 이 작업은 되돌릴 수 없습니다." }, "personaEvolutionTitle": "페르소나 진화 (실험적)", "personaEvolutionDescription": "리플렉션이 업데이트된 자기 모델을 초안으로 제안하여 승인을 받도록 합니다. 메모리와 독립적이며 기본값은 꺼짐입니다.", @@ -2427,13 +2427,13 @@ "weightSimilarity": "가중치 · 유사도", "weightRecency": "가중치 · 최신성", "weightImportance": "가중치 · 중요도", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "고급 메모리 설정", + "advancedHint": "추출, 프롬프트 예산, 검색 순위를 조정합니다. 비워 두면 기본값을 상속합니다.", + "inheritedHint": "비워 두면 상속/기본값을 사용합니다.", + "topKHint": "범위 1-100. 기본값: {default}.", + "rrfKHint": "범위 1-1000. 기본값: {default}.", + "similarityThresholdHint": "범위 0-1. 기본값: {default}.", + "weightHint": "음수가 아닌 순위 가중치. 기본값: {default}." } } } diff --git a/src/renderer/src/i18n/ms-MY/settings.json b/src/renderer/src/i18n/ms-MY/settings.json index df42f1ffc..e833e0eb4 100644 --- a/src/renderer/src/i18n/ms-MY/settings.json +++ b/src/renderer/src/i18n/ms-MY/settings.json @@ -222,10 +222,10 @@ "addDuplicate": "Ingatan serupa sudah wujud; tiada apa-apa ditambah.", "addDisabledHint": "Ingatan dimatikan untuk pembantu ini. Aktifkannya dalam Konfigurasi sebelum menambah ingatan.", "addSkipped": "Tiada ingatan ditambah.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Carian gagal. Cuba lagi.", + "deletePermanent": "Padam secara kekal", + "deleteConfirmTitle": "Padam memori ini secara kekal?", + "deleteConfirmBody": "Ini membuang memori dan bukannya mengarkibkannya. Tindakan ini tidak boleh dibuat asal." }, "personaEvolutionTitle": "Evolusi persona (eksperimental)", "personaEvolutionDescription": "Benarkan refleksi mencadangkan model diri yang dikemas kini sebagai draf untuk kelulusan anda. Bebas daripada memori; dimatikan secara lalai.", @@ -2418,13 +2418,13 @@ "weightSimilarity": "Berat · persamaan", "weightRecency": "Berat · kebaharuan", "weightImportance": "Berat · kepentingan", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Tetapan memori lanjutan", + "advancedHint": "Laraskan pengekstrakan, bajet prompt dan penarafan dapatan semula. Biarkan medan kosong untuk mewarisi lalai.", + "inheritedHint": "Kosong menggunakan nilai diwarisi/lalai.", + "topKHint": "Julat 1-100. Lalai: {default}.", + "rrfKHint": "Julat 1-1000. Lalai: {default}.", + "similarityThresholdHint": "Julat 0-1. Lalai: {default}.", + "weightHint": "Berat penarafan tidak negatif. Lalai: {default}." } } } diff --git a/src/renderer/src/i18n/pl-PL/settings.json b/src/renderer/src/i18n/pl-PL/settings.json index 8d103f135..418b9e4f4 100644 --- a/src/renderer/src/i18n/pl-PL/settings.json +++ b/src/renderer/src/i18n/pl-PL/settings.json @@ -222,10 +222,10 @@ "addDuplicate": "Podobne wspomnienie już istnieje; nic nie dodano.", "addDisabledHint": "Pamięć jest wyłączona dla tego asystenta. Włącz ją w Konfiguracji, zanim dodasz wspomnienia.", "addSkipped": "Nie dodano żadnego wspomnienia.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Wyszukiwanie nie powiodło się. Spróbuj ponownie.", + "deletePermanent": "Usuń trwale", + "deleteConfirmTitle": "Trwale usunąć tę pamięć?", + "deleteConfirmBody": "Spowoduje to usunięcie pamięci zamiast jej zarchiwizowania. Tej operacji nie można cofnąć." }, "personaEvolutionTitle": "Ewolucja persony (eksperymentalna)", "personaEvolutionDescription": "Pozwól, aby refleksja proponowała zaktualizowane modele siebie jako wersje robocze do zatwierdzenia. Niezależna od pamięci; domyślnie wyłączona.", @@ -2418,13 +2418,13 @@ "weightSimilarity": "Waga · podobieństwo", "weightRecency": "Waga · aktualność", "weightImportance": "Waga · ważność", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Zaawansowane ustawienia pamięci", + "advancedHint": "Dostosuj ekstrakcję, budżet promptu i ranking wyszukiwania. Pozostaw pola puste, aby dziedziczyć wartości domyślne.", + "inheritedHint": "Puste używa wartości odziedziczonej/domyślnej.", + "topKHint": "Zakres 1-100. Domyślnie: {default}.", + "rrfKHint": "Zakres 1-1000. Domyślnie: {default}.", + "similarityThresholdHint": "Zakres 0-1. Domyślnie: {default}.", + "weightHint": "Nieujemna waga rankingu. Domyślnie: {default}." } } } diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json index df1bb3c1e..e7390a770 100644 --- a/src/renderer/src/i18n/pt-BR/settings.json +++ b/src/renderer/src/i18n/pt-BR/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "Já existe uma memória semelhante; nada foi adicionado.", "addDisabledHint": "A memória está desativada para este assistente. Ative-a na Configuração antes de adicionar memórias.", "addSkipped": "Nenhuma memória foi adicionada.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "A busca falhou. Tente novamente.", + "deletePermanent": "Excluir permanentemente", + "deleteConfirmTitle": "Excluir esta memória permanentemente?", + "deleteConfirmBody": "Isso remove a memória em vez de arquivá-la. Esta ação não pode ser desfeita." }, "personaEvolutionTitle": "Evolução da persona (experimental)", "personaEvolutionDescription": "Permita que a reflexão proponha modelos de si atualizados como rascunhos para sua aprovação. Independente da memória; desativado por padrão.", @@ -2427,13 +2427,13 @@ "weightSimilarity": "Peso · similaridade", "weightRecency": "Peso · recência", "weightImportance": "Peso · importância", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Configurações avançadas de memória", + "advancedHint": "Ajuste extração, orçamento de prompt e ranqueamento de recuperação. Deixe os campos vazios para herdar os padrões.", + "inheritedHint": "Vazio usa o valor herdado/padrão.", + "topKHint": "Intervalo 1-100. Padrão: {default}.", + "rrfKHint": "Intervalo 1-1000. Padrão: {default}.", + "similarityThresholdHint": "Intervalo 0-1. Padrão: {default}.", + "weightHint": "Peso de ranqueamento não negativo. Padrão: {default}." } } } diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index 1cbe8b15a..c2a2bde61 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "Похожее воспоминание уже есть; ничего не добавлено.", "addDisabledHint": "Память отключена для этого ассистента. Включите её в настройках, прежде чем добавлять воспоминания.", "addSkipped": "Воспоминание не добавлено.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Поиск не удался. Попробуйте ещё раз.", + "deletePermanent": "Удалить навсегда", + "deleteConfirmTitle": "Удалить эту память навсегда?", + "deleteConfirmBody": "Память будет удалена, а не архивирована. Это действие нельзя отменить." }, "personaEvolutionTitle": "Эволюция персоны (эксперимент)", "personaEvolutionDescription": "Позвольте рефлексии предлагать обновлённые модели себя в виде черновиков для вашего одобрения. Независимо от памяти; по умолчанию выключено.", @@ -2427,13 +2427,13 @@ "weightSimilarity": "Вес · сходство", "weightRecency": "Вес · давность", "weightImportance": "Вес · важность", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Расширенные настройки памяти", + "advancedHint": "Настройте извлечение, бюджет промпта и ранжирование поиска. Оставьте поля пустыми, чтобы наследовать значения по умолчанию.", + "inheritedHint": "Пустое поле использует унаследованное/значение по умолчанию.", + "topKHint": "Диапазон 1-100. По умолчанию: {default}.", + "rrfKHint": "Диапазон 1-1000. По умолчанию: {default}.", + "similarityThresholdHint": "Диапазон 0-1. По умолчанию: {default}.", + "weightHint": "Неотрицательный вес ранжирования. По умолчанию: {default}." } } } diff --git a/src/renderer/src/i18n/tr-TR/settings.json b/src/renderer/src/i18n/tr-TR/settings.json index 3b97ab37d..379e07452 100644 --- a/src/renderer/src/i18n/tr-TR/settings.json +++ b/src/renderer/src/i18n/tr-TR/settings.json @@ -222,10 +222,10 @@ "addDuplicate": "Benzer bir anı zaten var; hiçbir şey eklenmedi.", "addDisabledHint": "Bu asistan için bellek kapalı. Anı eklemeden önce Yapılandırma'dan etkinleştirin.", "addSkipped": "Hiçbir anı eklenmedi.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Arama başarısız oldu. Tekrar deneyin.", + "deletePermanent": "Kalıcı olarak sil", + "deleteConfirmTitle": "Bu belleği kalıcı olarak sil?", + "deleteConfirmBody": "Bu işlem belleği arşivlemek yerine kaldırır. Geri alınamaz." }, "personaEvolutionTitle": "Kişilik evrimi (deneysel)", "personaEvolutionDescription": "Yansımanın güncellenmiş benlik modellerini onayınız için taslak olarak önermesine izin verin. Bellekten bağımsızdır; varsayılan olarak kapalıdır.", @@ -2418,13 +2418,13 @@ "weightSimilarity": "Ağırlık · benzerlik", "weightRecency": "Ağırlık · güncellik", "weightImportance": "Ağırlık · önem", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Gelişmiş bellek ayarları", + "advancedHint": "Çıkarımı, istem bütçesini ve geri çağırma sıralamasını ayarlayın. Varsayılanları devralmak için alanları boş bırakın.", + "inheritedHint": "Boş bırakılırsa devralınan/varsayılan değer kullanılır.", + "topKHint": "Aralık 1-100. Varsayılan: {default}.", + "rrfKHint": "Aralık 1-1000. Varsayılan: {default}.", + "similarityThresholdHint": "Aralık 0-1. Varsayılan: {default}.", + "weightHint": "Negatif olmayan sıralama ağırlığı. Varsayılan: {default}." } } } diff --git a/src/renderer/src/i18n/vi-VN/settings.json b/src/renderer/src/i18n/vi-VN/settings.json index c1afb03d5..e960ddd33 100644 --- a/src/renderer/src/i18n/vi-VN/settings.json +++ b/src/renderer/src/i18n/vi-VN/settings.json @@ -222,10 +222,10 @@ "addDuplicate": "Đã có một ký ức tương tự; không có gì được thêm.", "addDisabledHint": "Bộ nhớ đã tắt cho trợ lý này. Hãy bật trong phần Cấu hình trước khi thêm ký ức.", "addSkipped": "Không có ký ức nào được thêm.", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "Tìm kiếm thất bại. Hãy thử lại.", + "deletePermanent": "Xóa vĩnh viễn", + "deleteConfirmTitle": "Xóa vĩnh viễn ký ức này?", + "deleteConfirmBody": "Thao tác này xóa ký ức thay vì lưu trữ. Không thể hoàn tác." }, "personaEvolutionTitle": "Tiến hóa tính cách (thử nghiệm)", "personaEvolutionDescription": "Cho phép quá trình phản tư đề xuất các mô hình bản thân cập nhật dưới dạng bản nháp để bạn phê duyệt. Độc lập với bộ nhớ; mặc định tắt.", @@ -2418,13 +2418,13 @@ "weightSimilarity": "Trọng số · tương đồng", "weightRecency": "Trọng số · gần đây", "weightImportance": "Trọng số · quan trọng", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "Cài đặt bộ nhớ nâng cao", + "advancedHint": "Tinh chỉnh trích xuất, ngân sách prompt và xếp hạng truy xuất. Để trống để kế thừa mặc định.", + "inheritedHint": "Để trống sẽ dùng giá trị kế thừa/mặc định.", + "topKHint": "Phạm vi 1-100. Mặc định: {default}.", + "rrfKHint": "Phạm vi 1-1000. Mặc định: {default}.", + "similarityThresholdHint": "Phạm vi 0-1. Mặc định: {default}.", + "weightHint": "Trọng số xếp hạng không âm. Mặc định: {default}." } } } diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 5695ba6dd..2cabb3337 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -219,10 +219,10 @@ "addDuplicate": "已记住类似内容,未重复添加。", "addDisabledHint": "该助手的记忆已关闭,请先在「配置」中启用后再添加记忆。", "addSkipped": "未添加记忆。", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "搜索失败,请重试。", + "deletePermanent": "永久删除", + "deleteConfirmTitle": "永久删除这条记忆?", + "deleteConfirmBody": "这会直接移除该记忆,而不是归档。此操作无法撤销。" }, "compactionThreshold": "触发阈值", "compactionRetainPairs": "保留最近消息对", @@ -2418,13 +2418,13 @@ "weightSimilarity": "权重 · 相似度", "weightRecency": "权重 · 时近度", "weightImportance": "权重 · 重要度", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "高级记忆设置", + "advancedHint": "调整抽取、提示词预算和检索排序。字段留空则继承默认值。", + "inheritedHint": "留空使用继承值/默认值。", + "topKHint": "范围 1-100。默认:{default}。", + "rrfKHint": "范围 1-1000。默认:{default}。", + "similarityThresholdHint": "范围 0-1。默认:{default}。", + "weightHint": "非负排序权重。默认:{default}。" } } } diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index ad1d88a87..6b9b18931 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "已記住類似內容,未重複新增。", "addDisabledHint": "此助手的記憶已關閉,請先在「配置」中啟用後再新增記憶。", "addSkipped": "未新增記憶。", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "搜尋失敗,請再試一次。", + "deletePermanent": "永久刪除", + "deleteConfirmTitle": "永久刪除這段記憶?", + "deleteConfirmBody": "這會直接移除該記憶,而不是封存。此操作無法復原。" }, "personaEvolutionTitle": "人格演化(實驗)", "personaEvolutionDescription": "讓反思以草稿形式提出更新後的自我模型,交由你審核。獨立於記憶開關,預設關閉。", @@ -2427,13 +2427,13 @@ "weightSimilarity": "權重 · 相似度", "weightRecency": "權重 · 時近度", "weightImportance": "權重 · 重要度", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "進階記憶設定", + "advancedHint": "調整擷取、提示詞預算和檢索排序。欄位留空則繼承預設值。", + "inheritedHint": "留空會使用繼承值/預設值。", + "topKHint": "範圍 1-100。預設:{default}。", + "rrfKHint": "範圍 1-1000。預設:{default}。", + "similarityThresholdHint": "範圍 0-1。預設:{default}。", + "weightHint": "非負排序權重。預設:{default}。" } } } diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 76815dd31..7160fe60c 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -2291,10 +2291,10 @@ "addDuplicate": "已記住類似內容,未重複新增。", "addDisabledHint": "此助手的記憶已關閉,請先在「配置」中啟用後再新增記憶。", "addSkipped": "未新增記憶。", - "searchFailed": "Search failed. Try again.", - "deletePermanent": "Delete permanently", - "deleteConfirmTitle": "Delete this memory permanently?", - "deleteConfirmBody": "This removes the memory instead of archiving it. This cannot be undone." + "searchFailed": "搜尋失敗,請再試一次。", + "deletePermanent": "永久刪除", + "deleteConfirmTitle": "永久刪除這段記憶?", + "deleteConfirmBody": "這會直接移除該記憶,而不是封存。此操作無法復原。" }, "personaEvolutionTitle": "人格演化(實驗)", "personaEvolutionDescription": "讓反思以草稿形式提出更新後的自我模型,交由你審核。獨立於記憶開關,預設關閉。", @@ -2427,13 +2427,13 @@ "weightSimilarity": "權重 · 相似度", "weightRecency": "權重 · 時近度", "weightImportance": "權重 · 重要度", - "advancedTitle": "Advanced memory settings", - "advancedHint": "Tune extraction, prompt budget, and retrieval ranking. Leave fields empty to inherit defaults.", - "inheritedHint": "Empty uses inherited/default value.", - "topKHint": "Range 1-100. Default: {default}.", - "rrfKHint": "Range 1-1000. Default: {default}.", - "similarityThresholdHint": "Range 0-1. Default: {default}.", - "weightHint": "Non-negative ranking weight. Default: {default}." + "advancedTitle": "進階記憶設定", + "advancedHint": "調整擷取、提示詞預算與檢索排序。欄位留空則繼承預設值。", + "inheritedHint": "留空會使用繼承值/預設值。", + "topKHint": "範圍 1-100。預設:{default}。", + "rrfKHint": "範圍 1-1000。預設:{default}。", + "similarityThresholdHint": "範圍 0-1。預設:{default}。", + "weightHint": "非負排序權重。預設:{default}。" } } } diff --git a/test/main/routes/memoryDto.test.ts b/test/main/routes/memoryDto.test.ts index ba33e61ab..fec279793 100644 --- a/test/main/routes/memoryDto.test.ts +++ b/test/main/routes/memoryDto.test.ts @@ -197,11 +197,12 @@ describe('formatMemorySourceRecordContent', () => { { type: 'content', content: 'answer body' }, { type: 'reasoning', text: 'reasoning note' }, { type: 'reasoning_content', content: 'reasoning block' }, + { reasoning_content: 'legacy reasoning field' }, { type: 'tool_call', content: '{"raw":true}' } ]) ) ) - ).toBe('answer body reasoning note reasoning block') + ).toBe('answer body') }) it('returns empty text for malformed or unsupported records', () => { From 71e4c0f3aed4c0fbdda3d2c62cd6b5504a5e1d2e Mon Sep 17 00:00:00 2001 From: yyhhyyyyyy Date: Mon, 22 Jun 2026 11:36:35 +0800 Subject: [PATCH 3/3] fix(memory): gate restore button when memory off --- src/renderer/settings/components/MemoryManagerPanel.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/settings/components/MemoryManagerPanel.vue b/src/renderer/settings/components/MemoryManagerPanel.vue index 7ce713e4c..5b90fa333 100644 --- a/src/renderer/settings/components/MemoryManagerPanel.vue +++ b/src/renderer/settings/components/MemoryManagerPanel.vue @@ -256,6 +256,7 @@ v-if="memory.status === 'archived'" variant="ghost" size="sm" + :disabled="memoryDisabled" class="h-7 px-2 text-xs" :aria-label="t('settings.deepchatAgents.memoryManager.restore')" @click="handleRestore(memory.id)" @@ -1033,6 +1034,7 @@ async function handleSetAnchor(versionId: string, anchored: boolean): Promise { + if (memoryDisabled.value) return try { const ok = await memoryClient.restore(props.agentId, memoryId) if (!ok) return notifyActionFailed()