Skip to content

feat: 记忆召回反馈/加权/RRF融合/TTL过期/巩固 - #4

Merged
piexian merged 3 commits into
masterfrom
feat/memory-lifecycle
Jun 28, 2026
Merged

feat: 记忆召回反馈/加权/RRF融合/TTL过期/巩固#4
piexian merged 3 commits into
masterfrom
feat/memory-lifecycle

Conversation

@piexian

@piexian piexian commented Jun 28, 2026

Copy link
Copy Markdown
Owner

P0 召回反馈(recall_count 递增)+ 信号加权(importance/频次/时效)+ 稠密稀疏 RRF 融合;P1 TTL 过期标记 + 低频记忆 LLM 巩固为摘要;新增 9 项配置与中英 i18n

Summary by Sourcery

通过引入召回反馈、多信号重排、稠密/稀疏融合、基于 TTL 的生命周期管理,以及由 LLM 驱动的低频记忆整合,并配套相关配置与国际化更新,增强记忆系统。

New Features:

  • 通过在被召回的记忆上递增 recall_count 并更新 last_recalled_at 来跟踪记忆召回反馈。
  • 对被召回的记忆应用可配置的基于重要性/频率/新近性的加权策略进行重排序。
  • 通过互惠排序融合(Reciprocal Rank Fusion)将稠密向量检索与 FTS5 稀疏搜索融合,以提升召回相关性。
  • 为旧记忆增加基于 TTL 的过期机制,通过将其标记为弃用并从召回流程中排除。
  • 定期将旧的、低频的个人记忆整合为单条由 LLM 生成的摘要记忆。

Enhancements:

  • 引入原地更新元数据工具,用于修改记忆 JSON 元数据而不影响向量或 FTS 索引。
  • 使用知识库重排提供器集中处理融合后的重排序,以统一稠密/稀疏结果的排序逻辑。
  • 添加本地元数据过滤和查询分词辅助工具,以支持带过滤条件的稀疏检索。
  • 对后台 TTL 过期和整合任务进行节流,以避免影响请求延迟。

Documentation:

  • 添加一条英文记忆整合提示词,描述如何对旧的低频记忆进行总结。
Original summary in English

Summary by Sourcery

Enhance the memory system with recall feedback, multi-signal reranking, dense/sparse fusion, TTL-based lifecycle management, and LLM-driven consolidation of low-frequency memories, along with supporting config and i18n updates.

New Features:

  • Track memory recall feedback by incrementing recall_count and last_recalled_at for recalled memories.
  • Apply configurable importance/frequency/recency-based weighting to rerank recalled memories.
  • Fuse dense vector retrieval with FTS5 sparse search via Reciprocal Rank Fusion to improve recall relevance.
  • Add TTL-based expiration for old memories by marking them deprecated and excluding them from recall.
  • Periodically consolidate old, low-frequency personal memories into a single LLM-generated summary memory.

Enhancements:

  • Introduce metadata in-place update utilities to modify memory JSON metadata without touching vector or FTS indexes.
  • Centralize post-fusion reranking using the knowledge base rerank provider for combined dense/sparse results.
  • Add local metadata filtering and query tokenization helpers to support sparse retrieval with filters.
  • Throttle background TTL expiration and consolidation tasks to avoid impacting request latency.

Documentation:

  • Add an English memory consolidation prompt describing how to summarize old low-frequency memories.

P0 召回反馈(recall_count 递增)+ 信号加权(importance/频次/时效)+ 稠密稀疏 RRF 融合;P1 TTL 过期标记 + 低频记忆 LLM 巩固为摘要;新增 9 项配置与中英 i18n
@sourcery-ai

sourcery-ai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

实现带有每条记忆统计的召回反馈、按信号加权的稠密/稀疏 RRF 融合召回排序,以及后台 TTL 过期和通过 LLM 总结进行的低频记忆合并,并通过新的配置和 i18n 条目接入记忆注入生命周期。

带反馈的信号加权稠密/稀疏 RRF 召回的时序图

sequenceDiagram
    actor User
    participant MemoryPlugin
    participant MemoryManager
    participant VecDB
    participant DocumentStorage
    participant RerankProvider

    User->>MemoryPlugin: inject_memories(event, request)
    MemoryPlugin->>MemoryManager: recall_memories(event, query, top_k, bump=True)

    alt personal scope
        MemoryManager->>MemoryManager: _retrieve_with_filter(query, fetch_k, filters)
    else multi-scope
        MemoryManager->>MemoryManager: _build_recall_filters(...)
        MemoryManager->>MemoryManager: _retrieve_with_filter(query, fetch_k, filters_list[i]) *
    end

    activate MemoryManager
    MemoryManager->>VecDB: retrieve(query, k=top_k, rerank=False, metadata_filters)
    VecDB-->>MemoryManager: dense_results
    MemoryManager->>MemoryManager: _parse dense_results to dense_memories

    alt recall_sparse_fusion enabled
        MemoryManager->>MemoryManager: _sparse_retrieve(query, top_k, filters)
        activate MemoryManager
        MemoryManager->>DocumentStorage: search_sparse(query_tokens, limit)
        DocumentStorage-->>MemoryManager: sparse docs
        MemoryManager->>MemoryManager: _matches_filters(metadata, filters)
        MemoryManager->>MemoryManager: _rrf_fuse(dense_memories, sparse_memories, limit=top_k)
        deactivate MemoryManager
    else fallback
        MemoryManager->>MemoryManager: use dense_memories only
    end

    opt use_reranker
        MemoryManager->>RerankProvider: rerank(query, docs)
        RerankProvider-->>MemoryManager: reranked results
        MemoryManager->>MemoryManager: reorder memories by relevance_score
    end

    MemoryManager->>MemoryManager: _rerank_by_signal(memories)

    opt bump
        MemoryManager->>MemoryManager: _bump_recall_stats(recalled_uris)
        MemoryManager->>MemoryManager: _exec_metadata_update(set_clause, where_clause, params)
    end

    MemoryManager-->>MemoryPlugin: memories
    MemoryPlugin-->>User: inject recalled memories into request context
Loading

TTL 过期与基于 LLM 的记忆合并时序图

sequenceDiagram
    actor User
    participant MemoryPlugin
    participant MemoryManager
    participant VecDB
    participant DocumentStorage
    participant LLM

    User->>MemoryPlugin: inject_memories(event, request)
    MemoryPlugin->>MemoryPlugin: _maybe_expire_stale_memories()
    MemoryPlugin->>MemoryManager: _expire_stale_memories(ttl_days)
    MemoryManager->>MemoryManager: _exec_metadata_update(set_clause, where_clause, params)
    MemoryManager-->>MemoryPlugin: expired count

    MemoryPlugin->>MemoryPlugin: asyncio.create_task(_maybe_consolidate_memories(event))

    activate MemoryPlugin
    MemoryPlugin->>MemoryManager: _fetch_consolidation_candidates(min_age_days, max_recall, limit, owner_user_id, memory_scope=personal)
    MemoryManager->>VecDB: vec_db.document_storage
    MemoryManager->>DocumentStorage: get_session() / SELECT text, metadata FROM documents ...
    DocumentStorage-->>MemoryManager: candidate rows
    MemoryManager-->>MemoryPlugin: candidates

    MemoryPlugin->>MemoryPlugin: _get_llm_provider_id(event, "summarization" / "extraction")
    MemoryPlugin->>LLM: context.llm_generate(chat_provider_id, MEMORY_CONSOLIDATION_PROMPT)
    LLM-->>MemoryPlugin: summary text

    MemoryPlugin->>MemoryManager: _mark_consolidated(source_uris)
    MemoryManager->>MemoryManager: _exec_metadata_update(set_clause, where_clause, params)
    MemoryManager-->>MemoryPlugin: marked count

    MemoryPlugin->>MemoryManager: store_memory(event, summary, domain="consolidated", memory_type=CONTEXT, disclosure, importance=4, memory_scope=personal)
    MemoryManager-->>MemoryPlugin: stored consolidated memory
    deactivate MemoryPlugin

    MemoryPlugin-->>User: consolidation runs in background, user flow unaffected
Loading

File-Level Changes

Change Details Files
Add metadata in-place update utilities to support recall statistics, TTL expiration, and consolidation marking without touching vector/FTS indexes.
  • Introduce a generic _exec_metadata_update helper using raw SQL UPDATE on documents.metadata JSON to bypass missing update_metadata API.
  • Implement _bump_recall_stats to increment recall_count and update last_recalled_at for recalled memory URIs.
  • Add _expire_stale_memories to mark old memories as deprecated based on kb_id, created_at, and configurable TTL.
  • Add _fetch_consolidation_candidates to query low-frequency, old, uncompressed memories scoped by owner_user_id and memory_scope.
  • Add _mark_consolidated to mark original memories as deprecated and compressed after consolidation.
memory_manager.py
Enhance recall pipeline with signal-based re-ranking and dense/sparse RRF fusion, plus optional recall feedback bumping.
  • Add _rerank_by_signal to score memories using configurable weights over importance, recall_count frequency, and recency with exponential decay.
  • Refactor recall_memories to share single-user and multi-scope flows, apply signal re-ranking, and conditionally bump recall stats when bump=True.
  • Change _retrieve_with_filter to separate dense retrieval from reranking, add sparse FTS5 retrieval, and apply RRF fusion before optional reranker provider.
  • Introduce _sparse_retrieve using document_storage.search_sparse with local metadata filtering via _matches_filters.
  • Add _tokenize_query with cached AstrBot tokenizer and stopwords, and implement _rrf_fuse that fuses dense and sparse lists by URI using reciprocal rank fusion scores.
memory_manager.py
Introduce periodic TTL expiration and LLM-based consolidation of low-frequency memories, integrated into the injection lifecycle.
  • Add TTL_EXPIRE_INTERVAL and CONSOLIDATION_INTERVAL configuration constants and corresponding last-run timestamps on MemoryPlugin.
  • Implement _maybe_expire_stale_memories to periodically invoke _expire_stale_memories with throttling and configuration-based TTL.
  • Implement _maybe_consolidate_memories to periodically fetch consolidation candidates, summarize them via LLM, mark originals as deprecated+compressed, and store a new consolidated memory.
  • Wire lifecycle maintenance into inject_memories using a synchronous TTL pass and background consolidation task, and enable recall bumping via bump=True.
main.py
Add a dedicated prompt and configuration/i18n entries to support consolidation and new recall behaviors.
  • Define MEMORY_CONSOLIDATION_PROMPT describing consolidation instructions and output constraints.
  • Extend configuration schema to include recall weighting, recency halflife, sparse fusion toggle, TTL and consolidation parameters, and related options.
  • Update en-US and zh-CN i18n files with new configuration labels and descriptions for recall feedback, fusion, TTL, and consolidation features.
prompts.py
.astrbot-plugin/i18n/en-US.json
.astrbot-plugin/i18n/zh-CN.json
_conf_schema.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: 在 pull request 中评论 @sourcery-ai review
  • Continue discussions: 直接回复 Sourcery 的审查评论。
  • Generate a GitHub issue from a review comment: 通过回复审查评论,请 Sourcery 根据该评论创建一个 issue。你也可以在审查评论中回复 @sourcery-ai issue 来从该评论创建 issue。
  • Generate a pull request title: 在 pull request 标题的任意位置写上 @sourcery-ai,即可随时生成一个标题。你也可以在 pull request 中评论 @sourcery-ai title,在任何时候(重新)生成标题。
  • Generate a pull request summary: 在 pull request 正文的任意位置写上 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary,在任何时候(重新)生成摘要。
  • Generate reviewer's guide: 在 pull request 中评论 @sourcery-ai guide,在任何时候(重新)生成审查者指南。
  • Resolve all Sourcery comments: 在 pull request 中评论 @sourcery-ai resolve,以解决所有 Sourcery 评论。如果你已经处理完所有评论并且不想再看到它们,这会很有用。
  • Dismiss all Sourcery reviews: 在 pull request 中评论 @sourcery-ai dismiss,以关闭所有现有的 Sourcery 审查。特别适用于你想从一个新的审查重新开始——别忘了评论 @sourcery-ai review 来触发新的审查!

Customizing Your Experience

进入你的 dashboard 以:

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

Getting Help

Original review guide in English

Reviewer's Guide

Implements recall feedback with per-memory statistics, signal-weighted and dense/sparse RRF-fused recall ranking, plus background TTL expiration and low-frequency memory consolidation via LLM summaries, wired into the memory injection lifecycle with new configuration and i18n entries.

Sequence diagram for signal-weighted dense/sparse RRF recall with feedback

sequenceDiagram
    actor User
    participant MemoryPlugin
    participant MemoryManager
    participant VecDB
    participant DocumentStorage
    participant RerankProvider

    User->>MemoryPlugin: inject_memories(event, request)
    MemoryPlugin->>MemoryManager: recall_memories(event, query, top_k, bump=True)

    alt personal scope
        MemoryManager->>MemoryManager: _retrieve_with_filter(query, fetch_k, filters)
    else multi-scope
        MemoryManager->>MemoryManager: _build_recall_filters(...)
        MemoryManager->>MemoryManager: _retrieve_with_filter(query, fetch_k, filters_list[i]) *
    end

    activate MemoryManager
    MemoryManager->>VecDB: retrieve(query, k=top_k, rerank=False, metadata_filters)
    VecDB-->>MemoryManager: dense_results
    MemoryManager->>MemoryManager: _parse dense_results to dense_memories

    alt recall_sparse_fusion enabled
        MemoryManager->>MemoryManager: _sparse_retrieve(query, top_k, filters)
        activate MemoryManager
        MemoryManager->>DocumentStorage: search_sparse(query_tokens, limit)
        DocumentStorage-->>MemoryManager: sparse docs
        MemoryManager->>MemoryManager: _matches_filters(metadata, filters)
        MemoryManager->>MemoryManager: _rrf_fuse(dense_memories, sparse_memories, limit=top_k)
        deactivate MemoryManager
    else fallback
        MemoryManager->>MemoryManager: use dense_memories only
    end

    opt use_reranker
        MemoryManager->>RerankProvider: rerank(query, docs)
        RerankProvider-->>MemoryManager: reranked results
        MemoryManager->>MemoryManager: reorder memories by relevance_score
    end

    MemoryManager->>MemoryManager: _rerank_by_signal(memories)

    opt bump
        MemoryManager->>MemoryManager: _bump_recall_stats(recalled_uris)
        MemoryManager->>MemoryManager: _exec_metadata_update(set_clause, where_clause, params)
    end

    MemoryManager-->>MemoryPlugin: memories
    MemoryPlugin-->>User: inject recalled memories into request context
Loading

Sequence diagram for TTL expiration and LLM-based memory consolidation

sequenceDiagram
    actor User
    participant MemoryPlugin
    participant MemoryManager
    participant VecDB
    participant DocumentStorage
    participant LLM

    User->>MemoryPlugin: inject_memories(event, request)
    MemoryPlugin->>MemoryPlugin: _maybe_expire_stale_memories()
    MemoryPlugin->>MemoryManager: _expire_stale_memories(ttl_days)
    MemoryManager->>MemoryManager: _exec_metadata_update(set_clause, where_clause, params)
    MemoryManager-->>MemoryPlugin: expired count

    MemoryPlugin->>MemoryPlugin: asyncio.create_task(_maybe_consolidate_memories(event))

    activate MemoryPlugin
    MemoryPlugin->>MemoryManager: _fetch_consolidation_candidates(min_age_days, max_recall, limit, owner_user_id, memory_scope=personal)
    MemoryManager->>VecDB: vec_db.document_storage
    MemoryManager->>DocumentStorage: get_session() / SELECT text, metadata FROM documents ...
    DocumentStorage-->>MemoryManager: candidate rows
    MemoryManager-->>MemoryPlugin: candidates

    MemoryPlugin->>MemoryPlugin: _get_llm_provider_id(event, "summarization" / "extraction")
    MemoryPlugin->>LLM: context.llm_generate(chat_provider_id, MEMORY_CONSOLIDATION_PROMPT)
    LLM-->>MemoryPlugin: summary text

    MemoryPlugin->>MemoryManager: _mark_consolidated(source_uris)
    MemoryManager->>MemoryManager: _exec_metadata_update(set_clause, where_clause, params)
    MemoryManager-->>MemoryPlugin: marked count

    MemoryPlugin->>MemoryManager: store_memory(event, summary, domain="consolidated", memory_type=CONTEXT, disclosure, importance=4, memory_scope=personal)
    MemoryManager-->>MemoryPlugin: stored consolidated memory
    deactivate MemoryPlugin

    MemoryPlugin-->>User: consolidation runs in background, user flow unaffected
Loading

File-Level Changes

Change Details Files
Add metadata in-place update utilities to support recall statistics, TTL expiration, and consolidation marking without touching vector/FTS indexes.
  • Introduce a generic _exec_metadata_update helper using raw SQL UPDATE on documents.metadata JSON to bypass missing update_metadata API.
  • Implement _bump_recall_stats to increment recall_count and update last_recalled_at for recalled memory URIs.
  • Add _expire_stale_memories to mark old memories as deprecated based on kb_id, created_at, and configurable TTL.
  • Add _fetch_consolidation_candidates to query low-frequency, old, uncompressed memories scoped by owner_user_id and memory_scope.
  • Add _mark_consolidated to mark original memories as deprecated and compressed after consolidation.
memory_manager.py
Enhance recall pipeline with signal-based re-ranking and dense/sparse RRF fusion, plus optional recall feedback bumping.
  • Add _rerank_by_signal to score memories using configurable weights over importance, recall_count frequency, and recency with exponential decay.
  • Refactor recall_memories to share single-user and multi-scope flows, apply signal re-ranking, and conditionally bump recall stats when bump=True.
  • Change _retrieve_with_filter to separate dense retrieval from reranking, add sparse FTS5 retrieval, and apply RRF fusion before optional reranker provider.
  • Introduce _sparse_retrieve using document_storage.search_sparse with local metadata filtering via _matches_filters.
  • Add _tokenize_query with cached AstrBot tokenizer and stopwords, and implement _rrf_fuse that fuses dense and sparse lists by URI using reciprocal rank fusion scores.
memory_manager.py
Introduce periodic TTL expiration and LLM-based consolidation of low-frequency memories, integrated into the injection lifecycle.
  • Add TTL_EXPIRE_INTERVAL and CONSOLIDATION_INTERVAL configuration constants and corresponding last-run timestamps on MemoryPlugin.
  • Implement _maybe_expire_stale_memories to periodically invoke _expire_stale_memories with throttling and configuration-based TTL.
  • Implement _maybe_consolidate_memories to periodically fetch consolidation candidates, summarize them via LLM, mark originals as deprecated+compressed, and store a new consolidated memory.
  • Wire lifecycle maintenance into inject_memories using a synchronous TTL pass and background consolidation task, and enable recall bumping via bump=True.
main.py
Add a dedicated prompt and configuration/i18n entries to support consolidation and new recall behaviors.
  • Define MEMORY_CONSOLIDATION_PROMPT describing consolidation instructions and output constraints.
  • Extend configuration schema to include recall weighting, recency halflife, sparse fusion toggle, TTL and consolidation parameters, and related options.
  • Update en-US and zh-CN i18n files with new configuration labels and descriptions for recall feedback, fusion, TTL, and consolidation features.
prompts.py
.astrbot-plugin/i18n/en-US.json
.astrbot-plugin/i18n/zh-CN.json
_conf_schema.json

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

@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.

Hey - 我发现了 1 个问题,并给出了一些整体反馈:

  • 这个插件与 MemoryManager 的内部实现紧密耦合(例如调用 _expire_stale_memories_fetch_consolidation_candidates_mark_consolidated_current_owner_user_id),这会增加未来重构的难度;建议将这些提升为显式的公共方法,或者引入一个小的生命周期接口来替代对受保护成员的依赖。
  • _tokenize_query 中,一旦加载分词器失败,你会把结果缓存为 False 并且永远不再重试,即使发生了热重载或环境变化;如果这个对象在进程内是长生命周期的,建议增加基于时间的重试机制,或者提供一种重置缓存的方式,让稀疏检索能够从临时的初始化错误中恢复。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The plugin is tightly coupled to `MemoryManager` internals (e.g., calling `_expire_stale_memories`, `_fetch_consolidation_candidates`, `_mark_consolidated`, `_current_owner_user_id`), which makes future refactors harder; consider promoting these to explicit public methods or introducing a small lifecycle interface instead of relying on protected members.
- In `_tokenize_query`, once tokenizer loading fails you cache `False` and never retry, even after a hot reload or environment change; if this is meant to be long-lived within a process, consider adding a time-based retry or a way to reset the cache so sparse retrieval can recover from transient initialization errors.

## Individual Comments

### Comment 1
<location path="memory_manager.py" line_range="382-386" />
<code_context>
+        except Exception as e:
+            logger.warning(f"[简单长期记忆] 读取巩固候选失败: {e}")
+            return []
+        candidates: list[dict[str, Any]] = []
+        for row in rows:
+            text_val = row[0] if row else ""
+            meta = _safe_parse_metadata(row[1] if len(row) > 1 else {})
+            candidates.append({"text": text_val, "metadata": meta})
+        return candidates
+
</code_context>
<issue_to_address>
**suggestion:** 优先通过字段名而不是位置索引访问 SQLAlchemy 的行字段,以便在模式或查询发生变化时让代码更加健壮。

在 `_fetch_consolidation_candidates` 中,`row[0]``row[1]` 被用于 `text``metadata`,这会让行为与列顺序紧密绑定。请改用 `row.text` / `row.metadata`(或 `row._mapping['text']`),这样代码就能与 SELECT 子句保持一致,避免在查询变更时出现静默的字段错位。

建议实现:

```python
        except Exception as e:
            logger.warning(f"[简单长期记忆] 读取巩固候选失败: {e}")
            return []
        candidates: list[dict[str, Any]] = []
        for row in rows:
            # 使用字段名而不是位置索引,使代码在查询/列顺序变更时更健壮
            text_val = getattr(row, "text", "") or ""
            raw_metadata = getattr(row, "metadata", {}) or {}
            meta = _safe_parse_metadata(raw_metadata)
            candidates.append({"text": text_val, "metadata": meta})
        return candidates

```

1. 确认生成 `rows` 的查询 `SELECT` 子句中包含 `text``metadata` 字段名,与这里的属性访问保持一致。
2. 如果查询中使用了别名(例如 `SELECT text AS memory_text`),需要将这里的属性名改为对应的别名(如 `row.memory_text`)。
3. 在使用 SQLAlchemy 1.4+/2.0 的情况且 `row``Row` 对象时,上述代码依赖其支持属性访问;如果项目统一约定使用 `row._mapping["text"]` 访问字段,可将两行改为:
   - `text_val = row._mapping.get("text", "") or ""`
   - `raw_metadata = row._mapping.get("metadata", {}) or {}`
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • The plugin is tightly coupled to MemoryManager internals (e.g., calling _expire_stale_memories, _fetch_consolidation_candidates, _mark_consolidated, _current_owner_user_id), which makes future refactors harder; consider promoting these to explicit public methods or introducing a small lifecycle interface instead of relying on protected members.
  • In _tokenize_query, once tokenizer loading fails you cache False and never retry, even after a hot reload or environment change; if this is meant to be long-lived within a process, consider adding a time-based retry or a way to reset the cache so sparse retrieval can recover from transient initialization errors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The plugin is tightly coupled to `MemoryManager` internals (e.g., calling `_expire_stale_memories`, `_fetch_consolidation_candidates`, `_mark_consolidated`, `_current_owner_user_id`), which makes future refactors harder; consider promoting these to explicit public methods or introducing a small lifecycle interface instead of relying on protected members.
- In `_tokenize_query`, once tokenizer loading fails you cache `False` and never retry, even after a hot reload or environment change; if this is meant to be long-lived within a process, consider adding a time-based retry or a way to reset the cache so sparse retrieval can recover from transient initialization errors.

## Individual Comments

### Comment 1
<location path="memory_manager.py" line_range="382-386" />
<code_context>
+        except Exception as e:
+            logger.warning(f"[简单长期记忆] 读取巩固候选失败: {e}")
+            return []
+        candidates: list[dict[str, Any]] = []
+        for row in rows:
+            text_val = row[0] if row else ""
+            meta = _safe_parse_metadata(row[1] if len(row) > 1 else {})
+            candidates.append({"text": text_val, "metadata": meta})
+        return candidates
+
</code_context>
<issue_to_address>
**suggestion:** Prefer accessing SQLAlchemy row fields by name instead of positional indices to make the code more robust to schema/query changes.

In `_fetch_consolidation_candidates`, `row[0]` and `row[1]` are used for `text` and `metadata`, which couples behavior to column order. Please switch to `row.text` / `row.metadata` (or `row._mapping['text']`) so the code tracks the SELECT clause and avoids silent misalignment if the query changes.

Suggested implementation:

```python
        except Exception as e:
            logger.warning(f"[简单长期记忆] 读取巩固候选失败: {e}")
            return []
        candidates: list[dict[str, Any]] = []
        for row in rows:
            # 使用字段名而不是位置索引,使代码在查询/列顺序变更时更健壮
            text_val = getattr(row, "text", "") or ""
            raw_metadata = getattr(row, "metadata", {}) or {}
            meta = _safe_parse_metadata(raw_metadata)
            candidates.append({"text": text_val, "metadata": meta})
        return candidates

```

1. 确认生成 `rows` 的查询 `SELECT` 子句中包含 `text``metadata` 字段名,与这里的属性访问保持一致。
2. 如果查询中使用了别名(例如 `SELECT text AS memory_text`),需要将这里的属性名改为对应的别名(如 `row.memory_text`)。
3. 在使用 SQLAlchemy 1.4+/2.0 的情况且 `row``Row` 对象时,上述代码依赖其支持属性访问;如果项目统一约定使用 `row._mapping["text"]` 访问字段,可将两行改为:
   - `text_val = row._mapping.get("text", "") or ""`
   - `raw_metadata = row._mapping.get("metadata", {}) or {}`
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread memory_manager.py

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a memory lifecycle management and consolidation system, featuring TTL-based memory expiration, LLM-driven memory consolidation, and hybrid search fusing dense vector search with FTS5 sparse retrieval via Reciprocal Rank Fusion (RRF). The review feedback highlights several critical issues, including an invalid MemoryType.CONTEXT reference that will cause an AttributeError, potential garbage collection of unreferenced background tasks created via asyncio.create_task, and potential runtime crashes (ValueError, TypeError, or OverflowError) during signal-based re-ranking. Additionally, it suggests optimizing SQLite queries by simplifying nested json_set calls.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread main.py Outdated
Comment thread main.py Outdated
Comment thread memory_manager.py
Comment thread memory_manager.py
Comment on lines +297 to +301
set_clause = (
"json_set(json_set(metadata, '$.recall_count', "
"CAST(COALESCE(json_extract(metadata,'$.recall_count'),0) AS INTEGER) + 1), "
"'$.last_recalled_at', :now)"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

SQLite 的 json_set 函数原生支持传入多组 pathvalue 参数。使用嵌套的 json_set(json_set(...)) 会导致 SQLite 内部进行多次 JSON 解析和序列化,影响性能。建议将其简化为单次 json_set 调用,以提高 SQL 执行效率和代码可读性。

Suggested change
set_clause = (
"json_set(json_set(metadata, '$.recall_count', "
"CAST(COALESCE(json_extract(metadata,'$.recall_count'),0) AS INTEGER) + 1), "
"'$.last_recalled_at', :now)"
)
set_clause = (
"json_set(metadata, "
"'$.recall_count', CAST(COALESCE(json_extract(metadata,'$.recall_count'),0) AS INTEGER) + 1, "
"'$.last_recalled_at', :now)"
)

Comment thread memory_manager.py Outdated

@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 6 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread main.py Outdated
Comment thread memory_manager.py
Comment thread main.py Outdated
Comment thread memory_manager.py
Comment thread memory_manager.py
Comment thread memory_manager.py
return 0
kb_id = self._kb_helper.kb.kb_id
cutoff_iso = (datetime.now(timezone.utc) - timedelta(days=ttl_days)).isoformat()
set_clause = "json_set(metadata, '$.deprecated', 1)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Deprecated field type inconsistency: boolean False vs integer 1

Memory records are created with deprecated: False (Python bool) in _build_memory_metadata at memory_manager.py:865. But _expire_stale_memories at memory_manager.py:320 and _mark_consolidated at memory_manager.py:395 use json_set(metadata, '$.deprecated', 1) which stores integer 1. In SQLite JSON, false (boolean) and 0 (integer) are distinct types; similarly true and 1 are distinct. The recall filters at memory_manager.py:674 use deprecated: False which is passed to the vec_db's metadata filter. Whether deprecated: False matches records with deprecated: 0 (JSON false) but not deprecated: 1 (JSON integer) depends on the vec_db filter implementation. In practice, most implementations treat these as equivalent via truthiness checks, but this type mismatch is fragile and could cause issues if the filter implementation changes.

Open in Devin Review

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

piexian added 2 commits June 28, 2026 20:05
- MemoryType.CONTEXT→NORMAL(原不存在,巩固必崩+原文已mark无摘要=数据丢失)
- asyncio.create_task 加 _background_tasks 强引用防 GC(LLM 长任务)
- _rerank 数值安全转换+max(0,Δt) 防 int() 崩溃与 exp 溢出
- TTL 排除 permanent/global 记忆(管理员全局记忆不再被误过期)
- domain consolidated→context(进白名单,不再回退 facts)
- row 字段名访问(getattr)+ 嵌套 json_set 改单次调用
expire_stale_memories/fetch_consolidation_candidates/mark_consolidated 去掉 _ 前缀升为公共 API;fetch_consolidation_candidates 改接 event,owner 由内部 _current_owner_user_id 推导(main.py 不再跨边界调受保护成员)。回应 PR#4 sourcery 反馈。
@piexian
piexian merged commit 7a6862d into master Jun 28, 2026
4 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