From d9fc9a8783cce57b441604a33953ff3f60c688ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Sun, 24 May 2026 20:34:12 +0800 Subject: [PATCH] feat(hermes): add memory flush setting --- scripts/dev-api.js | 2 ++ src-tauri/src/commands/hermes.rs | 29 +++++++++++++++++++++++++++++ src/engines/hermes/pages/config.js | 6 ++++++ src/locales/modules/engine.js | 3 ++- tests/hermes-config-page-ui.test.js | 1 + tests/hermes-memory-config.test.js | 14 ++++++++++++++ 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index d497b43..f4b6a94 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3554,6 +3554,7 @@ export function buildHermesMemoryConfigValues(config = {}) { memoryCharLimit: parseHermesInteger(memory.memory_char_limit, 'memory.memory_char_limit', 2200, 100, 200000, false), userCharLimit: parseHermesInteger(memory.user_char_limit, 'memory.user_char_limit', 1375, 100, 200000, false), nudgeInterval: parseHermesInteger(memory.nudge_interval, 'memory.nudge_interval', 10, 0, 1000, false), + flushMinTurns: parseHermesInteger(memory.flush_min_turns, 'memory.flush_min_turns', 6, 0, 1000, false), } } @@ -3568,6 +3569,7 @@ export function mergeHermesMemoryConfig(config = {}, form = {}) { memory.memory_char_limit = parseHermesInteger(Object.hasOwn(form, 'memoryCharLimit') ? form.memoryCharLimit : currentValues.memoryCharLimit, 'memory.memory_char_limit', 2200, 100, 200000, true) memory.user_char_limit = parseHermesInteger(Object.hasOwn(form, 'userCharLimit') ? form.userCharLimit : currentValues.userCharLimit, 'memory.user_char_limit', 1375, 100, 200000, true) memory.nudge_interval = parseHermesInteger(Object.hasOwn(form, 'nudgeInterval') ? form.nudgeInterval : currentValues.nudgeInterval, 'memory.nudge_interval', 10, 0, 1000, true) + memory.flush_min_turns = parseHermesInteger(Object.hasOwn(form, 'flushMinTurns') ? form.flushMinTurns : currentValues.flushMinTurns, 'memory.flush_min_turns', 6, 0, 1000, true) next.memory = memory return next } diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 2459cec..893afc5 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -3577,6 +3577,9 @@ fn build_hermes_memory_config_values(config: &serde_yaml::Value) -> Value { let nudge_interval = memory .map(|map| bounded_hermes_i64(yaml_i64_field(map, "nudge_interval"), 10, 0, 1000)) .unwrap_or(10); + let flush_min_turns = memory + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "flush_min_turns"), 6, 0, 1000)) + .unwrap_or(6); serde_json::json!({ "memoryEnabled": memory_enabled, @@ -3584,6 +3587,7 @@ fn build_hermes_memory_config_values(config: &serde_yaml::Value) -> Value { "memoryCharLimit": memory_char_limit, "userCharLimit": user_char_limit, "nudgeInterval": nudge_interval, + "flushMinTurns": flush_min_turns, }) } @@ -3626,6 +3630,17 @@ fn merge_hermes_memory_config(config: &mut serde_yaml::Value, form: &Value) -> R 0, 1000, )?; + let flush_min_turns = validate_hermes_i64( + if form.get("flushMinTurns").is_some() { + form_i64(form, "flushMinTurns") + } else { + Some(current["flushMinTurns"].as_i64().unwrap_or(6)) + }, + "memory.flush_min_turns", + 6, + 0, + 1000, + )?; let root = ensure_yaml_object(config)?; let memory = yaml_child_object(root, "memory")?; @@ -3649,6 +3664,10 @@ fn merge_hermes_memory_config(config: &mut serde_yaml::Value, form: &Value) -> R yaml_key("nudge_interval"), serde_yaml::Value::Number(nudge_interval.into()), ); + memory.insert( + yaml_key("flush_min_turns"), + serde_yaml::Value::Number(flush_min_turns.into()), + ); Ok(()) } @@ -10846,6 +10865,7 @@ mod hermes_memory_config_tests { assert_eq!(values["memoryCharLimit"], 2200); assert_eq!(values["userCharLimit"], 1375); assert_eq!(values["nudgeInterval"], 10); + assert_eq!(values["flushMinTurns"], 6); } #[test] @@ -10858,6 +10878,7 @@ memory: memory_enabled: true provider: honcho custom_flag: keep-me + flush_min_turns: 9 streaming: enabled: true "#, @@ -10872,6 +10893,7 @@ streaming: "memoryCharLimit": "2600", "userCharLimit": "1500", "nudgeInterval": "0", + "flushMinTurns": "7", }), ) .unwrap(); @@ -10886,6 +10908,7 @@ streaming: assert_eq!(config["memory"]["memory_char_limit"].as_i64(), Some(2600)); assert_eq!(config["memory"]["user_char_limit"].as_i64(), Some(1500)); assert_eq!(config["memory"]["nudge_interval"].as_i64(), Some(0)); + assert_eq!(config["memory"]["flush_min_turns"].as_i64(), Some(7)); assert_eq!(config["memory"]["provider"].as_str(), Some("honcho")); assert_eq!(config["memory"]["custom_flag"].as_str(), Some("keep-me")); } @@ -10905,6 +10928,12 @@ streaming: let err = merge_hermes_memory_config(&mut config, &json!({ "nudgeInterval": 1001 })).unwrap_err(); assert!(err.contains("memory.nudge_interval")); + let err = + merge_hermes_memory_config(&mut config, &json!({ "flushMinTurns": -1 })).unwrap_err(); + assert!(err.contains("memory.flush_min_turns")); + let err = + merge_hermes_memory_config(&mut config, &json!({ "flushMinTurns": 1001 })).unwrap_err(); + assert!(err.contains("memory.flush_min_turns")); } } diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index 19ef9fe..ec5c22a 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -40,6 +40,7 @@ const MEMORY_DEFAULTS = { memoryCharLimit: 2200, userCharLimit: 1375, nudgeInterval: 10, + flushMinTurns: 6, } const STREAMING_DEFAULTS = { @@ -343,6 +344,10 @@ export function render() { ${t('engine.hermesMemoryConfigNudgeInterval')} +
${t('engine.hermesMemoryConfigFootnote')}
@@ -864,6 +869,7 @@ export function render() { memoryCharLimit: el.querySelector('#hm-memory-char-limit')?.value || '2200', userCharLimit: el.querySelector('#hm-memory-user-char-limit')?.value || '1375', nudgeInterval: el.querySelector('#hm-memory-nudge-interval')?.value || '10', + flushMinTurns: el.querySelector('#hm-memory-flush-min-turns')?.value || '6', } memorySaving = true memoryError = null diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index a033cdc..4d8bb20 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -608,7 +608,8 @@ export default { hermesMemoryConfigMemoryCharLimit: _('记忆字符上限', 'Memory character limit', '記憶字元上限'), hermesMemoryConfigUserCharLimit: _('用户画像字符上限', 'User profile character limit', '使用者画像字元上限'), hermesMemoryConfigNudgeInterval: _('整理提醒间隔', 'Review nudge interval', '整理提醒間隔'), - hermesMemoryConfigFootnote: _('提醒间隔按用户消息轮数计算,设为 0 可关闭提醒。外部记忆 provider 等高级字段会保留在 raw YAML 中。', 'The nudge interval is counted in user turns. Set it to 0 to disable nudges. Advanced fields such as external memory provider are preserved in raw YAML.', '提醒間隔依使用者訊息輪數計算,設為 0 可關閉提醒。外部記憶 provider 等進階欄位會保留在 raw YAML 中。'), + hermesMemoryConfigFlushMinTurns: _('退出/重置前最少轮数', 'Minimum turns before flush', '退出/重置前最少輪數'), + hermesMemoryConfigFootnote: _('提醒间隔按用户消息轮数计算,设为 0 可关闭提醒。flush 最小轮数会影响退出、重置和压缩前是否先写入记忆。外部记忆 provider 等高级字段会保留在 raw YAML 中。', 'The nudge interval is counted in user turns. Set it to 0 to disable nudges. flush minimum turns controls whether memory is written before exit, reset, or compression. Advanced fields such as external memory provider are preserved in raw YAML.', '提醒間隔依使用者訊息輪數計算,設為 0 可關閉提醒。flush 最小輪數會影響退出、重置和壓縮前是否先寫入記憶。外部記憶 provider 等進階欄位會保留在 raw YAML 中。'), // Batch 1 §E: 会话导出 sessionsExport: _('导出', 'Export', '匯出'), sessionsExportSuccess: _('已导出', 'Exported', '已匯出'), diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index 8254fa4..82b444f 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -33,6 +33,7 @@ test('Hermes 配置页会暴露记忆结构化配置字段', () => { 'hm-memory-char-limit', 'hm-memory-user-char-limit', 'hm-memory-nudge-interval', + 'hm-memory-flush-min-turns', ]) { assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) } diff --git a/tests/hermes-memory-config.test.js b/tests/hermes-memory-config.test.js index b91fda1..b97fd7f 100644 --- a/tests/hermes-memory-config.test.js +++ b/tests/hermes-memory-config.test.js @@ -15,6 +15,7 @@ test('Hermes 记忆配置读取会提供上游默认值', () => { memoryCharLimit: 2200, userCharLimit: 1375, nudgeInterval: 10, + flushMinTurns: 6, }) }) @@ -26,6 +27,7 @@ test('Hermes 记忆配置读取会回显 YAML 中的记忆字段', () => { memory_char_limit: 3200, user_char_limit: 1800, nudge_interval: 12, + flush_min_turns: 8, }, }) @@ -34,6 +36,7 @@ test('Hermes 记忆配置读取会回显 YAML 中的记忆字段', () => { assert.equal(values.memoryCharLimit, 3200) assert.equal(values.userCharLimit, 1800) assert.equal(values.nudgeInterval, 12) + assert.equal(values.flushMinTurns, 8) }) test('Hermes 记忆配置保存会保留无关 YAML 并写入 snake_case 字段', () => { @@ -43,6 +46,7 @@ test('Hermes 记忆配置保存会保留无关 YAML 并写入 snake_case 字段' memory_enabled: true, provider: 'honcho', custom_flag: 'keep-me', + flush_min_turns: 9, }, streaming: { enabled: true }, }, { @@ -51,6 +55,7 @@ test('Hermes 记忆配置保存会保留无关 YAML 并写入 snake_case 字段' memoryCharLimit: '2600', userCharLimit: '1500', nudgeInterval: '0', + flushMinTurns: '7', }) assert.deepEqual(next.model, { provider: 'anthropic' }) @@ -60,6 +65,7 @@ test('Hermes 记忆配置保存会保留无关 YAML 并写入 snake_case 字段' assert.equal(next.memory.memory_char_limit, 2600) assert.equal(next.memory.user_char_limit, 1500) assert.equal(next.memory.nudge_interval, 0) + assert.equal(next.memory.flush_min_turns, 7) assert.equal(next.memory.provider, 'honcho') assert.equal(next.memory.custom_flag, 'keep-me') }) @@ -81,4 +87,12 @@ test('Hermes 记忆配置保存会拒绝越界字符上限和提醒间隔', () = () => mergeHermesMemoryConfig({}, { nudgeInterval: '1001' }), /memory\.nudge_interval/, ) + assert.throws( + () => mergeHermesMemoryConfig({}, { flushMinTurns: '-1' }), + /memory\.flush_min_turns/, + ) + assert.throws( + () => mergeHermesMemoryConfig({}, { flushMinTurns: '1001' }), + /memory\.flush_min_turns/, + ) })