feat(memory): 引入群聊记忆作用域(personal/group/conversation)及可见性模型#2
Conversation
- 新增三层记忆作用域:personal(用户隔离)、group(群共享)、conversation(会话临时) - 新增可见性模型:private(仅所有者)/ group(群内共享),多所有者自动升级 - memory_protocol: 新增 MemoryScope 枚举、build_session_id、MemoryMetadata 扩展字段 - memory_manager: 作用域感知召回(复合过滤器链)、可见性判断、旧格式兼容、去重 - main: 提取 prompt 加入作用域信息、sender 追踪、scope/subject/entities/topics 解析 - 群聊召回自动合并 personal + group + conversation 三层记忆,私聊仅召回 personal - 版本升至 v0.3.0
- 新增 prompts.py:集中管理 MEMORY_EXTRACTION_PROMPT、RECALL_QUERY_PROMPT、 sanitize_memory_content()、SENSITIVE_PATTERNS 及提取上限常量 - main.py:导入 prompts 模块,新增 _validate_command() 统一命令参数校验, 简化 _strip_json_fence/_normalize_contexts 等工具函数 - 新增 GitHub Actions CI:ruff lint + format check + 语法编译 + metadata 校验 - extraction_min_content_length 默认值 500 → 150 - metadata.yaml 清理尾部空格
审阅者指南为聊天记忆引入作用域化的内存与可见性模型(个人 / 群组 / 会话),让存储 / 召回 / 列表逻辑具备作用域感知并保持向后兼容,丰富记忆元数据与格式,同时重构提示词 / 命令处理,并更新配置与 CI 以适配 v0.3.0。 群聊中具备作用域感知的记忆召回时序图sequenceDiagram
actor User
participant AstrBot
participant SimpleLongMemoryPlugin as PluginMain
participant MemoryManager
participant VecDB
User->>AstrBot: Send message in group chat
AstrBot->>PluginMain: on_message(event)
PluginMain->>MemoryManager: recall_memories(event, query, domain, top_k, all_users=false, memory_scope=None)
activate MemoryManager
MemoryManager->>MemoryManager: _build_recall_filters(event, global_memory, domain, memory_scope=None)
Note over MemoryManager: Determine current_user_id
MemoryManager->>MemoryManager: _scope_filter(scope=personal)
MemoryManager->>MemoryManager: _legacy_personal_filter()
MemoryManager->>MemoryManager: _scope_filter(scope=group)
MemoryManager->>MemoryManager: _scope_filter(scope=conversation)
loop For each filter in filters_list
MemoryManager->>VecDB: retrieve(query, top_k, filters)
VecDB-->>MemoryManager: results[] with metadata
MemoryManager->>MemoryManager: _retrieve_with_filter(..., legacy_personal, owner_user_id, require_owner_list)
MemoryManager->>MemoryManager: _is_visible_personal_memory(metadata, owner_user_id, require_owner_list)
MemoryManager-->>MemoryManager: visible_memories_for_filter
end
MemoryManager->>MemoryManager: _dedupe_memories(all_visible_memories)
MemoryManager-->>PluginMain: memories (personal + group + conversation)
deactivate MemoryManager
PluginMain->>PluginMain: format_memory_for_injection(memories)
PluginMain-->>AstrBot: augmented prompt with grouped memories
AstrBot-->>User: LLM reply with injected context
带作用域与所有权的基于 LLM 的记忆抽取时序图sequenceDiagram
actor User
participant AstrBot
participant SimpleLongMemoryPlugin as PluginMain
participant MemoryManager
participant LLM
User->>AstrBot: Chat message
AstrBot->>PluginMain: on_llm_response(event, response)
PluginMain->>PluginMain: _accumulate_request_snapshot(event, request)
PluginMain->>PluginMain: _build_conversation_from_snapshots(event)
PluginMain->>LLM: MEMORY_EXTRACTION_PROMPT with platform_id, session_type, session_id, sender_id, conversation
LLM-->>PluginMain: extraction_result JSON
PluginMain->>PluginMain: _parse_extracted_memories(extraction_result, session_type)
Note over PluginMain: For each item
PluginMain->>PluginMain: _normalize_extracted_scope(scope, session_type)
PluginMain->>PluginMain: _normalize_subject_ids(subject or subjects)
PluginMain->>PluginMain: _sanitize_string_list(entities, topics)
loop For each validated memory
PluginMain->>PluginMain: choose subject and subjects
PluginMain->>PluginMain: owner_sender_ids = subjects if scope==personal else []
PluginMain->>MemoryManager: store_memory(event, content, domain, memory_type, disclosure, importance, memory_scope=scope, subject=subject, entities=entities, topics=topics, owner_sender_ids=owner_sender_ids)
activate MemoryManager
MemoryManager->>MemoryManager: _event_scope_ids(event, owner_sender_ids[0] or sender)
MemoryManager->>MemoryManager: _build_owner_user_ids(platform_id, owner_sender_ids)
MemoryManager->>MemoryManager: _build_memory_metadata(...)
MemoryManager->>VecDB: add_documents with metadata(memory_scope, owner_user_ids, owner_session_id, visibility, speaker_id, subject, entities, topics, memory_content)
deactivate MemoryManager
end
PluginMain-->>AstrBot: extraction finished
AstrBot-->>User: continues conversation
作用域化内存模型与元数据的类图classDiagram
class MemoryDomain {
<<enumeration>>
USER_PROFILE : str
PREFERENCES : str
FACTS : str
EVENTS : str
CONTEXT : str
}
class MemoryScope {
<<enumeration>>
PERSONAL : str
GROUP : str
CONVERSATION : str
}
class MemoryVisibility {
<<enumeration>>
PRIVATE : str
GROUP : str
}
class MemoryMetadata {
+str uri
+str domain
+str user_id
+str platform_id
+str sender_id
+str umo
+str session_type
+str session_id
+str created_at
+str last_recalled_at
+int recall_count
+int importance
+bool compressed
+str memory_scope
+str owner_user_id
+list~str~ owner_user_ids
+str owner_session_id
+str visibility
+str speaker_id
+str subject
+list~str~ entities
+list~str~ topics
+str memory_content
+str impression
+str migrated_from
+str migrated_to
+to_dict() dict~str, Any~
+from_dict(data dict~str, Any~) MemoryMetadata
}
class MemoryManager {
+dict config
+store_memory(event AstrMessageEvent, content str, domain str, memory_type str, disclosure str, importance int, memory_scope str, visibility str, subject str, entities list~str~, topics list~str~, owner_sender_id str, owner_sender_ids list~str~) str
+recall_memories(event AstrMessageEvent, query str, domain str, top_k int, all_users bool, memory_scope str) list~dict~
+list_memories(event AstrMessageEvent, domain str, page int, page_size int, all_users bool) tuple
+get_memory_by_uri(event AstrMessageEvent, uri str) dict~str, Any~
+forget_memory(event AstrMessageEvent, uri str) bool
-_event_scope_ids(event AstrMessageEvent, owner_sender_id str) tuple~UMOInfo, str, str~
-_build_owner_user_ids(platform_id str, owner_sender_ids list~str~) list~str~
-_scope_filter(event AstrMessageEvent, memory_scope str, global_memory bool) dict~str, Any~
-_legacy_personal_filter(event AstrMessageEvent, global_memory bool) dict~str, Any~
-_build_recall_filters(event AstrMessageEvent, global_memory bool, domain str, memory_scope str) list~tuple~dict~str, Any~, bool, str, bool~~
-_retrieve_with_filter(query str, top_k int, filters dict~str, Any~, legacy_personal bool, owner_user_id str, require_owner_list bool) list~dict~
-_is_visible_personal_memory(metadata dict~str, Any~, owner_user_id str, require_owner_list bool) bool
-_dedupe_memories(memories list~dict~) list~dict~
-_list_visible_user_documents(event AstrMessageEvent, domain str, page int, page_size int) list~dict~
-_memory_list_scan_limit(page int, page_size int) int
}
class MemoryProtocolUtils {
+normalize_memory_scope(scope str) str
+build_user_id(platform_id str, sender_id str) str
+build_session_id(platform_id str, session_id str) str
+format_memory_content(content str, metadata MemoryMetadata) str
+format_memory_for_injection(memories list~dict~, max_length int) str
+format_memory_for_user(memories list~dict~, page int, page_size int, total int) str
}
MemoryMetadata --> MemoryScope : uses
MemoryMetadata --> MemoryVisibility : uses
MemoryMetadata --> MemoryDomain : uses
MemoryManager --> MemoryMetadata : stores
MemoryManager --> MemoryScope : filters_by
MemoryManager --> MemoryVisibility : checks
MemoryManager --> MemoryProtocolUtils : calls
MemoryProtocolUtils --> MemoryMetadata : constructs
MemoryProtocolUtils --> MemoryScope : reads
MemoryProtocolUtils --> MemoryDomain : formats
MemoryProtocolUtils --> MemoryVisibility : formats
文件级变更
提示与命令与 Sourcery 交互
自定义你的体验访问你的 控制面板 以:
获取帮助Original review guide in EnglishReviewer's GuideIntroduce a scoped memory & visibility model (personal/group/conversation) for chat memories, make storage/recall/listing logic scope-aware with backwards compatibility, enrich memory metadata and formatting, and refactor prompts/command handling plus config & CI updates for v0.3.0. Sequence diagram for scope-aware memory recall in group chatsequenceDiagram
actor User
participant AstrBot
participant SimpleLongMemoryPlugin as PluginMain
participant MemoryManager
participant VecDB
User->>AstrBot: Send message in group chat
AstrBot->>PluginMain: on_message(event)
PluginMain->>MemoryManager: recall_memories(event, query, domain, top_k, all_users=false, memory_scope=None)
activate MemoryManager
MemoryManager->>MemoryManager: _build_recall_filters(event, global_memory, domain, memory_scope=None)
Note over MemoryManager: Determine current_user_id
MemoryManager->>MemoryManager: _scope_filter(scope=personal)
MemoryManager->>MemoryManager: _legacy_personal_filter()
MemoryManager->>MemoryManager: _scope_filter(scope=group)
MemoryManager->>MemoryManager: _scope_filter(scope=conversation)
loop For each filter in filters_list
MemoryManager->>VecDB: retrieve(query, top_k, filters)
VecDB-->>MemoryManager: results[] with metadata
MemoryManager->>MemoryManager: _retrieve_with_filter(..., legacy_personal, owner_user_id, require_owner_list)
MemoryManager->>MemoryManager: _is_visible_personal_memory(metadata, owner_user_id, require_owner_list)
MemoryManager-->>MemoryManager: visible_memories_for_filter
end
MemoryManager->>MemoryManager: _dedupe_memories(all_visible_memories)
MemoryManager-->>PluginMain: memories (personal + group + conversation)
deactivate MemoryManager
PluginMain->>PluginMain: format_memory_for_injection(memories)
PluginMain-->>AstrBot: augmented prompt with grouped memories
AstrBot-->>User: LLM reply with injected context
Sequence diagram for LLM-based memory extraction with scope and ownershipsequenceDiagram
actor User
participant AstrBot
participant SimpleLongMemoryPlugin as PluginMain
participant MemoryManager
participant LLM
User->>AstrBot: Chat message
AstrBot->>PluginMain: on_llm_response(event, response)
PluginMain->>PluginMain: _accumulate_request_snapshot(event, request)
PluginMain->>PluginMain: _build_conversation_from_snapshots(event)
PluginMain->>LLM: MEMORY_EXTRACTION_PROMPT with platform_id, session_type, session_id, sender_id, conversation
LLM-->>PluginMain: extraction_result JSON
PluginMain->>PluginMain: _parse_extracted_memories(extraction_result, session_type)
Note over PluginMain: For each item
PluginMain->>PluginMain: _normalize_extracted_scope(scope, session_type)
PluginMain->>PluginMain: _normalize_subject_ids(subject or subjects)
PluginMain->>PluginMain: _sanitize_string_list(entities, topics)
loop For each validated memory
PluginMain->>PluginMain: choose subject and subjects
PluginMain->>PluginMain: owner_sender_ids = subjects if scope==personal else []
PluginMain->>MemoryManager: store_memory(event, content, domain, memory_type, disclosure, importance, memory_scope=scope, subject=subject, entities=entities, topics=topics, owner_sender_ids=owner_sender_ids)
activate MemoryManager
MemoryManager->>MemoryManager: _event_scope_ids(event, owner_sender_ids[0] or sender)
MemoryManager->>MemoryManager: _build_owner_user_ids(platform_id, owner_sender_ids)
MemoryManager->>MemoryManager: _build_memory_metadata(...)
MemoryManager->>VecDB: add_documents with metadata(memory_scope, owner_user_ids, owner_session_id, visibility, speaker_id, subject, entities, topics, memory_content)
deactivate MemoryManager
end
PluginMain-->>AstrBot: extraction finished
AstrBot-->>User: continues conversation
Class diagram for scoped memory model and metadataclassDiagram
class MemoryDomain {
<<enumeration>>
USER_PROFILE : str
PREFERENCES : str
FACTS : str
EVENTS : str
CONTEXT : str
}
class MemoryScope {
<<enumeration>>
PERSONAL : str
GROUP : str
CONVERSATION : str
}
class MemoryVisibility {
<<enumeration>>
PRIVATE : str
GROUP : str
}
class MemoryMetadata {
+str uri
+str domain
+str user_id
+str platform_id
+str sender_id
+str umo
+str session_type
+str session_id
+str created_at
+str last_recalled_at
+int recall_count
+int importance
+bool compressed
+str memory_scope
+str owner_user_id
+list~str~ owner_user_ids
+str owner_session_id
+str visibility
+str speaker_id
+str subject
+list~str~ entities
+list~str~ topics
+str memory_content
+str impression
+str migrated_from
+str migrated_to
+to_dict() dict~str, Any~
+from_dict(data dict~str, Any~) MemoryMetadata
}
class MemoryManager {
+dict config
+store_memory(event AstrMessageEvent, content str, domain str, memory_type str, disclosure str, importance int, memory_scope str, visibility str, subject str, entities list~str~, topics list~str~, owner_sender_id str, owner_sender_ids list~str~) str
+recall_memories(event AstrMessageEvent, query str, domain str, top_k int, all_users bool, memory_scope str) list~dict~
+list_memories(event AstrMessageEvent, domain str, page int, page_size int, all_users bool) tuple
+get_memory_by_uri(event AstrMessageEvent, uri str) dict~str, Any~
+forget_memory(event AstrMessageEvent, uri str) bool
-_event_scope_ids(event AstrMessageEvent, owner_sender_id str) tuple~UMOInfo, str, str~
-_build_owner_user_ids(platform_id str, owner_sender_ids list~str~) list~str~
-_scope_filter(event AstrMessageEvent, memory_scope str, global_memory bool) dict~str, Any~
-_legacy_personal_filter(event AstrMessageEvent, global_memory bool) dict~str, Any~
-_build_recall_filters(event AstrMessageEvent, global_memory bool, domain str, memory_scope str) list~tuple~dict~str, Any~, bool, str, bool~~
-_retrieve_with_filter(query str, top_k int, filters dict~str, Any~, legacy_personal bool, owner_user_id str, require_owner_list bool) list~dict~
-_is_visible_personal_memory(metadata dict~str, Any~, owner_user_id str, require_owner_list bool) bool
-_dedupe_memories(memories list~dict~) list~dict~
-_list_visible_user_documents(event AstrMessageEvent, domain str, page int, page_size int) list~dict~
-_memory_list_scan_limit(page int, page_size int) int
}
class MemoryProtocolUtils {
+normalize_memory_scope(scope str) str
+build_user_id(platform_id str, sender_id str) str
+build_session_id(platform_id str, session_id str) str
+format_memory_content(content str, metadata MemoryMetadata) str
+format_memory_for_injection(memories list~dict~, max_length int) str
+format_memory_for_user(memories list~dict~, page int, page_size int, total int) str
}
MemoryMetadata --> MemoryScope : uses
MemoryMetadata --> MemoryVisibility : uses
MemoryMetadata --> MemoryDomain : uses
MemoryManager --> MemoryMetadata : stores
MemoryManager --> MemoryScope : filters_by
MemoryManager --> MemoryVisibility : checks
MemoryManager --> MemoryProtocolUtils : calls
MemoryProtocolUtils --> MemoryMetadata : constructs
MemoryProtocolUtils --> MemoryScope : reads
MemoryProtocolUtils --> MemoryDomain : formats
MemoryProtocolUtils --> MemoryVisibility : formats
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - 我发现了 1 个问题,并给出了一些整体反馈:
- 在
_list_visible_user_documents中,你对个人和群组查询都硬编码了limit=10000;建议将其做成可配置项,或者从page_size推导出来,以避免在大数据集上出现潜在的性能或内存问题。 - 可见性值(
"private","group")在多个地方以原始字符串的形式使用(例如normalize_visibility,_build_recall_filters,_list_visible_user_documents);引入一个小型的 Visibility 枚举或类似MemoryScope的常量,可以降低拼写错误的风险并提升可读性。
提供给 AI Agent 的提示词
Please address the comments from this code review:
## Overall Comments
- 在 `_list_visible_user_documents` 中,你对个人和群组查询都硬编码了 `limit=10000`;建议将其做成可配置项,或者从 `page_size` 推导出来,以避免在大数据集上出现潜在的性能或内存问题。
- 可见性值(`"private"`, `"group"`)在多个地方以原始字符串的形式使用(例如 `normalize_visibility`, `_build_recall_filters`, `_list_visible_user_documents`);引入一个小型的 Visibility 枚举或类似 `MemoryScope` 的常量,可以降低拼写错误的风险并提升可读性。
## Individual Comments
### Comment 1
<location path="main.py" line_range="653-661" />
<code_context>
+ DEFAULT_RECALL_QUERY_OPTIMIZATION_TIMEOUT,
+ )
+ )
+ llm_response = await asyncio.wait_for(
+ self.context.llm_generate(
+ provider_id=provider_id,
</code_context>
<issue_to_address>
**issue (bug_risk):** 超时处理可能有问题,因为 `TimeoutError` 不会捕获 `asyncio.wait_for` 抛出的超时异常。
由于这里使用了 `asyncio.wait_for(...)`,但只捕获内置的 `TimeoutError`,因此该 `except` 代码块实际上永远不会被触发,你会直接落入后面的通用 `Exception` 处理分支。如果没有导入 `TimeoutError`,第一次超时时还会抛出 `NameError`。请显式捕获 `asyncio.TimeoutError`(并确保已导入),或者如果你确实希望处理两种异常,则有意地同时处理这两种异常类型。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的审查结果。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- In
_list_visible_user_documentsyou hard-codelimit=10000for both personal and group queries; consider making this configurable or deriving it frompage_sizeto avoid potential performance or memory issues on large datasets. - The visibility values (
"private","group") are used as raw strings in multiple places (e.g.normalize_visibility,_build_recall_filters,_list_visible_user_documents); introducing a small Visibility enum or constants similar toMemoryScopewould reduce the risk of typos and improve readability.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `_list_visible_user_documents` you hard-code `limit=10000` for both personal and group queries; consider making this configurable or deriving it from `page_size` to avoid potential performance or memory issues on large datasets.
- The visibility values (`"private"`, `"group"`) are used as raw strings in multiple places (e.g. `normalize_visibility`, `_build_recall_filters`, `_list_visible_user_documents`); introducing a small Visibility enum or constants similar to `MemoryScope` would reduce the risk of typos and improve readability.
## Individual Comments
### Comment 1
<location path="main.py" line_range="653-661" />
<code_context>
+ DEFAULT_RECALL_QUERY_OPTIMIZATION_TIMEOUT,
+ )
+ )
+ llm_response = await asyncio.wait_for(
+ self.context.llm_generate(
+ provider_id=provider_id,
</code_context>
<issue_to_address>
**issue (bug_risk):** Timeout handling is likely broken because `TimeoutError` won’t catch `asyncio.wait_for` timeouts.
Because this uses `asyncio.wait_for(...)` but catches the built-in `TimeoutError`, that `except` block will never run and you’ll fall through to the generic `Exception` handler instead. If `TimeoutError` isn’t imported, it will also raise `NameError` on the first timeout. Please catch `asyncio.TimeoutError` explicitly (and import it) or deliberately handle both exception types if that’s what you intend.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
This pull request introduces a three-layer memory scope model (personal, group, and conversation) to support group chat scenarios, along with enhanced metadata tracking for subjects, entities, and topics. Key updates include scope-aware recall logic, visibility controls, and a configurable timeout for retrieval optimization. Feedback highlights several improvement opportunities: ensuring the conversation scope is included in private chat recall, correcting visibility logic for group-shared memories, and parallelizing database retrievals using asyncio.gather to reduce latency. Additionally, it is recommended to move document filtering to the database level to prevent performance issues and to remove non-semantic metadata from vector text to enhance retrieval quality.
| scopes = ( | ||
| [normalize_memory_scope(memory_scope)] | ||
| if memory_scope | ||
| else [MemoryScope.PERSONAL] | ||
| ) | ||
| if not memory_scope and parsed.session_type == "group": | ||
| scopes.extend([MemoryScope.GROUP, MemoryScope.CONVERSATION]) | ||
| elif not memory_scope and not global_memory: | ||
| scopes.append(MemoryScope.CONVERSATION) |
There was a problem hiding this comment.
In private chats (parsed.session_type != "group"), if global_memory is enabled (default), the CONVERSATION scope is currently excluded from the default recall list. This means temporary conversation-specific context will be lost in private chats unless global_memory is disabled. CONVERSATION scope should likely be included in the default recall list for both group and private chats.
| scopes = ( | |
| [normalize_memory_scope(memory_scope)] | |
| if memory_scope | |
| else [MemoryScope.PERSONAL] | |
| ) | |
| if not memory_scope and parsed.session_type == "group": | |
| scopes.extend([MemoryScope.GROUP, MemoryScope.CONVERSATION]) | |
| elif not memory_scope and not global_memory: | |
| scopes.append(MemoryScope.CONVERSATION) | |
| scopes = ( | |
| [normalize_memory_scope(memory_scope)] | |
| if memory_scope | |
| else [MemoryScope.PERSONAL] | |
| ) | |
| if not memory_scope: | |
| if parsed.session_type == "group": | |
| scopes.extend([MemoryScope.GROUP, MemoryScope.CONVERSATION]) | |
| else: | |
| scopes.append(MemoryScope.CONVERSATION) |
| def _is_visible_personal_memory( | ||
| self, | ||
| metadata: dict[str, Any], | ||
| owner_user_id: str | None, | ||
| require_owner_list: bool = False, | ||
| ) -> bool: | ||
| scope = metadata.get("memory_scope") | ||
| if scope not in (None, "", MemoryScope.PERSONAL): | ||
| return False | ||
| owner_user_ids = metadata.get("owner_user_ids") | ||
| if isinstance(owner_user_ids, list) and owner_user_ids: | ||
| return owner_user_id in owner_user_ids if owner_user_id else True | ||
| if require_owner_list: | ||
| return False | ||
| return metadata.get("owner_user_id", metadata.get("user_id")) == owner_user_id |
There was a problem hiding this comment.
The visibility logic in _is_visible_personal_memory appears to restrict group-shared memories (visibility: "group") to only those users listed in owner_user_ids. This contradicts the goal of making group memories visible to all members of the group. If a memory is marked with visibility: "group", it should be considered visible to the current user if the session filter (already applied at the DB level) matches.
def _is_visible_personal_memory(
self,
metadata: dict[str, Any],
owner_user_id: str | None,
require_owner_list: bool = False,
) -> bool:
scope = metadata.get("memory_scope")
if scope not in (None, "", MemoryScope.PERSONAL):
return False
# If shared with group, it's visible to everyone in the session
if metadata.get("visibility") == "group":
return True
owner_user_ids = metadata.get("owner_user_ids")
if isinstance(owner_user_ids, list) and owner_user_ids:
return owner_user_id in owner_user_ids if owner_user_id else True
if require_owner_list:
return False
return metadata.get("owner_user_id", metadata.get("user_id")) == owner_user_id| results = [] | ||
| for filters, legacy_personal, owner_user_id, require_owner_list in filters_list: | ||
| results.extend( | ||
| await self._retrieve_with_filter( | ||
| query, | ||
| top_k, | ||
| filters, | ||
| legacy_personal=legacy_personal, | ||
| owner_user_id=owner_user_id, | ||
| require_owner_list=require_owner_list, | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Executing multiple sequential vector database retrievals in a loop can significantly increase latency. Consider using asyncio.gather to parallelize these requests.
| results = [] | |
| for filters, legacy_personal, owner_user_id, require_owner_list in filters_list: | |
| results.extend( | |
| await self._retrieve_with_filter( | |
| query, | |
| top_k, | |
| filters, | |
| legacy_personal=legacy_personal, | |
| owner_user_id=owner_user_id, | |
| require_owner_list=require_owner_list, | |
| ) | |
| ) | |
| tasks = [ | |
| self._retrieve_with_filter( | |
| query, | |
| top_k, | |
| filters, | |
| legacy_personal=legacy_personal, | |
| owner_user_id=owner_user_id, | |
| require_owner_list=require_owner_list, | |
| ) | |
| for filters, legacy_personal, owner_user_id, require_owner_list in filters_list | |
| ] | |
| results_list = await asyncio.gather(*tasks) | |
| results = [item for sublist in results_list for item in sublist] |
| docs = await self.vec_db.document_storage.get_documents( | ||
| metadata_filters=filters, | ||
| limit=10000, | ||
| ) |
There was a problem hiding this comment.
Fetching up to 10,000 documents into memory just to perform pagination and visibility filtering in Python is inefficient and poses a performance risk as the database grows. If the underlying vector database supports complex metadata filters (like OR or IN), this logic should be moved to the database level. If not, consider a more reasonable limit or a streaming approach.
| lines = [ | ||
| f"scope: {meta.memory_scope}", | ||
| f"domain: {domain_label}", | ||
| f"visibility: {meta.visibility}", | ||
| f"memory: {content}", | ||
| ] | ||
| if meta.subject: | ||
| lines.append(f"subject: {meta.subject}") | ||
| if meta.owner_user_ids: | ||
| lines.append(f"owners: {', '.join(meta.owner_user_ids)}") | ||
| if meta.disclosure: | ||
| lines.append(f"recall_when: {meta.disclosure}") | ||
| if meta.entities: | ||
| lines.append(f"entities: {', '.join(meta.entities)}") | ||
| if meta.topics: | ||
| lines.append(f"topics: {', '.join(meta.topics)}") | ||
| lines.append(f"importance: {meta.importance}") |
There was a problem hiding this comment.
Including non-semantic metadata like importance and visibility directly in the text content stored in the vector database can introduce noise and potentially degrade retrieval quality. These fields are better handled as metadata filters. Consider removing them from the formatted string while keeping them in the metadata dictionary.
| lines = [ | |
| f"scope: {meta.memory_scope}", | |
| f"domain: {domain_label}", | |
| f"visibility: {meta.visibility}", | |
| f"memory: {content}", | |
| ] | |
| if meta.subject: | |
| lines.append(f"subject: {meta.subject}") | |
| if meta.owner_user_ids: | |
| lines.append(f"owners: {', '.join(meta.owner_user_ids)}") | |
| if meta.disclosure: | |
| lines.append(f"recall_when: {meta.disclosure}") | |
| if meta.entities: | |
| lines.append(f"entities: {', '.join(meta.entities)}") | |
| if meta.topics: | |
| lines.append(f"topics: {', '.join(meta.topics)}") | |
| lines.append(f"importance: {meta.importance}") | |
| lines = [ | |
| f"scope: {meta.memory_scope}", | |
| f"domain: {domain_label}", | |
| f"memory: {content}", | |
| ] | |
| if meta.subject: | |
| lines.append(f"subject: {meta.subject}") | |
| if meta.owner_user_ids: | |
| lines.append(f"owners: {', '.join(meta.owner_user_ids)}") | |
| if meta.disclosure: | |
| lines.append(f"recall_when: {meta.disclosure}") | |
| if meta.entities: | |
| lines.append(f"entities: {', '.join(meta.entities)}") | |
| if meta.topics: | |
| lines.append(f"topics: {', '.join(meta.topics)}") |
There was a problem hiding this comment.
Pull request overview
该 PR 为 AstrBot 简单长期记忆插件引入群聊场景下的三层记忆作用域(personal/group/conversation)与可见性模型,并配套扩展元数据字段、召回过滤链路与注入/展示格式,同时更新版本与文档说明。
Changes:
- 新增记忆作用域与可见性相关的元数据字段,并在存储/召回路径中做作用域感知过滤与去重。
- 提取 prompt 增强:注入会话上下文信息,支持输出 scope/subjects/entities/topics 等结构化字段。
- 配置与文档更新:新增检索优化超时配置、调整默认提取长度阈值、版本升级到 v0.3.0。
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
prompts.py |
扩展记忆提取 prompt:增加会话作用域信息与结构化输出字段/规则。 |
metadata.yaml |
插件版本升级至 v0.3.0。 |
memory_protocol.py |
新增作用域/域枚举与元数据字段,调整记忆内容格式化与注入展示分组。 |
memory_manager.py |
增加作用域过滤、可见性判断、召回过滤链、去重与列表逻辑改造。 |
main.py |
提取结果解析支持 scope/subjects/entities/topics;检索优化增加超时;对话快照记录 sender_id。 |
_conf_schema.json |
移除 memory_domains,新增 optimize_recall_query_timeout 配置项。 |
README.md |
文档补充群聊记忆作用域说明与新增配置项。 |
CHANGELOG.md |
更新 v0.3.0 变更说明。 |
Comments suppressed due to low confidence (1)
memory_manager.py:1645
- _flush_pending_writes() 的语义去重过滤器固定使用
user_id+memory_scope,会让 group/conversation 作用域的去重范围过窄:同一群里的 group 记忆如果由不同 user_id 写入,将无法互相去重,可能导致重建/迁移后群共享记忆重复膨胀。建议按 scope 选择过滤维度:personal 用 owner_user_id(或 user_id),group 用 owner_session_id,conversation 用 umo(必要时再加 owner_session_id),并始终包含 is_memory_record/deprecated 约束。
filters: dict[str, Any] = {
"user_id": item["user_id"],
"memory_scope": item.get("memory_scope", MemoryScope.PERSONAL),
"is_memory_record": True,
"deprecated": False,
}
candidates = await write_kb.vec_db.retrieve(
query=content,
k=1,
metadata_filters=filters,
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| subject=subject, | ||
| entities=entities, | ||
| topics=topics, | ||
| owner_sender_ids=owner_sender_ids, |
| group_filters = { | ||
| "memory_scope": MemoryScope.PERSONAL, | ||
| "owner_session_id": build_session_id(parsed.platform_id, parsed.session_id), | ||
| "visibility": "group", | ||
| "is_memory_record": True, | ||
| "deprecated": False, | ||
| } | ||
| if domain: | ||
| group_filters["domain"] = domain | ||
| group_docs = await self.vec_db.document_storage.get_documents( | ||
| metadata_filters=group_filters, | ||
| limit=10000, | ||
| ) |
| for doc in [*docs, *group_docs]: | ||
| metadata = _safe_parse_metadata(doc.get("metadata", {})) | ||
| uri = metadata.get("uri") or doc.get("text", "") | ||
| if uri in seen: | ||
| continue | ||
| if self._is_visible_personal_memory( | ||
| metadata, | ||
| current_user_id, | ||
| require_owner_list=doc in group_docs, | ||
| ): |
- memory_protocol: 新增 MemoryVisibility 枚举(PRIVATE/GROUP),替换裸字符串 - memory_manager: 使用 MemoryVisibility 常量,新增 max_memory_list_scan 配置 (限制群聊可见记忆扫描量)及 _memory_list_scan_limit() 计算逻辑 - main: 修复 TimeoutError -> asyncio.TimeoutError - _conf_schema: 新增 max_memory_list_scan(默认 200,滑块 20-2000)
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 1 个问题,并且留下了一些整体层面的反馈:
- 目前有两个功能非常相似的字符串列表清理 helper(
memory_protocol.py中的_normalize_string_list和main.py中的_sanitize_string_list);建议将它们合并成一个共享工具函数,以避免后续行为在细节上逐渐出现差异。 - 围绕
_build_recall_filters/_scope_filter/_legacy_personal_filter构建召回过滤器的逻辑已经变得相当复杂;可以考虑把可见性/owner 选择相关的一些规则抽取成更小、命名清晰的 helper,从而让整体检索逻辑更易于理解和维护。
给 AI Agent 的提示词
Please address the comments from this code review:
## Overall Comments
- There are two very similar helpers for cleaning string lists (`_normalize_string_list` in `memory_protocol.py` and `_sanitize_string_list` in `main.py`); consider consolidating them into a shared utility to avoid subtle divergence in behavior over time.
- The recall filter construction around `_build_recall_filters` / `_scope_filter` / `_legacy_personal_filter` has become quite complex; extracting some of the visibility/owner selection rules into smaller, clearly named helpers would make the overall retrieval logic easier to reason about and maintain.
## Individual Comments
### Comment 1
<location path="memory_protocol.py" line_range="202-211" />
<code_context>
def from_dict(cls, data: dict[str, Any]) -> MemoryMetadata:
"""从字典创建实例(自动忽略多余键、缺失键使用默认值)"""
valid = {f.name for f in fields(cls)}
- return cls(**{k: v for k, v in data.items() if k in valid})
+ values = {k: v for k, v in data.items() if k in valid}
+ values["memory_scope"] = normalize_memory_scope(values.get("memory_scope", ""))
+ values["owner_user_id"] = values.get("owner_user_id") or values.get(
+ "user_id", ""
+ )
+ values["owner_user_ids"] = _normalize_string_list(
+ values.get("owner_user_ids", [])
+ )
+ values["speaker_id"] = values.get("speaker_id") or values.get("sender_id", "")
+ values["entities"] = _normalize_string_list(values.get("entities", []))
+ values["topics"] = _normalize_string_list(values.get("topics", []))
+ return cls(**values)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Visibility 字段未被规范化,可能会导致意外值向下游传播。
在 `MemoryMetadata.from_dict` 中,你已经对多个字段做了规范化处理,但 `visibility` 仍然直接使用输入值。由于下游逻辑期望的是 `MemoryVisibility.PRIVATE | GROUP`,这里如果出现任意或拼写错误的字符串,就有可能在不被察觉的情况下绕过可见性检查。请对 `visibility` 进行规范化(例如通过一个 helper),并在值非法时默认回退为 `PRIVATE`。
建议实现:
```python
@classmethod
def from_dict(cls, data: dict[str, Any]) -> MemoryMetadata:
"""从字典创建实例(自动忽略多余键、缺失键使用默认值)"""
valid = {f.name for f in fields(cls)}
values = {k: v for k, v in data.items() if k in valid}
values["memory_scope"] = normalize_memory_scope(values.get("memory_scope", ""))
values["owner_user_id"] = values.get("owner_user_id") or values.get(
"user_id", ""
)
values["owner_user_ids"] = _normalize_string_list(
values.get("owner_user_ids", [])
)
values["speaker_id"] = values.get("speaker_id") or values.get("sender_id", "")
values["entities"] = _normalize_string_list(values.get("entities", []))
values["topics"] = _normalize_string_list(values.get("topics", []))
# 将 visibility 规范化为受支持的枚举值,非法或空值回退为 PRIVATE
values["visibility"] = normalize_visibility(
values.get("visibility", MemoryVisibility.PRIVATE)
)
return cls(**values)
```
1. 在 `memory_protocol.py` 中(靠近 `normalize_memory_scope` / `_normalize_string_list`)新增一个 `normalize_visibility` helper,建议逻辑如下:
- 接受 `str | MemoryVisibility`(或 `Any`),返回一个字符串。
- 如果值是 falsy,则返回 `MemoryVisibility.PRIVATE`。
- 如果该值已经等于允许的枚举值之一(`MemoryVisibility.PRIVATE`、`MemoryVisibility.GROUP`),则直接返回。
- 如果是字符串,则通过 `.strip().lower()` 做规范化,并将已知别名(例如 `"private"`、`"group"`)映射到对应枚举值。
- 其他任何情况(未知或格式错误)都默认回退到 `MemoryVisibility.PRIVATE`。
2. 确保在定义 `MemoryMetadata.from_dict` 的作用域中可以使用 `normalize_visibility`(大概率在同一模块内,不需要额外导入)。
3. 如果在其他位置也存在类似可见性语义的字段且依赖外部输入的原始字符串,也可以考虑统一使用 `normalize_visibility`,以提升整体一致性。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈不断改进 Review 质量。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- There are two very similar helpers for cleaning string lists (
_normalize_string_listinmemory_protocol.pyand_sanitize_string_listinmain.py); consider consolidating them into a shared utility to avoid subtle divergence in behavior over time. - The recall filter construction around
_build_recall_filters/_scope_filter/_legacy_personal_filterhas become quite complex; extracting some of the visibility/owner selection rules into smaller, clearly named helpers would make the overall retrieval logic easier to reason about and maintain.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- There are two very similar helpers for cleaning string lists (`_normalize_string_list` in `memory_protocol.py` and `_sanitize_string_list` in `main.py`); consider consolidating them into a shared utility to avoid subtle divergence in behavior over time.
- The recall filter construction around `_build_recall_filters` / `_scope_filter` / `_legacy_personal_filter` has become quite complex; extracting some of the visibility/owner selection rules into smaller, clearly named helpers would make the overall retrieval logic easier to reason about and maintain.
## Individual Comments
### Comment 1
<location path="memory_protocol.py" line_range="202-211" />
<code_context>
def from_dict(cls, data: dict[str, Any]) -> MemoryMetadata:
"""从字典创建实例(自动忽略多余键、缺失键使用默认值)"""
valid = {f.name for f in fields(cls)}
- return cls(**{k: v for k, v in data.items() if k in valid})
+ values = {k: v for k, v in data.items() if k in valid}
+ values["memory_scope"] = normalize_memory_scope(values.get("memory_scope", ""))
+ values["owner_user_id"] = values.get("owner_user_id") or values.get(
+ "user_id", ""
+ )
+ values["owner_user_ids"] = _normalize_string_list(
+ values.get("owner_user_ids", [])
+ )
+ values["speaker_id"] = values.get("speaker_id") or values.get("sender_id", "")
+ values["entities"] = _normalize_string_list(values.get("entities", []))
+ values["topics"] = _normalize_string_list(values.get("topics", []))
+ return cls(**values)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Visibility field is not normalized, which may let unexpected values propagate.
In `MemoryMetadata.from_dict` you normalize several fields, but `visibility` is still taken directly from the input. Since downstream logic expects `MemoryVisibility.PRIVATE | GROUP`, arbitrary or misspelled strings here can silently bypass visibility checks. Please normalize `visibility` (e.g., via a helper) and default to `PRIVATE` when the value is invalid.
Suggested implementation:
```python
@classmethod
def from_dict(cls, data: dict[str, Any]) -> MemoryMetadata:
"""从字典创建实例(自动忽略多余键、缺失键使用默认值)"""
valid = {f.name for f in fields(cls)}
values = {k: v for k, v in data.items() if k in valid}
values["memory_scope"] = normalize_memory_scope(values.get("memory_scope", ""))
values["owner_user_id"] = values.get("owner_user_id") or values.get(
"user_id", ""
)
values["owner_user_ids"] = _normalize_string_list(
values.get("owner_user_ids", [])
)
values["speaker_id"] = values.get("speaker_id") or values.get("sender_id", "")
values["entities"] = _normalize_string_list(values.get("entities", []))
values["topics"] = _normalize_string_list(values.get("topics", []))
# 将 visibility 规范化为受支持的枚举值,非法或空值回退为 PRIVATE
values["visibility"] = normalize_visibility(
values.get("visibility", MemoryVisibility.PRIVATE)
)
return cls(**values)
```
1. Add a `normalize_visibility` helper in `memory_protocol.py` (near `normalize_memory_scope` / `_normalize_string_list`) with logic similar to:
- Accept `str | MemoryVisibility` (or `Any`) and return a string.
- If the value is falsy, return `MemoryVisibility.PRIVATE`.
- If it's already equal to one of the allowed enum values (`MemoryVisibility.PRIVATE`, `MemoryVisibility.GROUP`), return it directly.
- If it's a string, normalize via `.strip().lower()` and map known aliases (e.g. `"private"`, `"group"`) to the enum values.
- For anything else (unknown or malformed), default to `MemoryVisibility.PRIVATE`.
2. Ensure `normalize_visibility` is imported or available in the scope where `MemoryMetadata.from_dict` is defined (most likely same module, no extra imports needed).
3. If there are additional visibility-like fields elsewhere that rely on raw strings from external input, consider updating them to use `normalize_visibility` as well for consistency.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _normalize_subject_ids(value: Any) -> list[str]: | ||
| raw_values = value if isinstance(value, list) else str(value).split(",") | ||
| subjects = [] | ||
| for item in raw_values: | ||
| subject = _normalize_subject_id(_sanitize_memory_content(str(item))[:120]) | ||
| if subject and subject not in {"current_sender", "group", "conversation"}: |
| f"scope: {meta.memory_scope}", | ||
| f"domain: {domain_label}", | ||
| f"visibility: {meta.visibility}", | ||
| f"memory: {content}", | ||
| ] | ||
| if meta.subject: | ||
| lines.append(f"subject: {meta.subject}") | ||
| if meta.owner_user_ids: | ||
| lines.append(f"owners: {', '.join(meta.owner_user_ids)}") | ||
| if meta.disclosure: | ||
| lines.append(f"recall_when: {meta.disclosure}") | ||
| if meta.entities: | ||
| lines.append(f"entities: {', '.join(meta.entities)}") | ||
| if meta.topics: | ||
| lines.append(f"topics: {', '.join(meta.topics)}") | ||
| lines.append(f"importance: {meta.importance}") |
| group_filters = { | ||
| "memory_scope": MemoryScope.PERSONAL, | ||
| "owner_session_id": build_session_id(parsed.platform_id, parsed.session_id), | ||
| "visibility": MemoryVisibility.GROUP, | ||
| "is_memory_record": True, | ||
| "deprecated": False, | ||
| } |
| docs = await self._list_visible_user_documents( | ||
| event, domain, page=page, page_size=page_size | ||
| ) | ||
| total = len(docs) | ||
| offset = (page - 1) * page_size | ||
| docs = docs[offset : offset + page_size] |
| for doc in [*docs, *group_docs]: | ||
| metadata = _safe_parse_metadata(doc.get("metadata", {})) | ||
| uri = metadata.get("uri") or doc.get("text", "") | ||
| if uri in seen: | ||
| continue | ||
| if self._is_visible_personal_memory( | ||
| metadata, | ||
| current_user_id, | ||
| require_owner_list=doc in group_docs, | ||
| ): |
| # 语义去重:召回相似记忆,高相似度则跳过 | ||
| filters: dict[str, Any] = { | ||
| "user_id": item["user_id"], | ||
| "memory_scope": item.get("memory_scope", MemoryScope.PERSONAL), | ||
| "is_memory_record": True, | ||
| "deprecated": False, | ||
| } |
- memory_manager: recall 和 list 改用 asyncio.gather 并行查询多个过滤器源 - memory_manager: 修复 _memory_list_scan_limit 逻辑(max 替代 min), 确保能扫描到足够填充当前页的记录 - memory_manager: list_memories 新增 truncated 返回值,扫描被截断时提示用户 - memory_manager: 私聊也纳入 conversation 作用域,统一行为 - memory_manager: _is_visible_personal_memory 增加 GROUP visibility 快速路径 - memory_manager: 重建语义去重过滤器改用作用域专属键(owner_session_id/umo) - main: _normalize_subject_ids 过滤 None/空值/"none" 字符串 - main: 统一 memory_type=MemoryType.NORMAL,与 domain 字段职责分离 - memory_protocol: 新增 normalize_visibility,from_dict 自动标准化 visibility - memory_protocol: format_memory_content 精简输出字段
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| visibility=visibility, | ||
| subject=subject, | ||
| entities=entities, | ||
| topics=topics, | ||
| memory_content=content, | ||
| owner_sender_ids=owner_sender_ids, | ||
| speaker_id=owner_sender_ids[0], | ||
| ) |
| def _memory_list_scan_limit(self, page: int, page_size: int) -> int: | ||
| try: | ||
| configured = int(self.config.get("max_memory_list_scan", 200)) | ||
| except (TypeError, ValueError): | ||
| configured = 200 | ||
| configured = max(1, configured) | ||
| needed = max(1, page) * max(1, page_size) | ||
| return max(configured, needed) |
| lines.append(f"entities: {', '.join(meta.entities)}") | ||
| if meta.topics: | ||
| lines.append(f"topics: {', '.join(meta.topics)}") | ||
| return "\n".join(lines) |
| if not memory_scope: | ||
| if parsed.session_type == "group": | ||
| scopes.extend([MemoryScope.GROUP, MemoryScope.CONVERSATION]) | ||
| else: | ||
| scopes.append(MemoryScope.CONVERSATION) |
| subjects = _normalize_subject_ids( | ||
| item.get("subjects", item.get("subject", "")) | ||
| ) | ||
| subject = subjects[0] if subjects else "" | ||
| if session_type == "group" and scope == MemoryScope.PERSONAL: | ||
| if not subjects: | ||
| continue |
- memory_manager: 移除 _legacy_personal_filter / _is_visible_personal_memory, 不再在运行时兼容无 memory_scope 的旧记录;须通过 /memory rebuild 补齐字段 - memory_manager: _build_recall_filters 和 _retrieve_with_filter 简化签名, 不再携带 legacy_personal/owner_user_id/require_owner_list 参数 - memory_manager: 新增 _normalize_rebuild_record_metadata(), 重建时自动将旧记录规范化为 v0.3 metadata 结构(含 memory_scope/visibility 等) - memory_manager: 新增 _delete_rebuild_source_records(),删除时按 kb_id 限定范围, 同时处理 is_memory_record 和旧格式(仅 uri + kb_id)两类记录 - memory_manager: 重建分批拉取改用 collect_memory_records() 闭包, 分别按 is_memory_record 和 deprecated=False 拉取,均限定 kb_id - memory_manager: 重建完整性校验按 kb_id 计数 - main: _normalize_subject_ids 改为先取 subjects 字段,再回退 subject - README/CHANGELOG: 补充升级说明(从旧版本升级需执行 /memory rebuild)
概要
引入三层记忆作用域模型,解决群聊场景下的记忆归属问题。附带模块重构与 CI 集成,版本升至 v0.3.0。
主要变更
群聊记忆作用域
user_id隔离,仅本人可见session_id隔离,群内成员可见private(仅所有者)/group(群内共享),多所有者自动升级memory_scope字段)自动兼容为 personal重构
prompts.py:集中管理 prompt 模板、常量、sanitize 函数_validate_command()统一命令参数校验,减少 handler 重复代码CI / 配置
extraction_min_content_length默认值 500 → 150文件
main.pymemory_manager.pymemory_protocol.pyprompts.py.github/workflows/code-quality.ymlCHANGELOG.md/README.md由 Sourcery 提供的总结
引入带有可见性规则的作用域化记忆模型,覆盖个人、群组和会话上下文,并调整存储、检索和格式化逻辑,以支持群聊场景和更丰富的元数据。
新功能:
增强改进:
构建:
文档:
Original summary in English
由 Sourcery 提供的摘要
在个人、群组和会话上下文中引入带有作用域和可见性的记忆模型,增强存储、检索、格式化和抽取能力,以更好地支持群聊和更丰富的元数据,并同时更新文档和版本。
新特性:
增强改进:
构建:
文档:
Original summary in English
Summary by Sourcery
Introduce scoped and visible memory model across personal, group, and conversation contexts, enhancing storage, retrieval, formatting, and extraction to better support group chats and richer metadata, while updating docs and versioning.
New Features:
Enhancements:
Build:
Documentation: