Skip to content

fix: 解除记忆 UID 隔离,统一设备级读写 - #688

Open
Orsted290 wants to merge 12 commits into
MemTensor:v1.2.6from
Orsted290:fix-memory-uid-decoupling-v126
Open

Orsted290 wants to merge 12 commits into
MemTensor:v1.2.6from
Orsted290:fix-memory-uid-decoupling-v126

Conversation

@Orsted290

Copy link
Copy Markdown
Contributor

问题

记忆虽存储在本机,但 UID 同时参与 namespace、项目身份、读取过滤、采集幂等和 L3 scope。同一设备切换账号后会看到不同记忆集合,历史副本也可能重复召回。

修改方案

  • 将记忆统一为设备级身份:新写入的记忆、会话、回合和演化任务使用空 user_id;UID 不再参与项目 hash、访问过滤或记忆去重。L1/L2/L3/Skill/UserMemory 在同一设备跨账号共享。
  • schema 升至 v10,将相关 user_id 改为可空;保留历史行、UID、正文和项目 ID,不做批量迁移。项目/namespace alias 兼容旧记录;L3 合并历史 scope 时保留字段覆盖、删除语义及旧 batch 校验,避免已删除内容复活。
  • 统一普通检索、时间范围和多查询召回的同源 L1 去重。按原生来源、workspace、会话、回合及步骤识别副本;缺少可靠身份时不合并,相同文本但来源不同也保持独立。删除残留副本会一并抑制,写入幂等规则保持有效。
  • Token 计算、额度判断、用量归属和上报逻辑不变,相关生产文件无差异。

主要代码位于 Memory/src/storage/(schema、设备身份与仓储兼容)、Memory/src/service/namespace/(UID 无关的 workspace 身份)、Memory/src/service/retrieval/(同源去重及三个召回入口)、Memory/src/service/evolution/ 和 read-model/(L3 历史兼容),以及 App backend、Agent memory 配置同步处。

验收

  • Memory 回归 1371 项通过、1 项跳过;lint、typecheck、memory build、打包守卫及差异检查通过。
  • 旧库升级验收保留历史 UID、正文、项目与 L3 batch;跨账号矩阵验证 UserMemory/L1/L2/Skill 写入后可由另一 UID 召回,L3 项目上下文跨 UID 读取。普通、时间范围、多查询去重及删除副本场景由服务测试覆盖。
  • arm64 unsigned DMG 安装后的 GUI 检查覆盖各类记忆列表与详情。真实桌面验收覆盖登录与 API Key 模式往返;另一个真实云账号的新会话成功召回 L1/L2/L3/Skill。真实桌面演化确认 L2 到 v5、Skill 到 v2、L3 环境画像 v1→v2,并完成后续跨账号召回。
  • 边界:UserMemory 尚未单独完成两个真实云账号间的 GUI 召回;真实云端 token 扣费/上报未验证。包内跨账号矩阵使用合成 UID 和确定性模型;多查询融合由服务集成测试覆盖。

重点审核 schema v10 的历史值保留、项目 alias 的证据判定、L3 删除/旧 batch 兼容,以及三个召回入口是否统一使用同源身份规则。

Comment thread Memory/src/storage/repositories.ts Outdated
] as const;
const CLEAR_MEMORY_TABLES = [
...BUNDLE_TABLES,
...BUNDLE_TABLES.filter((table) => table !== "source_turn_captures" && table !== "memory_project_aliases" && table !== "memory_namespace_aliases"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

清空全部记忆后,已有原生会话的新回合无法继续采集

问题
现有设计区分两种删除操作:单条删除记忆后,保留其来源对应的删除记录,后续扫描同一来源的同一回合时,不应重新生成被删除的记忆;全量清空则重置记忆与采集记录,允许重新扫描历史。这里按来源身份识别,不是禁止所有语义相似的记忆。指定 base 的全量清空也会清除此前单条删除的记录,本条不要求新增“清空后仍保留单条删除状态”的行为。
用户清空全部记忆后,继续使用之前已经采集过的 Codex 等原生会话,新回合会返回 pending / source_session_missing,无法写入记忆;重新扫描此前已采集的回合也会被判为 capture_deleted。补充测试使用真实 Memory service 和临时 SQLite:先成功采集一轮,调用 clearAllData(),再提交同一原生会话的下一轮,base 返回 stored,本 PR 返回上述 pending。

原因
这里把记录原生回合采集结果的 source_turn_captures 表排除在全量清空之外,但关联的 sessions、episodes 和 raw_turns 仍被删除,旧记录中的会话编号及缓存结果也没有清理。
提交新回合时,latestSourceTurnCapture() 找到这条旧记录,resolveSourceSession() 随后找不到它指向的 session,于是拒绝继续创建会话;重扫旧回合则命中已删除结果。全量清空与单条删除需要不同语义,当前仍保留的源码注释及原有测试明确要求:全量清空重置采集身份,单条删除保留删除记录。

建议做法
恢复全量清空时同步清理采集记录的约定,保留 source_turn_capture_activated_at 所定义的 Hook 启用时间边界;单条记忆删除继续保留防止副本重新出现的记录。
同步调整本 PR 新增的“清空后保留 capture”测试,验证全量清空后旧历史可重新扫描、同一会话的新回合可继续采集、重启后行为一致,以及单条删除仍不会让副本重新召回。

@@ -156,7 +157,6 @@ export class L3WorldModelTraceFieldPipeline {
: undefined;
if (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

升级后,包含用户反馈的旧 L3 演化任务会永久失败

问题
v9 数据库中尚未完成的项目 L3 演化任务,如果包含用户反馈,升级并建立旧项目到设备项目的 alias 后,再执行该任务会抛出终止错误 feedback scope mismatch,无法继续演化。
临时 v9 数据库的补充测试包含有效的 session、原始回合、L1、反馈和原始 batch 哈希:base 能调用模型并完成,本 PR 在调用模型前失败。

原因
本 PR 的设备级项目编号兼容没有覆盖完整读取链路:getBatch() 与 getSession() 已把历史项目编号转换为设备级项目编号,但 getFeedback() 经 feedbackFromSql() 读取时仍返回数据库中原始的 project_id。
这里直接比较反馈与 batch 的项目编号,将同一项目的新旧编号误判为不同项目,并抛出不可重试的错误。本 PR 的旧 batch 测试使用空 feedbackIds,因此没有覆盖这条兼容路径。

建议做法
在反馈仓储的读取边界统一应用与 session、batch 相同的项目 alias 规则,覆盖 getFeedback()、listFeedback() 及相关演化读取;继续保留数据库里的历史 UID、项目编号和旧 batch 的哈希输入。
增加含显式反馈的 v9 排队/失败任务升级后继续执行的测试,同时验证真正跨项目的反馈仍被拒绝、旧 batch 被篡改时仍不能通过校验。

Comment thread Memory/src/storage/repositories.ts Outdated
if (scope.memoryId && !existing) throw new Error(`corrupt L3 World Model scope memory: ${scope.memoryId}`);
if (existing) validateL3WorldModelMemory(existing, input.userId, projectId);
const fields = existing ? fieldsFromL3WorldModelMemory(existing) : emptyL3WorldModelFields();
const fields = this.fields(null, projectId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

升级后首次更新项目约定,会让已应用的环境画像退出上下文

问题
这里的“扫描”是项目环境扫描:读取本地项目的目录结构、项目配置和相关运行时信息,用于生成或更新 L3 项目环境画像。它与 agent-source 扫描 Codex、Claude 等历史对话的功能不同。画像对应的“已应用扫描编号”记录这份画像由哪次项目环境扫描正式写入。
当升级前已有完成项目环境扫描并应用的项目环境画像、升级后还没有设备级 L3 记录时,首次更新“项目约定”或其他非环境画像字段,会让 Agent 读取的 projectEnvironmentProfile 从原来的内容变成 null。
补充测试先确认上下文能读到 Uses Node 22,再更新项目约定;base 更新前后都可读取,本 PR 更新后画像被过滤。画像正文仍在数据库中,并非被物理删除。

原因
这里通过 this.fields(null, projectId) 合并历史账号的 L3 字段,但后面读取已应用扫描编号 project_environment_applied_scan_id 时只看当前设备级 existing 记录。首次写入时该记录尚不存在,于是新 L3 记录继承了画像正文,却没有继承对应扫描编号。
随后 getMemory() 优先选择新记录,L3WorldModelContextReadModel 因缺少扫描编号而无法确认画像已应用,将其从返回上下文中去掉。

建议做法
合并历史 L3 字段时,一并携带该字段所属记录的有效元数据;继承环境画像时应保留与该画像对应的已应用扫描编号,后续更新其他字段也应保留它。仍需遵守删除/覆盖规则,避免从其他历史副本取来无关的扫描编号。
回归验证应直接检查实际 L3 上下文:旧库升级后更新项目约定或领域知识,环境画像保持可读,并覆盖重启、切换账号以及显式删除画像的场景。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants