Repository navigation
Conversation
🤖 Open Code ReviewTarget: PR #796 🔍 OpenCodeReview found 65 issue(s) in this PR.
1.
|
|
|
|
||
| listActive(userId: string, limit = 2000): UserMemoryRecord[] { | ||
| private activeWhere(userId: string, options: UserMemoryRecallOptions = {}) { | ||
| const clauses = ["user_memories.user_id = ?", "user_memories.status = 'active'", "user_memories.deleted_at IS NULL"]; |
There was a problem hiding this comment.
召回开关开启时,历史 User Memory 连原账号也无法召回
问题
升级前已保存的 User Memory(用户偏好、事实等),升级后仍能在记忆面板看到,但正式搜索无法召回;即使用原来写入它的账号查询也一样。补充测试保留历史行的 user_id=account-a,并显式开启 enableUserMemoryRecall,通过 service.search() 查询同一内容:指定 base 返回该记忆,新 head 返回空结果。新增的历史跨账号测试也在此处失败。这个问题发生在 User Memory 召回开关开启时,开关关闭时 Agent 不收到 User Memory 是预期行为;修复此底层查询问题不代表要打开开关或撤销关闭设置,而是保证开关以后开启时能够正常召回。
原因
请求的账号已在 namespace 归一化时替换为固定设备 owner,而这里的 activeWhere() 仍要求 user_memories.user_id 等于传入 owner。历史行按本 PR 的约定保留旧 UID,因此全部被过滤。这个条件同时影响有效数量统计、普通列表、全文检索、关键词检索和向量检索;后续分页补取无法补回已经被查询条件排除的行。记录没有被删除,是读取规则没有完成 UID 解耦。
建议做法
让 User Memory 的所有召回入口统一使用设备共享的读取规则,移除账号 UID 过滤,同时保留有效状态、删除状态、来源范围、排除回合及向量模型匹配等条件。不要通过批量改写历史 UID 修复。补充正式搜索的旧库升级/重启测试,覆盖原账号、另一账号和 API Key 模式,以及全文、关键词、向量三条补取路线;修复后跑通当前失败的历史保留与补取测试。
| }); | ||
| // A read projection only: retain each historical record and its ordering. | ||
| // Never seed/merge the writable device scope with these historical fields. | ||
| const field = (key: keyof typeof records[number]["fields"]) => { |
There was a problem hiding this comment.
修改或清空旧 L3 规则后,Agent 仍会收到旧规则
问题
例如,v9 历史项目规则是 Run tests。升级后把它改成 Use pnpm test instead of npm test,Agent 实际收到的上下文却同时包含旧规则和新规则;再清空该字段,Agent 又会收到旧的 Run tests。补充测试直接读取实际 L3 上下文:更新后预期只有新规则,head 返回新旧两条;清空后预期为空,head 返回旧规则。直接更新、执行排队演化任务、更新后清空这三个场景在指定 base 全部通过。这里不是要求删除历史数据库行;问题是旧行保留后,读取层仍把旧值当成当前有效规则。
原因
读取层把历史账号记录和设备记录的同一字段直接拼接,并过滤掉 null,没有表达“新版已覆盖旧版”或“此字段已明确删除”。同时,演化任务读取当前字段时调用的 fields() 只看设备记录,读不到仍会注入 Agent 的历史内容。于是演化可能把旧的 Run tests 当作空字段并按“新建”处理;写入新值后上下文仍拼出新旧两条;清空设备字段时,因 null 没有作为删除标记保存,历史旧值又成为唯一非空值并重新出现。
建议做法
统一演化输入与 Agent 上下文所使用的有效字段规则:没有设备更新时读取经验证的历史字段;设备明确更新或删除后,以该结果覆盖历史字段,区分“尚未写入”和“明确清空”。可以单独记录覆盖/删除状态,保持历史行原值不变。继续保留旧 batch 原始哈希及项目检查;环境画像还需携带其对应的已应用项目环境扫描编号。回归覆盖修订、清空、重启、跨账号读取及旧排队任务,直接断言 Agent 收到的内容。
| if (projectId === null) return [null]; | ||
| const rows = db.prepare(` | ||
| SELECT DISTINCT peer.project_id AS projectId | ||
| FROM sessions origin JOIN sessions peer |
There was a problem hiding this comment.
历史记录缺少机器标识时,相同路径会被误认成同一项目
问题
PR 前生成的数据确实有缺少机器标识的可能。目标 base v1.2.6 的 hook 在 Memory 服务未报告支持 v2 时会走旧协议兼容分支,只传 workspacePath,不传 workspaceHostId(App/memmy-agent/src/memmy-memory/hook.ts,第 728、742 行);服务端旧版 session 写入使用清洗后的元数据(Memory/src/service/session/session-turn-service.ts,第 896 行),清洗函数会移除 workspace_host_id(同文件第 4608 行)。因此通过该路径创建的 session 不会保存机器标识。
本次复现没有运行真实导出/导入流程。测试构造的是两台机器曾各自生成不同历史项目编号、但 session 中 workspace_host_id 缺失且 workspace URI 相同的情形。测试 fixture 原先有 host 值,随后专门移除它来模拟缺失;这不能说明真实存量中缺失记录的比例,也不能证明标准导入会丢掉该字段。读取其中一个项目的 L3 时,head 会把另一个项目的规则也读进来,base 只返回本项目规则。机器标识仍存在且能区分两台机器时,该测试没有复现问题。
原因
这段 JOIN 只比较 workspace URI,并使用 SQL IS 比较 host;两个缺失值满足 NULL IS NULL,所以两条记录进入同一别名集合。结果是查询时把两个原本不同的项目范围当成别名一起读取,而不是把数据库中的项目记录物理合并。这里没有校验项目编号是否确实由该历史 owner、workspace 和 host 推导,也没有拒绝冲突证据。后面的 host/hash 校验只会追加别名,不会撤销前面已加入的错误项目。L3 读取和工作记忆范围查询都会使用这个集合。
建议做法
所有别名入口统一采用可验证的项目身份规则:本地路径必须有可靠 host 证据,并验证旧 UID 派生编号与设备编号的关系;缺失、矛盾或无法验证时保留原项目隔离,不因路径相同而扩大读取范围。补充同路径不同机器、双方缺 host、伪造或修改 workspace 元数据、冲突映射和无 session 的历史 scope 测试,同时验证正确的跨账号项目仍可共享。
|
三条 review comment 已逐项修复,修复提交为 1. 历史 User Memory 在召回开启时仍无法召回
2. 修改或清空旧 L3 后仍注入旧规则
代码:有效字段与独立状态、演化输入和修订号校验、Agent 上下文。 3. 历史记录缺少 host 时同路径项目串读
代码:统一项目身份验证。 测试结果与验收边界
全量回归的唯一失败是 新增回归集中在 uid-review-regressions.test.ts,并复跑了既有 UID、L3、召回补取、画像与向量维护测试。 上述验证使用独立 SQLite fixture、隔离的 Memory home 和本地 HTTP 服务;未进行 DMG 安装/真实桌面 GUI 验收、真实模型调用或真实历史库导入导出。缺 host 的复现证明该数据形态下存在串读风险,不据此推断真实导入会丢失 host 或存量发生比例。 |
|
| const before = this.memories.get(memoryId); | ||
| if (!before) throw new Error(`corrupt L3 World Model scope memory: ${memoryId}`); | ||
| validateL3WorldModelMemory(before, scope.userId, scope.projectId); | ||
| for (const field of L3_FIELD_NAMES) { |
There was a problem hiding this comment.
删除旧 L3 记录会同时屏蔽其他未删除记录的有效字段
问题
同一项目保留历史 L3 记录,并且升级后已经生成新的设备级 L3 时,删除旧记录会连带清空其他记录的注入内容。例如旧记录只有项目约定,新设备记录保存了独立的领域知识;通过正式 deleteMemory() 删除旧记录后,新记录仍在数据库中,正文和状态都没变,但 Agent 上下文里的领域知识变成了空。相同场景在指定 base 通过、当前 head 失败。
原因
这里不检查被删记录实际包含哪些字段,也不检查哪个记录提供当前有效值,而是把该项目的画像、约定、领域知识全部写成 cleared: true。这些标记按共享项目编号保存,effectiveFields() (line 6081) 看到标记就不再读取任何候选,因此一次按记录 ID 的删除实际变成了整个共享项目的字段清空。
建议做法
让按 ID 删除只影响选中的记录。写清空标记前,逐字段确认当前有效内容是否由该记录提供;来自其他未删除记录的有效值和已有覆盖标记应保留。显式清空字段仍需防止旧内容重新生效,但不能借此覆盖无关字段。补充“旧记录仅有约定、新记录另有知识或画像,删除旧记录后其他字段继续注入”的正式服务测试,并验证重启后的结果一致。
| ) {} | ||
|
|
||
| getScope(userId: string, projectId?: string | null): L3WorldModelScopeRecord | undefined { | ||
| userId = DEVICE_MEMORY_OWNER; |
There was a problem hiding this comment.
升级前排队的项目环境扫描无法正确完成
关联位置:project-environment-service.ts (line 70)、repositories.ts (line 6834)。
问题
升级前已有排队的项目环境扫描任务时,旧任务和旧 scope 仍保存账号 UID。升级后继续执行该任务,会报 project_environment_workspace_uri_missing,尽管旧 scope 中确实保存了 workspace URI。补充测试从 v9 数据库恢复任务,通过正式 processProfileJob() 执行:base 成功,新 head 失败。这里是生成项目环境画像的目录扫描,不是 agent-source 对话扫描。
原因
processProfileJob() 用旧任务的 UID 读取 scope,但 getScope() 把传入 UID 强制替换为设备 owner,因此找不到历史 scope。只补这个读取也不够:下游 applyProfile() 将新画像写成设备 owner,却仍按旧 UID 更新已应用扫描记录;读取画像时又按画像的设备 owner 校验扫描编号。单独执行画像生成阶段的补充测试已确认:状态变成 clean、正文保存成功,但上下文仍返回空。
建议做法
为旧任务保留可验证的历史 scope 读取,并统一任务执行后的设备级画像与扫描完成记录的关联。旧任务的 owner、项目和扫描编号仍需校验,历史行保持原值;不要只放宽读取校验或批量改写历史 UID。覆盖“升级前排队→升级后完成→Agent 读到画像→重启后仍可读”的完整流程,同时验证过期任务不能覆盖较新的扫描结果。
|
已补充修复“旧会话恢复后,新建的设备扫描任务被误拒绝”,本次更新包含以下两条修复提交:
本次新增修复历史 session 保留原账号 UID,新扫描任务使用 这次仅移除该相等要求,保留任务 owner/payload/持久化任务一致性、session 项目、精确 owner 的 scope_key、workspace URI、扫描编号、字段修订号和 worker lease 校验。没有改写历史 session、scope 或 Memory,也没有新增 UID 分支、扫描版本或修改队列排序。 测试及本地 auto-cr
范围与边界本次新增修复只处理 session UID 相等限制。另两项本地 CR(删除较新画像后旧任务重新生效、导入旧任务后 enqueue_seq 顺序判断)未在本轮调整。验证使用隔离 SQLite、临时目录真实扫描及模型 mock;未进行真实模型、生产旧库、DMG/GUI 或远端 CI 验收。 请按以上范围复审。 |
|
| `UPDATE l3_world_model_scopes SET memory_id = NULL, updated_at = ? WHERE scope_key = ?` | ||
| ).run(at, scope.scopeKey); | ||
|
|
||
| const pendingTargets = this.db.prepare( |
There was a problem hiding this comment.
删除 L3 后,历史排队任务仍可重新生成已删除内容
关联当前文件 6305–6319。
问题
删除当前设备级 L3 后,旧账号遗留的任务仍可重新生成已清空的内容,重启后也会继续注入 Agent 上下文。已确认两种路径:
- 环境画像扫描:较新的设备扫描完成后,删除其画像;更早排队的旧账号扫描随后执行,又生成一份画像。
- 对话演化任务:删除项目约定、领域知识或无项目通用规则后,旧任务仍可根据旧对话重新创建这些字段;生成过程中被删除操作阻止的任务,重试后也能重新写入。
这些场景都没有在删除后创建新任务,也不是另一个原本已存在的记忆副本。隔离数据库与固定模型输出的服务测试已复现,并验证重启后内容仍存在。
原因
deleteScopeMemory() 取消任务时,仍按被删记录的原始范围编号,或精确的 owner、project ID 查找任务,没有覆盖已验证属于同一共享范围的历史账号和项目别名。
对于画像扫描,删除会清空最新扫描状态,留下的旧任务又被 isCurrentScan() 判定为当前任务。对于对话演化,旧任务开始或重试时会读取删除后的空值和最新版本,把它们当作合法起点;模型返回 create 后,提交检查通过,并覆盖删除标记。因此,现有版本检查只能阻止一次生成期间的数据冲突,不能确保旧任务在删除后持续失效。
建议做法
针对本次删除实际清空的字段,将旧任务失效范围扩展到已验证属于同一共享范围的历史 owner/项目别名,或保存删除边界,让任务开始、提交和重试都检查它。画像扫描的先后判定也不能因清空当前状态而倒退。
保留历史 batch 的身份和内容校验,避免影响其他未删除字段、其他 workspace,以及删除后正常创建的新任务。补充两条路径的回归,覆盖旧任务尚未开始、正在生成、失败后重试及数据库重启。
|
已将本地 4 个提交推送到本 PR,最新 head: 1. 删除按“实际有效字段”生效删除前读取 例如同一工作区对应历史项目别名 2. 对话演化:旧任务在开始、提交和重试时都不能恢复已删字段删除时将受影响的 queued target 持久化为 任务入口检查 target 终态;模型生成后的提交事务再次检查终态,并保留原有字段版本/hash 校验。因此任务尚未开始、正在生成、失败重试及数据库重启四条路径均受约束,生成中的旧输出也不能覆盖删除结果。 3. 导入:保留本地删除意图,覆盖分批和乱序到达使用本地私有 每次导入事务在改动任何行之前,先保留本地已有的旧版 cleared 证据;因此“先 replace 元数据、再 skip/导入旧任务”也不能擦掉删除意图。外部包携带的私有删除/扫描边界不能替换本地边界。 只处理本次实际 insert/replace 的 target/job,不误取消 skip 保留的本地新任务。补齐 target/job/batch 分批到达、job 先到、target 省略状态默认为 queued、applied/dead_letter target 后又到达 failed/configuration job 等情况;已有项目或精确 field scope 证据不足时不扩大范围。已撤销的迟到任务既不能恢复内容,也不能阻塞后续正常任务。 4. 环境画像扫描:删除后旧任务不能重新取得发布资格环境画像是扫描项目目录后生成的项目环境说明,与对话演化生成的约定/知识分开处理。本地私有 扫描执行和最终发布均检查资格与现有并发条件。导入包的 enqueue_seq 不能赋予旧扫描新的发布资格;删除后真正新请求的本地扫描仍可正常生成画像。 同时修正删除约定误伤画像的链路:只有被删记录实际提供当前有效画像,才撤销画像扫描并重置相关状态。若该记录仅提供约定,其他记录的有效画像、来源信息、排队/生成中的刷新,以及首次独立扫描继续保留。旧 lifecycle 测试预期已同步调整,并增加真实临时目录扫描回归。 5. 身份与范围保持
回归与本地 auto-cr
对应提交: |
|
合入 upstream/v1.2.6 的 330c393,解决 5 个文件的重叠冲突。 保留目标分支的面板缓存、轻量读取、事务与来源归属保护,同时保留设备 owner、历史项目别名读取及 L3 删除/导入撤销边界。 适配新详情和任务删除接口:本地以任务/会话/来源身份兼容历史 UID,云端面板保留账号范围;旧 Viewer 读取不再按配置 UID 过滤。 同步冲突旧测试,补充历史任务与设备记忆混用、来源转移、云端范围及历史 Viewer 读取回归。 验证:完整 Memory 3041 通过、2 跳过、1 项 pkg-fetch Node v24.22.1 工具链失败;lint、Memory/viewer 类型检查、runtime-bundle 6 项与 diff check 通过。 全仓 typecheck 受桌面 Electron 模块/类型缺失阻塞。
|
已解决与最新 本次同步保留双方逻辑:
最终验证:
|
|
合入 PR800 4a53878;保留缓存成员查询、索引与回填逻辑,标签和普通过滤统一沿用带数据库上下文的 memoryWhere。补充历史 UID、经验证的项目别名及其他机器隔离回归。
|
已将本地修复和 PR #800 的兼容合并推送到本 PR,最新 head: 本次更新包括 与 PR #800 的冲突处理冲突集中在
相对合并前 保留的修复逻辑
生产代码没有批量改写历史 UID、项目编号、batch 身份/hash 或历史正文;独立的 本次回归结果
本地 auto-cr 结果与验证边界已实际运行本地 auto-cr 退出码为 2(代码审查未发现阻塞项,但自动化验证未完成),以下未通过/未验证项单独保留:
审核未修改待审源码;最终开发工作区干净。 |
|
问题定位
记忆 owner 和召回候选池此前仍受账号 UID 影响。历史记忆可能因不同账号重复写入,重复项会占用召回额度,导致去重后的有效记忆数量不足。调整 owner 时还必须保留历史行原值,以及既有 source、project、tenant 等范围约束。
修改内容
具体改动与代码位置
v1.2.6 适配
本次只回移植 PR #795 的 4 个代码提交,没有合入其他分支提交。冲突解决保留了 v1.2.6 的数据库范围过滤,同时接入召回补取条件;会话服务则同时保留异步让出逻辑和设备 owner import。
测试结果
git diff --check upstream/v1.2.6...HEAD通过;未运行自动化测试。npm run lint、npm run typecheck通过;历史行/重启保留、权限范围及请求传输/UID 解耦定向测试曾通过。better-sqlite3原生模块时触发 Node assertion,未能完成;不将其列为通过。