Skip to content

feat: 后台记忆整理系统(Maintenance Agent) - #12

Merged
piexian merged 34 commits into
masterfrom
dev
Aug 11, 2026
Merged

feat: 后台记忆整理系统(Maintenance Agent)#12
piexian merged 34 commits into
masterfrom
dev

Conversation

@piexian

@piexian piexian commented Jul 31, 2026

Copy link
Copy Markdown
Owner

概述

基于后台维护 agent 对长期记忆进行离线整理、分析和审核的完整实现。

实施内容

Phase 1: 基础设施收尾

  • prompt 模板统一迁移至 string.Template
  • 配置 schema 补全(maintenance 相关 48 项配置)
  • 级联清理验证(forget/clear/rebuild 时同步清理 links)

Phase 2: LLM 唯一入口 + 执行管线

  • llm.py:唯一 LLM 入口,pair-hash 磁盘缓存,三态裁决(None≠none)
  • runner.py:执行管线框架,串行执行 purge → organizer → analyst → reviewer
  • MaintenanceReport:结构化报告,可审计

Phase 3: 整理师 Agent

  • 余弦 ≥0.9 预筛 merge 候选对
  • merge supersede 语义:新融合节点 + 旧节点 deprecated + supersedes 边
  • 按 scope 边界分组,防止跨租户合并

Phase 4: 分析师 Agent

  • 余弦 ≥0.7 预筛 link 候选对,排除已连边
  • 矛盾检测:基于对话历史发现 contradicts 关系
  • 按 scope 边界分组,防止跨租户关联

Phase 5: 审核员 Agent + 人工升级

  • reviewer.py:复核 agent 提案,controversial 标记(confidence < 0.7)
  • 互审模式:多 agent 交叉验证
  • KV 待审队列 + /memory review 命令(list/approve/reject/clear)
  • global 记忆操作强制人工审批

Phase 6: 关联召回注入

  • 召回时单跳注入关联记忆(最多 3 条)
  • 同时查询出边和入边(related/supports 对称)
  • 可见性过滤:personal/group/conversation 边界精确匹配
  • all_users 召回跳过可见性过滤
  • 关联注入后截断到 top_k

Phase 7: 配置与文档

  • 配置 schema 补全(48 项)
  • README 后台整理章节

验证

  • ruff check: 全部通过
  • 单元测试: 11/11 通过

变更统计

  • 21 commits on dev ahead of master
  • 11 files changed, ~1000 insertions, ~200 deletions

Open in Devin Review

Summary by Sourcery

引入针对长期记忆的后台维护代理系统,增加基于 LLM 的定期整合、关联与回顾工作流,同时提供更安全的废弃、合并和召回行为。

Enhancements:

  • 添加专用维护子系统,包括调度器(scheduler)、运行器(runner)和代理抽象(organizer、analyst、reviewer),用于编排离线记忆清理和分析。
  • 在独立的 links 表中跟踪记忆之间的关系,并在执行 purge、delete、clear、rebuild 和 migration 操作时,对关联进行级联清理。
  • 通过引入 deprecated_at 时间戳、为遗留记录提供迁移补丁,以及尊重宽限期的清理(purge)流程,改进废弃语义,并保持知识库(KB)与向量存储的一致性。
  • 通过具备披露感知的重排序、新的披露检索通道、多通道 RRF 融合,以及在严格可见性规则下的单跳相关记忆注入,提升召回质量。
  • 增加健壮的记忆替换、合并和废弃操作,以保留租户元数据、支持 supersede 语义,并安全迁移相关链接。
  • 将维护调度接入插件生命周期,在维护启用时禁用旧版整合逻辑,并调整整合流程,使其在将来源标记为废弃之前写入摘要。
  • 扩展用于记忆操作的 LLM 工具,增加域/范围校验、重要性和范围参数、专用的全局存储工具,以及一个在更新时保留所有权和范围的新更新工具。
  • 将提示处理统一到基于 string.Template 的模板上,并扩展配置和文档,以涵盖与维护相关的选项和行为。

Documentation:

  • 在 README 和专门的 maintenance-agent-spec markdown 文件中,记录后台维护代理的设计、工作流、安全保证和配置。
Original summary in English

Summary by Sourcery

Introduce a background maintenance agent system for long-term memory, adding scheduled LLM-based consolidation, linking, and review workflows, along with safer deprecation, merging, and recall behaviour.

Enhancements:

  • Add a dedicated maintenance subsystem with scheduler, runner, and agent abstractions (organizer, analyst, reviewer) to orchestrate offline memory cleanup and analysis.
  • Track memory relationships in a separate links table and ensure cascaded cleanup of associations during purge, delete, clear, rebuild, and migration operations.
  • Improve deprecation semantics by introducing deprecated_at timestamps, a migration patch for legacy records, and a purge routine that respects grace periods and keeps KB and vector store in sync.
  • Enhance recall quality with disclosure-aware reranking, a new disclosure retrieval channel, multi-channel RRF fusion, and single-hop injection of related memories under strict visibility rules.
  • Add robust memory replacement, merge, and deprecate operations that preserve tenant metadata, support supersede semantics, and safely migrate associated links.
  • Wire maintenance scheduling into plugin lifecycle, disable legacy consolidation when maintenance is enabled, and adjust consolidation flow to write summaries before marking sources deprecated.
  • Extend LLM tools for memory operations with domain/scope validation, importance and scope parameters, a dedicated global store tool, and a new update tool that preserves ownership and scope.
  • Unify prompt handling onto string.Template-based templates and expand configuration and documentation to cover maintenance-related options and behaviour.

Documentation:

  • Document the background maintenance agent design, workflow, safety guarantees, and configuration in README and a dedicated maintenance-agent-spec markdown file.

piexian and others added 30 commits July 22, 2026 09:51
- memory_store: 新增 importance/scope 参数,严格校验不静默降级
- memory_recall: 新增 domain/scope 过滤参数
- memory_update: 新增工具,按 URI 更新记忆保留旧属性
- memory_store_global: 补充 memory_type 校验
- 召回新增 disclosure 精确匹配通道(第三路 RRF 融合)
- _rrf_fuse 泛化为 *channels 支持任意数量检索通道
- _rerank_by_signal 新增 disclosure 匹配加成(默认 +0.25)
- prompts.py 新增 VALID_TOOL_SCOPES/VALID_TOOL_DOMAINS 校验常量
- docs/maintenance-agent-spec.md 后台整理系统设计规格
- expire_stale_memories/mark_consolidated 写入时补上 deprecated_at
- 迁移补丁: 已废弃但缺 deprecated_at 的记录用 created_at 回填
- 新增 maintenance/links.py: 关联表 CRUD (memory_links)
- 新增 maintenance/purge.py: 按 deprecated_at + N 天宽限期物理清理
- _collect_kb_doc_ids_for_filters 同时收集 URI
- _delete_by_filters/_clear_by_filters 级联清理关联边
- MemoryManager 新增 link_manager 属性和 purge_deprecated 方法
- maintenance/prompts.py: 整理师/分析师/审核员默认模板,使用
  string.Template ($var) 避免 JSON 花括号问题,build_prompt 支持
  override/extra 选配
- maintenance/scheduler.py: MaintenanceScheduler 使用 AstrBot
  cron_manager.add_basic_job 注册定时任务,single-flight 防并发
- main.py: 集成调度器(initialize/on_loaded 启动,terminate 停止)
- 自动清理 purge 作为首个 cron 任务注册
Critical:
- purge 向量删除改为按精确 kb_doc_id 逐条删除,不再用宽泛过滤器
  误删宽限期内记录;向量删除全失败时不继续下游清理
- deprecated_at 迁移回填改用 datetime('now')(迁移时间),
  不再用 created_at 导致历史废弃记忆被立即清理

High:
- memory_update 改为先写后删,写入失败不删旧记录,
  删除失败回滚新记录;迁移关联边到新 URI
- scheduler stop() 改用 cron_mgr.delete_job()(AstrBot 4.26.8 API),
  注销失败保留 job_id 不清空
- KB 迁移后重新绑定 link_manager 到目标 vec_db

Warning:
- disclosure 召回扫描上限从 30 提升到 200(可配置),
  加 ORDER BY created_at DESC 确保确定性
- _delete_by_filters/_clear_by_filters 初始化 uris=[]
  防止异常路径 UnboundLocalError
- .ccg/ 加入 .gitignore 并从 git 索引移除
High:
- purge 跟踪成功集合 (doc_id, uri),只用成功子集清理 KB/links
- memory_update 删除前读取双向关联(出边+入边),迁移到新 URI
- memory_update 拒绝 global 记忆,保留 visibility/subject/owner_sender_ids
- KB 迁移时导出源关联表 → 目标建表 → 导入,不丢失关联数据
- _delete_by_filters 候选收集失败时禁止进入删除

Warning:
- disclosure_scan_limit 加 try/except + 边界约束 [10, 1000]
- deprecated_at 迁移改用 Python UTC ISO 格式,与 purge cutoff 一致
- 审核员 prompt 破坏性操作默认 reject,非破坏性默认 approve
- scheduler _run_purge 异常重新抛出,cron manager 记录为失败
High:
- 新增 MemoryManager.replace_memory() API,完整保留原始 metadata
  (租户/作用域/归属/关联),tool_update 改用此 API 替代
  store_memory+forget_memory 组合
- 关联查询支持 limit=0(无限制),replace_memory 内部处理
  双向关联迁移,查询/写入失败记录日志
- KB 迁移先导出源关联表再删除源记录,导入后校验数量一致性
- purge 返回结构化结果 {purged, links_cleaned, failed, errors, partial},
  scheduler 检查 errors 非空时抛出异常让 cron 记录为失败

Warning:
- disclosure 召回改为分页扫描(每页 200,上限可配置至 5000),
  直到找到足够匹配或遍历完所有候选,不再有硬截断漏召回
- prompts.py: MEMORY_EXTRACTION/RECALL_QUERY/CONSOLIDATION 迁移 string.Template
- main.py: 三处 .format() 改为 .substitute()
- _conf_schema.json: 补 maintenance_* / auto_purge_* / context_* / persona_* 配置项

Phase 1 剩余项全部完成,级联清理路径已验证(forget/clear/rebuild 均走 link-aware executor)
feat(phase-1): 收尾 prompt Template 迁移 + 配置 schema 补全
- maintenance/llm.py: 唯一 LLM 入口,pair-hash 磁盘缓存,三态裁决,调用上限
- maintenance/runner.py: 执行管线框架(purge→organizer→analyst→reviewer),结构化报告
- maintenance/scheduler.py: 接入 runner,注册整理周期 cron job
- _conf_schema.json: 补 maintenance_cron 和 maintenance_max_llm_calls

Phase 2 核心框架完成,具体 Agent 实现留 Phase 3/4/5
feat(phase-2): 执行管线框架 + LLM 唯一入口 + MaintenanceReport
- maintenance/agents/organizer.py: 整理师 Agent 实现
  - 余弦 ≥0.9 预筛 merge 候选对
  - LLM 裁决 merge/link/none
  - 调用上限约束
- memory_manager.py: 补 get_all_active_memories + merge_memories
  - merge 走 supersede 语义:新建合并记忆 + 旧记忆标 deprecated + 写 supersedes 边
  - 不物理删除,由 purge 统一清理
- maintenance/runner.py: 接入 OrganizerAgent

Phase 3 核心功能完成,质量精炼(archive/update)留 Phase 5 完善
feat(phase-3): 整理师 Agent(预筛 + supersede merge)
- maintenance/agents/analyst.py: 分析师 Agent 实现
  - 余弦 ≥0.7 预筛 link 候选对
  - 排除已连边(TODO: 接入 memory_links 查询)
  - 矛盾检测(简化版:内容重叠度 + 时间差)
  - 对话历史拉取接口预留(待 AstrBot API 确认)
- maintenance/runner.py: 接入 AnalystAgent
- _conf_schema.json: 补 analyst 相关配置

Phase 4 核心功能完成,对话历史拉取待 AstrBot conversation_manager API 确认后实现
feat(phase-4): 分析师 Agent(关联发现 + 矛盾检测)
- maintenance/agents/reviewer.py: 审核员 Agent
  - 复核 merge/archive/update/new_link 操作建议
  - approve/reject verdicts
  - 置信度 < 0.5 标记 controversial,触发人工升级
- maintenance/runner.py: 互审模式 + 操作执行
  - 阶段 3.5: 驳回理由回传修正(最多 2 轮)
  - 阶段 4: 按审核结果执行操作(approve 执行 / reject 跳过 / controversial 待人工)
  - _execute_operation: merge/archive/update/new_link/contradiction 五种操作执行
  - merge 走 supersede 语义(新建 + 废弃旧 + 写 supersedes 边)
- _conf_schema.json: 补 reviewer 相关配置

Phase 5 核心功能完成,人工升级通知待 Phase 6/7 完善
feat(phase-5): 审核员 Agent + 互审模式 + 操作执行
- memory_manager.py: recall_memories 添加关联注入
  - 单跳查询,最多 3 条关联记忆
  - 只注入 related/supports/context,排除 contradicts/supersedes
  - 只召回未 deprecated 的关联记忆
  - 标记 _is_linked 便于识别
  - 新增 _inject_linked_memories 和 _get_memory_by_uri 方法

Phase 6 完成
feat(phase-6): 召回时关联记忆注入
- _conf_schema.json: 补全 organizer/analyst 开关 + 待审通知配置
- README.md:
  - 配置说明表添加后台整理相关配置
  - 工作原理添加后台整理说明
  - 新增「后台记忆整理」章节(角色/流程/安全机制/配置建议)

Phase 7 完成
feat(phase-7): 配置 schema 补全 + README 后台整理说明
1. _get_memory_by_uri SELECT text 而非 content
2. _is_memory_visible 使用 _current_owner_user_id + _is_visible_shared_personal
3. replace 回滚时同时 _unregister_kb_documents
4. organizer 分组:conversation 按完整 UMO 精确匹配,group 按 session
5. _is_memory_visible:conversation 按完整 UMO 匹配,避免私聊/群聊 ID 碰撞
6. all_users 召回模式下关联记忆跳过可见性过滤
1. get_all_active_memories 通过 FAISS reconstruct 加载向量
2. runner 改用 merge_memories / deprecate_memory / replace_memory
3. reviewer 缺失裁决时 fail closed(跳过而非放行)
4. analyst 候选对按 scope/owner/UMO 分组(与 organizer 一致)
5. maintenance_model_id 为空时回退到全局默认 provider
6. merge_memories 改用 vec_db.insert(自动生成 embedding)
7. 新增 deprecate_memory 方法(delete + re-insert 保持一致性)
8. 失败操作写入 report.errors(scheduler 可感知)
9. ruff format 全量格式化
1. merge_memories 写入 memory_content 而非 content
2. organizer 用 consumed_uris 防止源 URI 重复进入多个 merge
3. maintenance_enabled 时禁用旧 _maybe_consolidate_memories
4. reviewer 禁用/缺失裁决时 new_link 直接执行,destructive 仍 fail closed
5. get_all_active_memories 支持 offset 分页,organizer/analyst 遍历全部页
6. analyst 候选对排除已有 memory_links 边
7. 裁决缓存 key 使用解析后的 provider ID(_resolve_model_id)
1. runner 争议项写入 KV maintenance_pending_review 队列
2. scheduler/runner 接收 kv_put/kv_get 回调
3. main.py 新增 /memory review 命令(list/approve/reject/clear)
4. approve 时通过 runner._execute_operation 执行被批准的操作
5. main.py 传递 KV 回调给 MaintenanceScheduler
…时序 / LLM 超时 / 对话历史

1. deprecate_memory 先插后删(插入失败不影响原记录)
2. runner 执行前检查 global 作用域,转人工审批队列
3. merge_memories 任一源 deprecate 失败则回滚新记录并报失败
4. _run_reviewer 拉取源记录传入 original_data 供审核员对比
5. /memory review approve 先执行后标记(失败保留可重试)
6. LLM 调用加 asyncio.wait_for 超时(默认 120s,可配置)
7. analyst 实现对话历史拉取(conversation_manager API,三轮限制)
piexian added 2 commits August 1, 2026 06:41
…onfidence 字段

1. 旧巩固先写摘要再标记源(store 失败不丢数据)
2. merge 预验证所有源 + 回滚时恢复已 deprecate 的源和 supersedes 边
3. _op_touches_global 查询实际 metadata 而非 URI 子串
4. deprecate_memory 同步 KBDocument(注册新 ID / 注销旧 ID)
5. reviewer 禁用时 destructive 操作转待审队列而非静默丢弃
6. 关联召回同时查出边和入边(对称关系双向可召回)
7. reviewer prompt 输出格式加 confidence 字段(驱动 controversial 标记)
… top_k 截断 / 待审通知

1. ruff --select I 修复 4 处 import 排序(CI 通过)
2. deprecate_memory 删除旧向量失败时返回 False
3. merge_memories 插入后注册 KBDocument + 同步统计
4. merge 回滚恢复源时同步 KB(注册新 / 注销旧)
5. organizer/analyst 预筛按 batch_size 截断(默认 200)
6. 关联注入后截断到 top_k
7. 待审项入队时推送通知(review_notify_enabled + review_notify_umo)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @piexian, your pull request is larger than the review limit of 150000 diff characters

@sourcery-ai

sourcery-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Reviewer's Guide

实现一个完整的离线维护系统,用于长期记忆,包括链接存储、清理/合并/更新操作、基于 LLM 的组织者/分析师/审阅者代理、由 cron 驱动的调度器、更丰富的回忆/排序能力,以及管理员审查工具,同时收紧元数据/可见性语义并完善文档。

Sequence diagram for a maintenance cycle with merge and link operations

sequenceDiagram
    participant CronManager
    participant MaintenanceScheduler
    participant MaintenanceRunner
    participant MemoryManager
    participant MaintenanceLLM
    participant OrganizerAgent
    participant MemoryLinkManager

    CronManager->>MaintenanceScheduler: _run_maintenance_cycle(**kwargs)
    activate MaintenanceScheduler

    MaintenanceScheduler->>MaintenanceRunner: run_cycle()
    activate MaintenanceRunner

    Note over MaintenanceRunner,MemoryManager: Phase 0: purge
    MaintenanceRunner->>MemoryManager: purge_deprecated(after_days)
    MemoryManager->>MemoryLinkManager: delete_links_for_uris(uris)
    MemoryLinkManager-->>MemoryManager: links_cleaned

    Note over MaintenanceRunner,OrganizerAgent: Phase 1: organizer
    MaintenanceRunner->>OrganizerAgent: run(owner_filter)
    activate OrganizerAgent
    loop for each merge candidate
        OrganizerAgent->>MaintenanceLLM: judge_relation(text_a, text_b, cosine, model_id)
        MaintenanceLLM-->>OrganizerAgent: LLMVerdict(verdict, fused_text)
    end
    OrganizerAgent-->>MaintenanceRunner: manifest.merge
    deactivate OrganizerAgent

    Note over MaintenanceRunner,MemoryManager: Execute approved merge op
    MaintenanceRunner->>MaintenanceRunner: _execute_operation(op)
    MaintenanceRunner->>MaintenanceRunner: _execute_merge(op)
    MaintenanceRunner->>MemoryManager: merge_memories(source_uris, merged_content, reason, created_by)
    activate MemoryManager
    MemoryManager->>MemoryManager: deprecate_memory(old_uri, reason)
    MemoryManager->>MemoryLinkManager: add_link(source_uri=new_uri, target_uri=old_uri, relation_type="supersedes", reason, confidence, created_by)
    MemoryLinkManager-->>MemoryManager: bool
    MemoryManager-->>MaintenanceRunner: {success, merged_uri}
    deactivate MemoryManager

    MaintenanceRunner-->>MaintenanceScheduler: MaintenanceReport
    deactivate MaintenanceRunner
    MaintenanceScheduler-->>CronManager: done
    deactivate MaintenanceScheduler
Loading

File-Level Changes

Change Details Files
引入维护子系统,包括调度器、LLM 封装、代理流水线,以及用于离线记忆清理和重组的结构化报告。
  • 添加 MaintenanceScheduler,用于注册清理和完整维护周期的 cron 任务,支持单次执行控制(single-flight)和知识库连接性检查。
  • 实现 MaintenanceRunner 来编排 purge → organizer → analyst → reviewer 阶段,在 MaintenanceReport 中收集统计,并在具备针对全局记忆和待审队列的安全保护前提下执行已批准的操作。
  • 创建 MaintenanceLLM 作为唯一的 LLM 入口点,支持基于配对哈希的磁盘缓存、按周期的调用预算、JSON 提取,以及三态裁决(link/merge/none/None)。
  • 添加 OrganizerAgent、AnalystAgent、ReviewerAgent,提供默认提示词、候选预筛选,以及基于 manifest 的操作提议。
  • 添加维护提示词模块,使用 string.Template 进行代理提示词组合,并注入管理员指南。
maintenance/scheduler.py
maintenance/runner.py
maintenance/llm.py
maintenance/agents/organizer.py
maintenance/agents/analyst.py
maintenance/agents/reviewer.py
maintenance/prompts.py
maintenance/__init__.py
maintenance/agents/__init__.py
添加专用的记忆链接表和 API,并在清理、清空、删除、重建、回忆和替换流程中集成链接级联处理。
  • 定义 MemoryLinkManager 和 memory_links SQLite 表,包含索引、关系类型、可注入的关系集合、用于知识库迁移的导入/导出,以及 CRUD 辅助函数。
  • 在知识库连接时初始化 MemoryLinkManager,确保表存在,并在重建迁移过程中保留它。
  • 更新 purge_deprecated_memories,以删除 FAISS 向量、知识库文档,并仅对成功清理的 URI 进行级联链接移除。
  • 通过扩展 _collect_kb_doc_ids_for_filters 使其返回 URI,将链接级联删除接入 _delete_by_filters 和 _clear_by_filters。
  • 添加 replace_memory,用于创建保留元数据的新记录,删除旧记录(失败时回滚),并将旧 URI 的出站/入站链接迁移到新 URI。,在回忆过程中注入已链接记忆(单跳,最多 3 个,仅限可注入关系,且通过可见性检查),通过 _inject_linked_memories 和 _get_memory_by_uri/_is_memory_visible 辅助函数实现。
maintenance/links.py
maintenance/purge.py
memory_manager.py
收紧废弃元数据和废弃行为,并添加合并/废弃辅助方法以支持“被取代(supersede)”语义。
  • 添加 _migrate_patch_deprecated_at,用于为历史废弃记录补全 deprecated_at 时间戳,并在知识库连接时运行。
  • 扩展 expire_stale_memories 和 mark_consolidated,使其也使用当前 UTC ISO 时间设置 deprecated_at。
  • 实现 deprecate_memory,通过“插入新记录/删除旧记录”的语义标记单个 URI 为已废弃,同时保持 embeddings/KBDocument 一致性。
  • 实现 merge_memories,以创建新的合并记录、标记源记录为废弃、添加 supersedes 链接,并在部分失败时进行原子回滚。
  • 将整合流程切换为先写入摘要,再标记源记录为废弃,在记录失败日志的同时避免留下孤立摘要。
memory_manager.py
main.py
增强回忆流水线,引入披露感知评分、多通道(稠密/稀疏/披露)融合,以及为工具提供可选的 domain/scope 过滤。
  • 扩展 _rerank_by_signal 以接受 query,并通过 _tokenize_query 使用基于披露的奖励(token 重叠),调整文档字符串权重。
  • 重构 _retrieve_with_filter,以收集 sparse_memories,通过 _disclosure_retrieve 获取新的 disclosure_memories,并使用通用的 _rrf_fuse 融合各通道。
  • 实现 _filters_to_sql,将 metadata_filters 转换为带参数的 SQL,用于披露扫描并处理废弃状态。
  • 添加 _disclosure_retrieve,用于扫描具有非空披露字段的文档,对 query 进行分词并求交集,返回匹配的记忆。
  • 更新 recall_memories,以将 query 传入重排序,并注入已链接记忆,在注入后截断至 top_k。
  • 扩展 memory_recall 工具以接受 domain/scope,基于 VALID_TOOL_DOMAINS/VALID_TOOL_SCOPES 进行校验,并将过滤条件传入 recall_memories。
memory_manager.py
main.py
暴露新的 LLM 工具,用于具有作用域的记忆存储和更新,并提供更严格的参数校验以及更好的 scope/importance 处理。
  • 更新 memory_recall 工具签名以包含 domain 和 scope,对其进行校验,并通过过滤条件路由到 recall_memories。
  • 扩展 memory_store 工具以包含 importance 和 scope 参数,校验 ALLOWED_MEMORY_TYPES、scope 规则(包括仅限群聊的 group-only),并将 importance/scope 传递给 store_memory。
  • 引入 memory_update 工具,通过 URI 查找记忆(确保所有权并拒绝 global scope),然后使用 replace_memory 更新 content/disclosure。
  • 保留 memory_store_global,但通过 normalize_domain 和 ALLOWED_MEMORY_TYPES 一致地校验 memory_type。
main.py
prompts.py
添加面向管理员的审查命令和待审 KV 队列,并支持针对有争议或全局操作的通知。
  • 实现 /memory review 命令,带有 list/approve/reject/clear 子操作,仅限管理员使用,读写 maintenance_pending_review KV 队列。
  • 将 MaintenanceRunner._enqueue_pending_review 接入,以将操作追加到 KV,并附加元数据(session_id、op_type、verdict_reason、controversial 标记),必要时通过 _send_review_notification 发送通知。
  • 允许审阅者将低置信度决策标记为 controversial,从而驱动待审队列创建和人工处理流程。
  • 在 maintenance-agent-spec.md 和 README 中记录审查流程、待审队列和命令用法。
main.py
maintenance/runner.py
maintenance/agents/reviewer.py
docs/maintenance-agent-spec.md
README.md
将核心提示词迁移到 string.Template,并扩展维护和上下文设置的配置模式。
  • 将 MEMORY_EXTRACTION_PROMPT、RECALL_QUERY_PROMPT、MEMORY_CONSOLIDATION_PROMPT 更改为 Template 实例,并更新调用方以使用 substitute。
  • 添加 VALID_TOOL_SCOPES 和 VALID_TOOL_DOMAINS 常量,用于 LLM 工具参数校验。
  • 扩展 _conf_schema.json 和 README,以包含 maintenance_enabled、maintenance_model_id、maintenance_cron/limits、organizer/analyst/reviewer 开关、上下文限制、角色设定(persona settings)、以及 review_notify 选项。
  • 确保维护文档解释各阶段、代理、保护机制和配置建议。
prompts.py
_conf_schema.json
README.md
docs/maintenance-agent-spec.md

Tips and commands

Interacting with Sourcery

  • 触发新的审查: 在拉取请求中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub Issue: 通过回复审查评论让 Sourcery 创建一个 issue。你也可以回复审查评论并写上 @sourcery-ai issue 来基于该评论创建 issue。
  • 生成拉取请求标题: 在拉取请求标题的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成拉取请求摘要: 在拉取请求正文的任意位置写上 @sourcery-ai summary,即可在你想要的具体位置随时生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary 来在任何时候(重新)生成摘要。
  • 生成审查指南: 在拉取请求中评论 @sourcery-ai guide,即可在任何时候(重新)生成审查指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 忽略所有 Sourcery 审查: 在拉取请求中评论 @sourcery-ai dismiss,即可忽略所有现有 Sourcery 审查。特别适用于你希望从一个新的审查开始——不要忘记评论 @sourcery-ai review 来触发新的审查!

Customizing Your Experience

访问你的 dashboard 来:

  • 启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审查指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Implements a full offline maintenance system for long-term memory, including link storage, purge/merge/update operations, LLM-based organizer/analyst/reviewer agents, a cron-driven scheduler, richer recall/ranking, and admin review tooling, while tightening metadata/visibility semantics and documentation.

Sequence diagram for a maintenance cycle with merge and link operations

sequenceDiagram
    participant CronManager
    participant MaintenanceScheduler
    participant MaintenanceRunner
    participant MemoryManager
    participant MaintenanceLLM
    participant OrganizerAgent
    participant MemoryLinkManager

    CronManager->>MaintenanceScheduler: _run_maintenance_cycle(**kwargs)
    activate MaintenanceScheduler

    MaintenanceScheduler->>MaintenanceRunner: run_cycle()
    activate MaintenanceRunner

    Note over MaintenanceRunner,MemoryManager: Phase 0: purge
    MaintenanceRunner->>MemoryManager: purge_deprecated(after_days)
    MemoryManager->>MemoryLinkManager: delete_links_for_uris(uris)
    MemoryLinkManager-->>MemoryManager: links_cleaned

    Note over MaintenanceRunner,OrganizerAgent: Phase 1: organizer
    MaintenanceRunner->>OrganizerAgent: run(owner_filter)
    activate OrganizerAgent
    loop for each merge candidate
        OrganizerAgent->>MaintenanceLLM: judge_relation(text_a, text_b, cosine, model_id)
        MaintenanceLLM-->>OrganizerAgent: LLMVerdict(verdict, fused_text)
    end
    OrganizerAgent-->>MaintenanceRunner: manifest.merge
    deactivate OrganizerAgent

    Note over MaintenanceRunner,MemoryManager: Execute approved merge op
    MaintenanceRunner->>MaintenanceRunner: _execute_operation(op)
    MaintenanceRunner->>MaintenanceRunner: _execute_merge(op)
    MaintenanceRunner->>MemoryManager: merge_memories(source_uris, merged_content, reason, created_by)
    activate MemoryManager
    MemoryManager->>MemoryManager: deprecate_memory(old_uri, reason)
    MemoryManager->>MemoryLinkManager: add_link(source_uri=new_uri, target_uri=old_uri, relation_type="supersedes", reason, confidence, created_by)
    MemoryLinkManager-->>MemoryManager: bool
    MemoryManager-->>MaintenanceRunner: {success, merged_uri}
    deactivate MemoryManager

    MaintenanceRunner-->>MaintenanceScheduler: MaintenanceReport
    deactivate MaintenanceRunner
    MaintenanceScheduler-->>CronManager: done
    deactivate MaintenanceScheduler
Loading

File-Level Changes

Change Details Files
Introduce maintenance subsystem with scheduler, LLM wrapper, agent pipeline, and structured reporting for offline memory cleanup and reorganization.
  • Add MaintenanceScheduler to register cron jobs for purge and full maintenance cycles, with single-flight execution and KB connectivity checks.
  • Implement MaintenanceRunner to orchestrate purge → organizer → analyst → reviewer stages, collect stats in MaintenanceReport, and execute approved operations with safeguards for global memories and pending-review queue.
  • Create MaintenanceLLM as the single LLM entrypoint with pair-hash disk caching, per-cycle call budget, JSON extraction, and three-state verdicts (link/merge/none/None).
  • Add OrganizerAgent, AnalystAgent, ReviewerAgent with default prompts, candidate pre-screening, and manifest-based operation proposals.
  • Add maintenance prompts module for string.Template-based agent prompt composition with admin guide injection.
maintenance/scheduler.py
maintenance/runner.py
maintenance/llm.py
maintenance/agents/organizer.py
maintenance/agents/analyst.py
maintenance/agents/reviewer.py
maintenance/prompts.py
maintenance/__init__.py
maintenance/agents/__init__.py
Add a dedicated memory link table and APIs, and integrate link cascade handling across purge, clear, delete, rebuild, recall, and replace flows.
  • Define MemoryLinkManager and memory_links SQLite table with indexes, relation types, injectable relation set, export/import for KB migration, and CRUD helpers.
  • Initialize MemoryLinkManager when KB connects, ensure table, and carry it through rebuild migrations.
  • Update purge_deprecated_memories to delete FAISS vectors, KB documents, and cascade link removal only for successfully purged URIs.
  • Wire link cascade deletion into _delete_by_filters and _clear_by_filters via extended _collect_kb_doc_ids_for_filters returning URIs.
  • Add replace_memory to create a new record preserving metadata, delete old record with rollback on failure, and migrate outgoing/incoming links from old URI to new URI.,Inject linked memories during recall (single hop, max 3, injectable relations only, visibility-checked) via _inject_linked_memories and _get_memory_by_uri/_is_memory_visible helpers.
maintenance/links.py
maintenance/purge.py
memory_manager.py
Tighten deprecated metadata and deprecation behavior, and add merge/deprecate helpers to support supersede semantics.
  • Add _migrate_patch_deprecated_at to backfill deprecated_at timestamps for legacy deprecated records and run it on KB connection.
  • Extend expire_stale_memories and mark_consolidated to also set deprecated_at using current UTC ISO time.
  • Implement deprecate_memory to mark a single URI deprecated via insert-new/delete-old semantics while keeping embeddings/KBDocument consistent.
  • Implement merge_memories to create a new merged record, mark sources deprecated, add supersedes links, and roll back atomically on partial failures.
  • Switch consolidation flow to write summary first, then mark sources deprecated, logging failures without leaving orphan summaries.
memory_manager.py
main.py
Enhance recall pipeline with disclosure-aware scoring, multi-channel (dense/sparse/disclosure) fusion, and optional domain/scope filtering for tools.
  • Extend _rerank_by_signal to accept query and include disclosure-based bonus using token overlap via _tokenize_query; adjust docstring weights.
  • Refactor _retrieve_with_filter to collect sparse_memories, new disclosure_memories via _disclosure_retrieve, and fuse channels with generalized _rrf_fuse.
  • Implement _filters_to_sql to convert metadata_filters to parameterized SQL for disclosure scanning with deprecated handling.
  • Add _disclosure_retrieve to scan documents with non-empty disclosure, tokenize and intersect with query tokens, and return matched memories.
  • Update recall_memories to pass query into rerank and to inject linked memories, truncating to top_k after injection.
  • Extend memory_recall tool to accept domain/scope, validate against VALID_TOOL_DOMAINS/VALID_TOOL_SCOPES, and pass filters to recall_memories.
memory_manager.py
main.py
Expose new LLM tools for scoped memory store and update, with stricter parameter validation and better scope/importance handling.
  • Update memory_recall tool signature to include domain and scope, validate them, and route to recall_memories with filters.
  • Extend memory_store tool with importance and scope parameters, validate ALLOWED_MEMORY_TYPES, scope rules (including group-only in group chats), and pass importance/scope through to store_memory.
  • Introduce memory_update tool that looks up a memory by URI (ensuring ownership and rejecting global scope), then uses replace_memory to update content/disclosure.
  • Keep memory_store_global but validate memory_type consistently via normalize_domain and ALLOWED_MEMORY_TYPES.
main.py
prompts.py
Add admin-facing review command and pending-review KV queue with notification support for controversial or global operations.
  • Implement /memory review command with sub-actions list/approve/reject/clear, limited to admins, reading/writing maintenance_pending_review KV queue.
  • Wire MaintenanceRunner._enqueue_pending_review to append operations to KV with metadata (session_id, op_type, verdict_reason, controversial flag) and optionally send notifications via _send_review_notification.
  • Allow reviewer to tag low-confidence decisions as controversial, driving pending-review creation and manual flows.
  • Document review flow, pending queue, and command usage in maintenance-agent-spec.md and README.
main.py
maintenance/runner.py
maintenance/agents/reviewer.py
docs/maintenance-agent-spec.md
README.md
Migrate core prompts to string.Template and extend configuration schema for maintenance and context settings.
  • Change MEMORY_EXTRACTION_PROMPT, RECALL_QUERY_PROMPT, MEMORY_CONSOLIDATION_PROMPT to Template instances and update callers to use substitute.
  • Add VALID_TOOL_SCOPES and VALID_TOOL_DOMAINS constants for LLM tool argument validation.
  • Extend _conf_schema.json and README to include maintenance_enabled, maintenance_model_id, maintenance_cron/limits, organizer/analyst/reviewer toggles, context limits, persona settings, and review_notify options.
  • Ensure maintenance docs explain phases, agents, safeties, and configuration recommendations.
prompts.py
_conf_schema.json
README.md
docs/maintenance-agent-spec.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 9 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread memory_manager.py
Comment on lines +1205 to +1209
if linked_memories:
memories.extend(linked_memories)
# 关联注入后截断到 top_k,避免超出调用方预期
if top_k and len(memories) > top_k:
memories = memories[:top_k]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Related memories are found but then thrown away before reaching the assistant

The extra related memories are added to the result list and then cut back down to the requested size (memories[:top_k] at memory_manager.py:1209) after the list is already full, so the newly added related memories are always discarded whenever the normal search returns a full page of results.
Impact: The new "related memory injection" feature almost never actually adds anything, so linked context is silently missing from replies.

Truncation order removes exactly the appended items

In MemoryManager.recall_memories, the primary results are already truncated to top_k earlier (memory_manager.py:1170 for the all-users branch and memory_manager.py:1187 for the normal branch). Then at memory_manager.py:1201-1209 the linked memories are appended to the end of that list and the combined list is truncated back to top_k. Since len(memories) == top_k in the common case, slicing [:top_k] keeps only the original head and drops every appended linked memory. Only when the primary recall returns fewer than top_k hits does any linked memory survive.

A fix would be to reserve budget for linked memories (e.g. truncate primary results to top_k - len(linked) or interleave before truncation).

Prompt for agents
In MemoryManager.recall_memories (memory_manager.py, around lines 1200-1209), linked memories obtained from _inject_linked_memories are appended to `memories` and then the combined list is truncated back to top_k. Because `memories` was already truncated to top_k earlier in the function, the appended linked memories are always dropped in the common case, making the Phase-6 link injection feature a no-op. Rework the budgeting so linked memories actually get a slot: e.g. trim the primary results to `top_k - min(len(linked), reserve)` before extending, or apply a dedicated reserve (like the documented 'max 3 linked') on top of top_k, and make sure the primary results are not silently starved.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +336 to +345
for i, mem_a in enumerate(memories):
for mem_b in memories[i + 1 :]:
# 检查内容是否涉及同一主题(简化版:检查关键词重叠)
content_a = mem_a.get("content", "").lower()
content_b = mem_b.get("content", "").lower()

# 如果两条记忆内容高度相似,但创建时间差距大,可能是矛盾
time_a = mem_a.get("metadata", {}).get("created_at", "")
time_b = mem_b.get("metadata", {}).get("created_at", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Background contradiction scan compares every memory with every other one, freezing the maintenance run on large memory pools

Every stored memory is compared against every other stored memory (nested loop at maintenance/agents/analyst.py:336-337) without any size limit, tenant boundary, or cap on the produced results, so a large memory pool makes each nightly maintenance run take a huge amount of CPU and produce an enormous list of alleged contradictions.
Impact: On installations with many memories the scheduled cleanup can hog the process for a long time and generate an unbounded pile of items, and it also pairs up memories that belong to different users or chats.

Unbounded O(n²) scan, no scope grouping, unbounded output

AnalystAgent.run fetches every active memory via _get_active_memories (maintenance/agents/analyst.py:111-136), which pages through the whole knowledge base with no upper bound. _screen_link_candidates at least samples down to maintenance_analyst_batch_size (default 200) before its O(n²) similarity loop, but _detect_contradictions is called with the full memories list (maintenance/agents/analyst.py:104-107) and performs a nested Python loop with per-pair set(content.split()) construction. With e.g. 5000 memories that is ~12.5M pair comparisons plus tokenization per iteration, in the same event loop.

Secondly, unlike _screen_link_candidates (which groups by memory_scope / owner_user_id / owner_session_id / umo at maintenance/agents/analyst.py:254-280), _detect_contradictions applies no scope grouping, so it pairs memories belonging to different users/groups.

Thirdly, the resulting list has no cap; every contradiction becomes an operation in the analyst manifest (maintenance/runner.py:386-395) and the whole manifest is JSON-dumped into a single reviewer prompt (maintenance/agents/reviewer.py:104-108), which can blow up the LLM request size.

Prompt for agents
AnalystAgent._detect_contradictions (maintenance/agents/analyst.py) runs a nested loop over the complete active-memory list returned by _get_active_memories, which is unbounded (it pages the entire KB). Three problems: (1) O(n²) with per-pair tokenization makes the nightly maintenance cycle extremely expensive on large memory pools; (2) unlike _screen_link_candidates it does not group by memory_scope / owner_user_id / owner_session_id / umo, so it pairs memories across users and chats; (3) the returned list is uncapped and every entry becomes a manifest operation that is JSON-dumped into the reviewer prompt (maintenance/runner.py and maintenance/agents/reviewer.py), potentially producing a huge LLM request. Consider reusing the same sampling/scope-grouping used for link candidates, capping the number of contradictions returned, and ideally reusing the precomputed cosine matrix instead of word-overlap over all pairs.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

manifest["merge"].append(
{
"uris": [mem_a["uri"], mem_b["uri"]],
"merged_content": verdict.fused_text or mem_a["content"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Merging two memories can save a garbled, doubly-formatted copy of the memory text

When the model does not return a fused sentence, the merged memory falls back to the raw stored blob of one of the originals (mem_a["content"] at maintenance/agents/organizer.py:100), which already contains the storage formatting, so the newly created memory is saved with duplicated formatting lines instead of clean text.
Impact: The merged memory shown to the assistant and to users becomes a messy nested blob containing labels like "domain:"/"memory:" twice.

Stored text is the formatted blob, not the plain content

get_all_active_memories sets "content": doc.get("text", "") (memory_manager.py:2210-2216), i.e. the value produced by format_memory_content, which is a multi-line blob of the form:

domain: fact
memory: <real text>
recall_when: ...
topics: ...

When verdict.fused_text is empty the organizer proposes that whole blob as merged_content. MemoryManager.merge_memories then stores it as memory_content and runs format_memory_content(merged_content, new_metadata) again (memory_manager.py:2348-2364), yielding nested domain:/memory: headers. Display uses meta.memory_content (memory_protocol.py:248), so users/LLM see the nested blob.

A safer fallback is to skip the merge proposal when fused_text is empty, or to extract the plain content (e.g. metadata["memory_content"]) instead of the formatted document text.

Suggested change
"merged_content": verdict.fused_text or mem_a["content"],
"merged_content": verdict.fused_text
or mem_a.get("metadata", {}).get("memory_content", ""),
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread main.py
Comment on lines +758 to +763
marked = await mgr.mark_consolidated(source_uris)
if marked <= 0:
logger.debug("[简单长期记忆] 巩固:原文标记 0 条(摘要已写入)")
logger.info(
f"[简单长期记忆] 巩固 {len(candidates)} 条 → 1 条摘要,标记原文 {marked} 条"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Memory consolidation can keep creating duplicate summaries of the same old memories

The consolidated summary is now written first and the originals are only marked afterwards (mark_consolidated at main.py:758), so if that marking step fails the originals stay eligible and the same batch is summarised again on the next run.
Impact: Repeated background runs can pile up near-identical summary memories for the same old conversations.

Loss of the previous guard

The previous implementation marked the source memories deprecated+compressed first and aborted when marked <= 0, so a failed mark never produced a summary. The new order writes the summary first and then calls mark_consolidated; when it returns 0 (SQL update failed, all rows already changed, etc.) the code only logs at debug level (main.py:759-763) and still reports success. Since fetch_consolidation_candidates selects on deprecated IS NOT 1 AND compressed IS NOT 1, the same candidates return every CONSOLIDATION_INTERVAL (2h) and another summary is stored each time.

A compromise is to keep the new ordering but delete/deprecate the just-written summary (or record a marker) when marked <= 0, and to log at warning level.

Prompt for agents
In MemoryPlugin._maybe_consolidate_memories (main.py, ~lines 744-763) the ordering was flipped so the summary is stored before the source memories are marked consolidated. If mark_consolidated returns 0 the code only logs at debug level and reports success, but the sources remain non-deprecated/non-compressed, so fetch_consolidation_candidates will select the very same batch on the next scheduled run and store another duplicate summary indefinitely. Add a guard: on marked <= 0, either roll back / delete the just-created summary, or persist a marker so the same candidate set is not re-summarised, and raise the log level to warning.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread memory_manager.py
Comment on lines +2259 to +2278
old_doc_id = doc.get("doc_id", "")
try:
await self._register_kb_document(new_doc_id, uri, len(text))
except Exception:
pass
if old_doc_id:
try:
await self._unregister_kb_documents([old_doc_id])
except Exception:
pass
try:
await self.vec_db.delete_documents(
metadata_filters={"kb_doc_id": old_doc_id}
)
except Exception as del_err:
# 删除失败 → 原记录仍活跃,返回失败让调用方处理
logger.warning(
f"[简单长期记忆] deprecate 删除旧记录失败: {del_err}"
)
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 deprecate_memory can leave an active original plus a deprecated duplicate

deprecate_memory inserts a new document carrying the same uri with deprecated=True and only afterwards deletes the original by kb_doc_id. Two follow-ups:

  1. If doc.get("doc_id", "") is empty (the rest of the codebase consistently reads the id from metadata["kb_doc_id"], e.g. memory_manager.py:626, memory_manager.py:1817, and get_all_active_memories uses doc.get("id") at memory_manager.py:2204), the delete branch is skipped entirely and the function returns True while the original record is still active — the archive silently does nothing and a duplicate deprecated copy remains.
  2. If the delete throws, the function returns False but the freshly inserted deprecated duplicate (and its registered KB document) is not rolled back, leaving two rows for the same URI.

Worth confirming which key document_storage.get_documents() actually returns for the document id.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +159 to +199
max_rounds = self._config.get("context_max_rounds", 50)
max_chars = self._config.get("context_max_chars", 30000)

conv_mgr = self._context.conversation_manager
# 按 UMO 限定范围(owner_filter 中可能有 umo)
umo = (owner_filter or {}).get("umo")
conversations = await conv_mgr.get_conversations(unified_msg_origin=umo)

lines: list[str] = []
total_chars = 0
total_rounds = 0
for conv in conversations:
history_raw = getattr(conv, "history", None)
if not history_raw:
continue
try:
history = (
_json.loads(history_raw)
if isinstance(history_raw, str)
else history_raw
)
except Exception:
continue
if not isinstance(history, list):
continue
for entry in reversed(history): # 最新的在前
if total_rounds >= max_rounds or total_chars >= max_chars:
break
role = entry.get("role", "")
text = entry.get("content", "")
if not text or role not in ("user", "assistant"):
continue
line = f"[{role}]: {text}"
if total_chars + len(line) > max_chars:
break
lines.append(line)
total_chars += len(line)
total_rounds += 1
if total_rounds >= max_rounds or total_chars >= max_chars:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Analyst pulls conversation history for all sessions when no UMO filter is supplied

_get_conversation_history reads umo from owner_filter, but the runner always calls AnalystAgent.run() with no owner_filter (maintenance/runner.py:372), so conv_mgr.get_conversations(unified_msg_origin=None) is invoked. Depending on the AstrBot API this either errors (caught and returns "") or returns conversations across all sessions/users, which contradicts the spec requirement "对话历史拉取按 UMO 限定,不跨会话读取" (docs/maintenance-agent-spec.md). Also note context_max_age_days is declared in the schema but never applied here — only rounds and chars are enforced.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread maintenance/runner.py
Comment on lines +213 to +247
for i, op in enumerate(all_operations):
# 查找对应的审核结果
verdict = None
for v in report.reviewer_verdicts:
if v.get("index") == i:
verdict = v
break

# 缺失裁决 → fail closed,拒绝执行(避免 reviewer 故障时放行破坏性操作)
if verdict is None:
# additive 操作(new_link)无需审核可直接执行
if op.get("type") == "new_link":
try:
success = await self._execute_operation(op)
if success:
executed += 1
else:
failed += 1
report.errors.append(f"op[{i}] new_link 执行返回失败")
except Exception as e:
failed += 1
report.errors.append(f"op[{i}] new_link: {e}")
continue
# destructive 操作缺少裁决 → 转待审队列(reviewer 禁用时不静默丢弃)
await self._enqueue_pending_review(
op,
{"verdict": "pending", "reason": "reviewer 未启用或缺失裁决"},
report.session_id,
)
skipped += 1
logger.info(
f"[简单长期记忆] 操作 {i} 缺少审核裁决,转待审: {op.get('type')}"
)
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Reviewer verdicts are matched to operations purely by LLM-supplied index

Operations are collected in the same order for both the reviewer prompt (_run_reviewer) and the execution loop, and matching relies on the model echoing back a correct index. Any hallucinated/shifted index silently maps a verdict onto a different operation; an approve intended for a link could authorise a merge. The current fail-closed default (missing verdict → pending review) helps, but a mismatched-but-present index is not detected. Consider embedding a stable operation id in the prompt and validating it on return.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread memory_manager.py
Comment on lines +1575 to +1612
async def _disclosure_retrieve(
self, query: str, top_k: int, filters: dict[str, Any]
) -> list[dict[str, Any]]:
"""Disclosure 精确匹配检索通道。

查找 disclosure 字段与查询关键词有交集的记忆,
确保“当用户问 X 时”类型的触发条件不会被向量相似度漏掉。
"""
if not self._kb_helper:
return []

# 分词:优先用 AstrBot 分词器,不可用时 fallback 到空格分词
query_tokens = self._tokenize_query(query)
if not query_tokens:
query_tokens = [t for t in query.lower().split() if len(t) >= 2]
if not query_tokens:
return []

# 构建 SQL:在现有 filters 基础上追加 disclosure 非空条件
where_clause, params = self._filters_to_sql(filters)
where_clause += (
" AND json_extract(metadata,'$.disclosure') IS NOT NULL"
" AND json_extract(metadata,'$.disclosure') != ''"
)
# 分页扫描直到找到足够匹配或遍历完所有候选
try:
scan_limit = int(self.config.get("disclosure_scan_limit", 200))
except (TypeError, ValueError):
scan_limit = 200
scan_limit = max(10, min(scan_limit, 5000))

query_token_set = set(query_tokens)
matched: list[dict[str, Any]] = []
target_matches = min(top_k * 2, 10)
page_size = min(scan_limit, 200)
offset = 0

try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Disclosure channel adds a full table scan per recall filter

_disclosure_retrieve runs on every recall (there is no feature flag) and issues paged SELECT text, metadata FROM documents ... ORDER BY json_extract(metadata,'$.created_at') DESC queries — up to disclosure_scan_limit (default 200, max 5000) rows — once per filter in filters_list (which is 2–5 filters per recall). Since json_extract on an unindexed JSON column cannot use an index, each recall now performs several sorted scans over the documents table in the request path of every LLM call. Worth measuring on a large KB, and possibly gating it behind a config toggle like recall_sparse_fusion.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread memory_manager.py
Comment on lines +2194 to +2217
# 从 FAISS 索引加载向量(document_storage 不返回 vector 字段)
faiss_index = None
embedding_storage = getattr(self.vec_db, "embedding_storage", None)
if embedding_storage is not None:
faiss_index = getattr(embedding_storage, "index", None)

memories = []
for doc in docs:
metadata = _safe_parse_metadata(doc.get("metadata", {}))
vector = None
doc_int_id = doc.get("id")
if faiss_index is not None and doc_int_id is not None:
try:
vector = faiss_index.reconstruct(int(doc_int_id)).tolist()
except Exception:
pass # 向量不存在时跳过,organizer/analyst 会过滤
memories.append(
{
"uri": metadata.get("uri", ""),
"content": doc.get("text", ""),
"metadata": metadata,
"vector": vector,
}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Vector loading for candidate screening depends on FAISS reconstruct and a possibly wrong id field

get_all_active_memories reconstructs vectors via faiss_index.reconstruct(int(doc.get("id"))). This assumes (a) get_documents returns an id key holding the FAISS internal id and (b) the index type supports reconstruct (IVF/HNSW variants often require make_direct_map() first, and IDMap wrappers reconstruct by internal offset, not external id). Any mismatch is silently swallowed by the bare except: pass, in which case vector is None for every memory and both organizer and analyst screening return zero candidates — the whole maintenance pipeline becomes a silent no-op with no diagnostic. At minimum a debug log on repeated reconstruct failures would make this observable.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

piexian and others added 2 commits August 1, 2026 10:30
新增 MaintenanceRunner._op_lock 互斥锁,run_cycle 阶段 4 与
/memory review approve 共用同一锁执行操作;Scheduler 暴露
runner 属性,替换对私有成员的访问。
@piexian
piexian merged commit 7c065b9 into master Aug 11, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant