> 🤖 本 issue 由审阅 agent 手动提交(`File agent issue` workflow 尚未进入 `main`),结论**未经维护者核实**,不代表维护者判断。 ## 问题 ```rust // crates/soul-mem-algo/src/algo/forget/decay_revise.rs:117-133 Err(error) => { log::warn!("记忆补全失败,降级为仅遮罩(kind={}, unavailable={}): {}", ...); set_summary(node, &masked_text); // ← 用遮罩文本覆写节点内容 ForgetAction::MaskOnly { missing_degree: md, masked_count: ..., masked_text } } ``` `set_summary` 会把 `SpecificSituation.narrative` 或 `SemMemory.content` **原地替换**为遮罩文本(`:140-148`)。LLM 失败(超时、限流、断流——恰恰是最常见的失败)时,原始内容就此永久消失,只剩下一条日志,且没有任何重试入口:下一次 `lazy_forget` 面对的已经是遮罩文本。 代码注释本身也承认这一点:「节点上留下遮罩文本,原始文本已不可逆」。但同仓库的滑动窗口刚刚做过一次方向相反的重构——从「先出队再总结」改成「只有成功才移除」(`sliding_window.rs:10-24`),明确把「失败即静默丢历史」判为缺陷。遗忘路径保留了同类问题。 > 可达性说明:目前只有 `soul-tune` 的遗忘流水线会调用 `lazy_forget`,但它是 `soul-mem-algo` 的公开算法入口,会被后续编排(`docs/architecture/orchestration.md` 的 🔲 遗忘流程)继承。 ## 最小复现 ```rust let mut node = specific_situation_note("张三昨天在上海的一家酒馆里喝醉了"); // 引擎返回可重试失败(Transport 等) let action = lazy_forget(&mut node, now, &jieba, None, Some(&failing_engine)).await; assert!(matches!(action, ForgetAction::MaskOnly { .. })); assert_eq!(get_summary(&node).unwrap(), "张三昨天在 [masked] 的一家酒馆里喝醉了"); // ↑ 原文已丢失;引擎恢复后也无法重试,因为原文不再存在 ``` ## 建议修法 失败时保留原文,把「需要补全」变成可重试的状态,例如: - 不覆写内容,仅在返回值/节点上标记待补全(`ForgetAction::MaskOnly` 里区分「已写入」与「仅计算」); - 或把遮罩结果与原文一并保存在节点上,仅当 LLM 成功时才用补全结果替换原文。 如果「失败即丢弃原文」确实是有意设计(「遗忘」语义上就该不可逆),请把它写进模块注释,并让返回值显式表达「原文已不可恢复」,避免调用方以为可以重试。 ## 位置 - `crates/soul-mem-algo/src/algo/forget/decay_revise.rs:106-148` - `crates/soul-mem-algo/src/algo/forget/llm_completion.rs:78-92` - 对照原则:`crates/soul-mem-runtime/src/working_memory/sliding_window.rs:10-24` ## 环境 分支 `dev`(`efaf5cb`);仅静态分析——沙箱为只读,未运行 `cargo`。
Activity
dynamder commentedon Oct 14, 2026
这是正确的语义,被遮罩的文本在遮罩的那一刻已经判定为丢失,llm失败时保留遮罩没有造成额外的丢失。遗忘的目的正是不可逆的修改原文,来模拟人类的记错过程。
滑动窗口是短期记忆,因此我们要保证短期记忆的连续性而不能静默丢失。