feature: advanced memory 服务化 - #329
Conversation
Please enter the commit message for your changes. Lines starting
AI Code Review审查结论不通过 审查范围:f05797d..eb29c64(93 个变更文件,+4796/-4591),计划为 "feature: advanced memory 服务化 / 记忆与上下文部分解耦",涉及三个新后端(local/redis/sql)的 AdvancedMemoryRuntime、Session Compact 新包(AutoCompact/SessionMemoryExtractor/ToolResultBudget 等)、SessionService 的 compact_events/patch_session_state、Runner 接线与示例。 计划符合性:数据面主体(作用域化、键隔离、compact/patch 幂等、Runner 接线)基本符合计划;但 SQL 与 Redis 后端存在两处会直接导致清理功能失效或后端不可用的死引用缺陷(SEVERE),另有 AutoCompact 持久化边界解析失败升级为硬阻断(MODERATE)与 Redis 去重键 TTL 语义缺口(MODERATE),以及一处同秒排序不确定性(LOW)。 主要风险:
测试充分性:新增测试覆盖 session 服务 patch/update/compact 与 compact_events 幂等、token budget、跨事件循环锁;但 _redis_stores.py 与 _sql_stores.py 两个新存储模块完全无测试,SQL 清理周期路径也无测试,正是 SEVERE-1/2 与 MODERATE-4 未被拦截的原因;AutoCompact 的边界映射失败路径(_resolve_boundary_event_id 返回 None)同样无测试覆盖。 门禁结论:本变更包含会直接导致 Redis 后端不可用与 SQL 清理功能失效的高置信缺陷,判定为 FAILED。 发现的问题严重
问题: 触发条件: 实际影响: 条件成立时表现为死代码而非异常( 修正方向: 在 严重
问题: 触发条件: 实际影响: 初次会话级写入/读取即抛 修正方向: 为该键恢复一个等价的安全会话标识(例如在 中等
问题: 触发条件: Redis 后端、 实际影响: 该去重集合键永不获得过期时间,同时 修正方向: 让跳过匹配精确到转录流键而不是前缀——例如把 skip 逻辑改为 中等
问题: 触发条件: 启用 AutoCompact + Session Memory,会话发生多轮压缩且当前按 legacy 回退压缩,或记录边界签名/出现次数无法映射回活跃事件( 实际影响: 每次压缩尝试都抛出 修正方向: 使边界解析失败时可降级:在 较低
问题: 触发条件: SQL 会话服务下发生 Session Compact,且被压缩的前缀最后事件与保留的首个事件之间没有新增事件(时间戳相同或接近);此后重新加载会话。 实际影响: 重载的会话事件顺序在摘要与保留事件之间可能抖动(例如摘要排在保留事件之后),导致上下文注入顺序与用户实际对话顺序不符,压缩边界附近的历史呈现错位。 修正方向: 为摘要分配一个严格早于边界但大于前驱的时间戳(如 |
| if self.config.storage_backend == "sql" and self.root._sql_cleanup is not None: | ||
| await self.root._sql_cleanup.start() |
There was a problem hiding this comment.
问题: ScopedAdvancedMemoryRuntime.initialize() 引用 self.root._sql_cleanup,但 AdvancedMemoryRuntime 从未定义或赋初值该属性(全仓 grep 仅有此处两行引用),SqlAdvancedMemoryCleanup 也从未在 create() 中被实例化;create() 仅为 sql 分支创建 _sql_storage、为 local 分支创建 _local_cleanup。
触发条件: storage_backend="sql" 且配置了 memory_ttl_seconds 或 session_ttl_seconds,任何会话调用 ScopedAdvancedMemoryRuntime.initialize()(例如 Advanced Memory 工具/记忆注入首次作用域化时)都会进入该分支。
实际影响: 条件成立时表现为死代码而非异常(_sql_cleanup 为 None,条件不满足),因此不报错但过期清理任务永远不会启动:SqlMemoryIndex/SqlMemoryTopic/SqlTranscript/SqlTranscriptSeen/SqlToolResult 中设置了 expires_at 的行永不被删除,SQL 后端 TTL 保留语义完全失效,表随使用无界增长。
修正方向: 在 AdvancedMemoryRuntime.create() 的 sql 分支实例化 SqlAdvancedMemoryCleanup(resolved_config, sql_storage) 并存入 _sql_cleanup 字段,或将 initialize() 改为在该分支直接创建并 start() 清理任务;同时补充 SQL 清理路径的单元/集成测试。
| ) | ||
|
|
||
| def _session_base(self, session_id: str) -> str: | ||
| safe_session_id = self._paths.session_dir(session_id).name |
There was a problem hiding this comment.
问题: _session_base() 调用 self._paths.session_dir(session_id),而 session_dir 已被本次变更从 AdvancedMemoryPaths 中删除(当前 _paths.py 仅含 memory_topic_path 等方法,session_dir 只存在于旧版 f05797d 基座中)。
触发条件: storage_backend="redis" 下任何一次会话级存取(RedisToolResultStore.write/read、RedisTranscriptStore.append/append_unique/read_all、delete_session、_refresh_session_ttl)都会先调用 _session_base() 构造键。
实际影响: 初次会话级写入/读取即抛 AttributeError: 'AdvancedMemoryPaths' object has no attribute 'session_dir',Redis 后端的会话数据(工具结果、去重转录)完全不可用;去重转录与大型工具结果正是本计划声称的 Redis 后端核心能力,现有消费者与其对接即失败。
修正方向: 为该键恢复一个等价的安全会话标识(例如在 _paths.py 重新实现 session_dir/安全组件化函数,或在 _redis_stores.py 用 _collision_safe_component(session_id) 直接生成),并为 Redis 存储补充调用 _session_base() 的测试。
| async def _refresh_session_ttl(self, session_id: str, *keys: str) -> None: | ||
| skip_prefixes: tuple[str, ...] = () | ||
| if not self._config.session_ttl_delete_transcripts: | ||
| skip_prefixes = (f"{self._session_base(session_id)}:transcript", ) | ||
| await self._refresh_ttl_group( | ||
| self._session_registry(session_id), | ||
| list(keys), | ||
| self._config.session_ttl_seconds, | ||
| skip_prefixes=skip_prefixes, |
There was a problem hiding this comment.
问题: _refresh_session_ttl 在 session_ttl_delete_transcripts=False 时设置 skip_prefixes=(f"{base}:transcript",),_refresh_ttl_group 用 startswith 前缀匹配跳过 TTL 刷新;去重集合键 {stream}:seen:{unique_key}(append_unique 中构造)同样以 :transcript 开头,被一并跳过。
触发条件: Redis 后端、session_ttl_delete_transcripts=False(保留转录语义)且配置了 session_ttl_seconds,调用 append_unique() 写入新去重记录后刷新 TTL。
实际影响: 该去重集合键永不获得过期时间,同时 :transcript 流本身也被跳过(此为本意——保留转录),但 :seen: 集合内的唯一键值将永久留存:数据随会话唯一记录数无界增长,且陈旧值永久抑制相同 unique_key 记录的重新写入(去重语义与保留语义矛盾)。
修正方向: 让跳过匹配精确到转录流键而不是前缀——例如把 skip 逻辑改为 key == stream 或按 key.endswith(":transcript")(流键以 :transcript 结尾,而 seen 键以 :transcript:seen: 续接),从而仅跳过流、仍为 :seen: 键设置 TTL。
| compact_events = getattr(ctx.session, "compact_events", None) | ||
| if not callable(compact_events): | ||
| # AutoCompact remains usable as a request-only primitive in unit | ||
| # tests and custom integrations. The standard Manager supplies | ||
| # the framework Session and persists the compacted window. | ||
| return | ||
|
|
||
| boundary_event_id = record.boundary_event_id or self._resolve_boundary_event_id( | ||
| ctx, | ||
| record.boundary_signature, | ||
| record.boundary_occurrence, | ||
| ) | ||
| if boundary_event_id is None: |
There was a problem hiding this comment.
问题: _persist_session_compaction 在 boundary_event_id is None 时无条件 raise ValueError,而旧版实现(f05797d 的 _persist_success)通过转录记录持久化失败并让电路熔断器重试;本次变更新增的 _legacy_boundary_event_id 在多轮压缩后(session memory 已存在、边界事件已被归档)会返回 None,直接把压缩路径变成异常路径。
触发条件: 启用 AutoCompact + Session Memory,会话发生多轮压缩且当前按 legacy 回退压缩,或记录边界签名/出现次数无法映射回活跃事件(_resolve_boundary_event_id 返回 None)时。
实际影响: 每次压缩尝试都抛出 ValueError: Cannot map the AutoCompact boundary to an active Session Event,apply 捕获后把 request.contents 恢复为未压缩内容并递增 consecutive_failures——接近硬限时直接进入阻断态(blocked=True),会话在上下文溢出边缘被硬阻断,甚至无法通过降级压缩延续。
修正方向: 使边界解析失败时可降级:在 _persist_session_compaction 中 boundary_event_id 为 None 时改为调用 session.compact_events 的降级路径(例如按 _legacy_boundary_event_id 的索引语义回退),或将持久化失败与压缩回滚解耦,避免每次失败都计为一次压缩失败。
| summary_event.set_summary_event(True) | ||
| # SQL backends restore active Events in timestamp order. Give the | ||
| # replacement summary the prefix's timestamp so it remains the anchor | ||
| # before every retained Event after persistence. | ||
| summary_event.timestamp = replaced[0].timestamp | ||
|
|
There was a problem hiding this comment.
问题: compact_events 将摘要事件时间戳设为被替换前缀的首个事件时间戳(replaced[0].timestamp),使摘要与保留事件可能出现完全相同的浮点时间戳;SQL 后端恢复时按 timestamp.desc 取回后 reversed() 还原,同秒内的并列序由数据库物理行序决定,无法保证与压缩前一致。
触发条件: SQL 会话服务下发生 Session Compact,且被压缩的前缀最后事件与保留的首个事件之间没有新增事件(时间戳相同或接近);此后重新加载会话。
实际影响: 重载的会话事件顺序在摘要与保留事件之间可能抖动(例如摘要排在保留事件之后),导致上下文注入顺序与用户实际对话顺序不符,压缩边界附近的历史呈现错位。
修正方向: 为摘要分配一个严格早于边界但大于前驱的时间戳(如 replaced[0].timestamp 与上一个可区分时间戳之间取微步长),或为 StorageSessionEvent 增加持久化序号列并按其排序,消除同秒并列的不确定性。
AI Code Review审查结论不通过 审查范围与结论审查范围: 主要风险
测试充分性变更新增了 门禁结论存在 4 条 SEVERE(全部为已删除 API 的 import 残留导致测试收集失败与示例不可运行),本次变更不满足合入门禁,建议修复后再合入。 发现的问题严重
问题: 本变更将 advanced memory 服务化并整体删除 触发条件: 任何一次 pytest 收集到 实际影响: 该测试文件整文件无法导入,导致相关测试全部失败,CI( 修正方向: 删除本文件(及同目录 严重
问题: 本变更在 触发条件: 按 实际影响: 三个文件全部收集失败, 修正方向: 删除这三个残留测试文件,或将它们迁移到 严重
问题: 本变更将示例迁移到新架构( 触发条件: 按 README 说明 实际影响: 该示例完全不可运行(连同 修正方向: 删除本脚本中已失效的 严重
问题: 本变更删除了 触发条件: 按 README 执行 实际影响: SQL 持久化示例(含 修正方向: 移除脚本对 中等
问题: 本变更删除了 触发条件: 任何用户按 README 组装代码或执行示例。 实际影响: 文档与实际 API 完全脱节,用户按文档操作会遇到 修正方向: 将 README 重写为新的标准 中等
问题: 触发条件: 同一 session 上有两个并发 checkpoint 写(或多进程中同一 session 被并发访问)时,后完成的 实际影响: 部分 修正方向: 改用事务内 较低
问题: 触发条件: 每次触发 auto-compact 的模型请求( 实际影响: 会话 state 中持续累积一个永远不被读取的指纹键,state(含 Redis/SQL 存储的完整 JSON)随对话轮次增长,且易让后续 Session Memory 读取方误以为该字段是受支持的契约;同时说明 usage-baseline 应对“模型与工具更新后指纹失效”的机制实际并未闭环。 修正方向: 删除 |
| from trpc_agent_sdk.advanced_memory import AdvancedMemoryServiceConfig | ||
| from trpc_agent_sdk.advanced_memory import AdvancedMemoryRuntime | ||
| from trpc_agent_sdk.advanced_memory import HistorySnipCallback | ||
| from trpc_agent_sdk.advanced_memory import LongTermMemoryContext | ||
| from trpc_agent_sdk.advanced_memory import LongTermMemoryContextCallback | ||
| from trpc_agent_sdk.advanced_memory import MemoryIndexEntry | ||
| from trpc_agent_sdk.advanced_memory import MicrocompactCallback | ||
| from trpc_agent_sdk.advanced_memory import setup_advanced_memory | ||
| from trpc_agent_sdk.advanced_memory import setup_context_management | ||
| from trpc_agent_sdk.advanced_memory import ToolResultBudgetCallback | ||
| from trpc_agent_sdk.advanced_memory import TranscriptSessionService | ||
| from trpc_agent_sdk.advanced_memory._callbacks import install_staged_callback | ||
| from trpc_agent_sdk.memory import AdvancedMemoryService | ||
| from trpc_agent_sdk.models import LlmRequest | ||
| from trpc_agent_sdk.sessions.compact._callbacks import install_staged_callback |
There was a problem hiding this comment.
问题: 本变更将 advanced memory 服务化并整体删除 trpc_agent_sdk/advanced_memory 包、trpc_agent_sdk/sessions/compact/_callbacks.py(含 install_staged_callback)以及 trpc_agent_sdk/memory 中的 AdvancedMemoryService(_advanced_memory_service.py 已清空且不再导出),但本文件在变更中仍保留/修改了对这些已删除符号的导入,第 10-16 行依次导入 AdvancedMemoryServiceConfig、AdvancedMemoryRuntime、LongTermMemoryContext、LongTermMemoryContextCallback、MemoryIndexEntry、AdvancedMemoryService、install_staged_callback,这些模块在 HEAD 上均不存在。
触发条件: 任何一次 pytest 收集到 tests/(如 CI 的 pytest --cov=trpc_agent_sdk ... tests/,pyproject.toml 的 testpaths = ["tests"])或单独运行该测试文件时,import 阶段必然抛出 ModuleNotFoundError: No module named 'trpc_agent_sdk.advanced_memory'。
实际影响: 该测试文件整文件无法导入,导致相关测试全部失败,CI(--cov-fail-under=80)无法通过;同时 advanced_memory 新能力(Session Memory、filter 安装等)没有任何存量测试覆盖,重构正确性无法被验证。
修正方向: 删除本文件(及同目录 test_preload_memory.py、test_advanced_memory_tools.py 等仍引用已删除 API 的测试),或按新 API 重写为针对 AdvancedAutoCompactSummarizerManager/AdvancedAutoCompactSummarizerFilter/SessionMemoryExtractor(_trpc_agent:summary)的测试。
| from trpc_agent_sdk.advanced_memory import AdvancedMemoryServiceConfig | ||
| from trpc_agent_sdk.memory import AdvancedMemoryService | ||
| from trpc_agent_sdk.runners import Runner | ||
| from trpc_agent_sdk.sessions import InMemorySessionService |
There was a problem hiding this comment.
问题: 本变更将示例迁移到新架构(agent/ 子包 + create_agent),但脚本第 17 行仍 from trpc_agent_sdk.advanced_memory import AdvancedMemoryServiceConfig、第 19 行仍 from trpc_agent_sdk.memory import AdvancedMemoryService 并随后 AdvancedMemoryService(config=...)——trpc_agent_sdk.advanced_memory 已被本次变更删除,memory.AdvancedMemoryService 已清空不再导出,而 agent.agent.create_agent 实际使用的是标准 SessionService + AdvancedAutoCompactSummarizerFilter/advanced memory 的替代机制,与本脚本的组装方式不一致。
触发条件: 按 README 说明 python run_agent.py 运行 redis 示例时,import 阶段即抛出 ModuleNotFoundError: No module named 'trpc_agent_sdk.advanced_memory'。
实际影响: 该示例完全不可运行(连同 examples/memory_service_with_advanced_memory_sql/run_agent.py 第 17、19 行同样失败),与 README 中“运行本示例验证”的说明直接矛盾;重构后 Redis/SQL 持久化能力没有可运行的演示路径。
修正方向: 删除本脚本中已失效的 advanced_memory/AdvancedMemoryService 导入与实例化,改为使用 agent.agent.create_agent 内置的标准 SessionService(InMemory)+ advanced compaction 组合,或直接删除该目录。
| from trpc_agent_sdk.advanced_memory import AdvancedMemoryServiceConfig | ||
| from trpc_agent_sdk.memory import AdvancedMemoryService | ||
| from trpc_agent_sdk.runners import Runner | ||
| from trpc_agent_sdk.sessions import InMemorySessionService |
There was a problem hiding this comment.
问题: 本变更删除了 trpc_agent_sdk.advanced_memory、BaseSessionCompactManager、AdvancedSessionCompactManager、AdvancedCompactConfig 及 session_compact_manager= 接线,但本文件作为变更中新增的 SQL 示例脚本,第 17-19 行仍导入并实例化这些已删除 API(AdvancedMemoryServiceConfig、AdvancedMemoryService(config=...)),与 agent.agent.create_agent 的新组装方式矛盾。
触发条件: 按 README 执行 python run_agent.py 时,import 阶段抛出 ModuleNotFoundError: No module named 'trpc_agent_sdk.advanced_memory'。
实际影响: SQL 持久化示例(含 .env、agent/ 子包、README 全文)全部成为不可运行的死示例;对本变更宣称的“advanced memory 服务化,支持 redis/sql”能力形成负向宣传。
修正方向: 移除脚本对 AdvancedMemoryService/advanced_memory 的依赖,改用新架构下 create_agent 的组合方式,或删除该示例目录。
| └── AdvancedSessionCompactManager | ||
| ├── Session Memory | ||
| ├── Tool Result Budget | ||
| ├── History Snip | ||
| ├── Microcompact | ||
| └── AutoCompact | ||
|
|
||
| AdvancedMemoryService | ||
| ├── save_memory | ||
| ├── read_memory | ||
| ├── list_memory_index | ||
| └── long-term memory injection | ||
| ``` | ||
|
|
||
| 1. `session-1` 连续输入多轮 Python 开发偏好。 | ||
| 2. 当累计上下文和工具调用达到配置阈值后,系统会提取 session memory,并写入 | ||
| `session_memory.md`。 | ||
| 3. `session-1` 请求总结已经学习到的开发偏好。 | ||
| 4. `session-2` 查询长期记忆,验证不同 Session 共享同一个 `MEMORY/`。 | ||
| 不再使用独立的 Advanced SessionService。Session 的创建、Event 保存和状态管理始终 | ||
| 由标准 `InMemorySessionService`、`RedisSessionService` 或 `SqlSessionService` | ||
| 负责;Advanced Compact 通过 `BaseSessionCompactManager` 生命周期接入。 | ||
|
|
||
| ## 使用方式 | ||
| ## 核心组装 | ||
|
|
||
| ```python | ||
| from pathlib import Path | ||
|
|
||
| from trpc_agent_sdk.memory import AdvancedMemoryConfig | ||
| from trpc_agent_sdk.sessions import AdvancedMemorySessionService | ||
| from trpc_agent_sdk.runners import Runner | ||
| config = AdvancedMemoryServiceConfig( | ||
| root_dir=Path(__file__).resolve().parent, | ||
| ) | ||
|
|
||
| session_service = AdvancedMemorySessionService( | ||
| config=AdvancedMemoryConfig( | ||
| root_dir=Path(__file__).resolve().parent, | ||
| ) | ||
| session_service = InMemorySessionService( | ||
| session_config=SessionServiceConfig( | ||
| store_historical_events=True, | ||
| ), | ||
| session_compact_manager=AdvancedSessionCompactManager( | ||
| config=AdvancedCompactConfig(), |
There was a problem hiding this comment.
问题: 本变更删除了 AdvancedSessionCompactManager、BaseSessionCompactManager、AdvancedCompactConfig 及 session_compact_manager= 参数(最终在 4116be2 中整体移除),并将 run_agent.py 清空为 0 字节,但本 README 仍在第 7 行(类图)、第 23 行(“Advanced Compact 通过 BaseSessionCompactManager 生命周期接入”)、第 36-37 行(session_compact_manager=AdvancedSessionCompactManager(config=AdvancedCompactConfig()))展示这些不存在的 API,并指导用户“执行 python run_agent.py”,而该文件已为空。
触发条件: 任何用户按 README 组装代码或执行示例。
实际影响: 文档与实际 API 完全脱节,用户按文档操作会遇到 ImportError(BaseSessionCompactManager/AdvancedSessionCompactManager 已不存在)和空脚本无输出,造成误导;也表明该示例目录是本次重构遗留的未清理残骸。
修正方向: 将 README 重写为新的标准 SessionService + AdvancedAutoCompactSummarizerFilter/AdvancedAutoCompactSummarizerManager 组合方式(参照 redis/sql 示例的 agent/agent.py),恢复或删除 run_agent.py。
| async def update_session_state( | ||
| self, | ||
| session: Session, | ||
| state_delta: dict[str, Any], | ||
| ) -> None: | ||
| """Persist session-scoped state without replacing the caller's Event window.""" | ||
| if not state_delta: | ||
| return | ||
| session.state.update(state_delta) | ||
|
|
||
| async with self._redis_storage.create_db_session() as redis_session: | ||
| key = session_key(session.app_name, session.user_id, session.id) | ||
| storage_session = await self._get_session(redis_session, key) | ||
| if not storage_session: | ||
| logger.warning( | ||
| "Session %s not found in Redis while updating state", | ||
| session.id, | ||
| ) | ||
| return | ||
| storage_session.state.update(state_delta) |
There was a problem hiding this comment.
问题: update_session_state(_redis_session_service.py 第 264-283 行)是非原子的读-改-写:先 GET 整个存储 session,在内存中 storage_session.state.update(state_delta),再 SET 整个 JSON 文档。该方法是本次变更新增的核心持久化路径,被 SessionMemoryExtractor._persist_checkpoint 在每个 compaction checkpoint 时调用;两个并发调用(如并发 SDK 进程或多个 Session 的 checkpoint)之间没有锁/事务保护,后写者覆盖先写者的 state 增量。
触发条件: 同一 session 上有两个并发 checkpoint 写(或多进程中同一 session 被并发访问)时,后完成的 SET 会以自己读到的旧 state 为基底写回,覆盖对方刚写入的字段。
实际影响: 部分 session.state 字段(如 _trpc_agent:summary、advanced_memory_pending_request_context_fingerprint 或自定义 state)丢失,属于静默数据丢失而非报错;对比同变更中 SQL 实现已采用 SELECT ... FOR UPDATE 行锁,Redis 路径的一致性明显低于 SQL 路径。
修正方向: 改用事务内 WATCH key/MULTI + 条件写入,或将 state 变更做 merge 后仅写变更字段(如使用 HMSET/JSON.SET 定点更新 state 子文档)以减小覆盖窗口;至少为 Redis 路径补充并发写入测试(现有测试的 mock 是 GET/SET 直写,未覆盖此竞态)。
| def record_request_context( | ||
| self, | ||
| request: "LlmRequest", | ||
| ctx: "InvocationContext | None", | ||
| cls, | ||
| request: LlmRequest, | ||
| ctx: InvocationContext, | ||
| ) -> None: | ||
| """Stage the final request fingerprint for persistence on the response Event.""" | ||
| session = getattr(ctx, "session", None) if ctx is not None else None | ||
| state = getattr(session, "state", None) | ||
| session = ctx.session | ||
| state = session.state | ||
| if isinstance(state, dict): | ||
| state["advanced_memory_pending_request_context_fingerprint"] = _request_static_fingerprint(request) | ||
|
|
There was a problem hiding this comment.
问题: TokenContextTracker.record_request_context(_token_budget.py 第 242-252 行)每次在 before-model compaction 成功后向 session.state 写入 advanced_memory_pending_request_context_fingerprint,但全仓(trpc_agent_sdk/、examples/、tests/)没有任何读取该键的代码——SessionMemoryExtractor/_latest_usage_baseline 消费的是写在 Event custom_metadata 上的 advanced_memory_request_context_fingerprint(_token_budget.py:178),且写入端只有测试里伪造的 event metadata,真正的响应 Event 从未写入该字段。
触发条件: 每次触发 auto-compact 的模型请求(_auto_compact.py:581 调用)都会执行该写入,且该 state 会随 update_session_state/update_session 持久化。
实际影响: 会话 state 中持续累积一个永远不被读取的指纹键,state(含 Redis/SQL 存储的完整 JSON)随对话轮次增长,且易让后续 Session Memory 读取方误以为该字段是受支持的契约;同时说明 usage-baseline 应对“模型与工具更新后指纹失效”的机制实际并未闭环。
修正方向: 删除 record_request_context 及其调用点,或将指纹写入响应 Event 的 custom_metadata(advanced_memory_request_context_fingerprint)以对应 _latest_usage_baseline 的读取逻辑,二选一并补充对应测试。
AI Code Review审查结论通过 审查范围:f05797d9f9f..cacda11(6 个提交,"advanced memory 服务化:重构 memory 为 service,支持 local/redis/sql 存储,重写 Session Compact(compact_events 幂等、state 持久化、新 token budget)",110 文件、+6061/-6315,含 23 个测试文件)。 计划符合性:总体按计划完成——advanced memory 已与上下文解耦(AdvancedMemoryService.bind 只安装 LongTermMemoryContext+tools,返回未修改的 session_service);local/redis/sql 三个后端齐全,读写与 TTL 清理路径完整;compact_events 幂等(compaction_id 检查)、update_session_state 三个后端实现正确;storage_reference、_collision_safe_component 路径防护合理;Lua 释放锁脚本、CrossLoopLock、SQL 行锁等并发机制经核查正确。 主要风险(高置信、由本次变更引入):
测试充分性:新增 test_session_compact.py/test_token_budget.py 覆盖 compact_events、session memory 写 state、tool_result budget、usage 基线 fallback 单元路径,但未覆盖 AdvancedAutoCompactSummarizerFilter 的 BEFORE_MODEL 阻塞集成路径(end_invocation 语义、被短路事件不持久化)、指纹 metadata 端到端读写链路,以及 redis/sql 后端 extract+compact+update_session_state 组合。 门禁结论:无 SEVERE 级阻断缺陷,存在 2 个 MODERATE 问题;建议合入前修复 fingerprint metadata 链路(补 writer 或删死代码)并评估 BEFORE_MODEL 阻塞路径的持久化缺口。 发现的问题中等
问题: 重构拆分 advanced memory 时删除了把请求指纹写入事件 custom_metadata 的唯一 writer。旧实现 触发条件: 启用 token 模式( 实际影响: usage 基线机制整体失效:token 估算 100% 回退到启发式 修正方向: 二选一:恢复 writer——在绑定/事件落库路径(如 中等
问题: 触发条件: 任意 compact 压力达到 blocker 阈值(token 模式下 实际影响: 阻塞响应(如 修正方向: 让阻塞路径走持久化链路:在 |
| cls, | ||
| request: LlmRequest, | ||
| ctx: InvocationContext, | ||
| ) -> None: | ||
| """Stage the final request fingerprint for persistence on the response Event.""" | ||
| session = getattr(ctx, "session", None) if ctx is not None else None | ||
| state = getattr(session, "state", None) | ||
| session = ctx.session | ||
| state = session.state |
There was a problem hiding this comment.
问题: 重构拆分 advanced memory 时删除了把请求指纹写入事件 custom_metadata 的唯一 writer。旧实现 trpc_agent_sdk/advanced_memory/_session_service.py 的 append_event 会把 session.state 中的 advanced_memory_pending_request_context_fingerprint 转写到事件 custom_metadata["advanced_memory_request_context_fingerprint"];新代码中没有任何模块再写这两个 key(全仓库 grep 仅 _token_budget.py 自身与测试手工构造)。此处 record_request_context(第 243-249 行)仍每天向 session.state 写入指纹,但无任何消费方;_latest_usage_baseline 读取的元数据 key 也没有写入方,指纹过滤分支(recorded_fingerprint != static_fingerprint 时 continue)永不执行。
触发条件: 启用 token 模式(token_context_tracker.model_context_window_tokens 已配置)且事件带 usage_metadata 的会话。由于事件元数据 key 永远为空,_latest_usage_baseline 无法按请求上下文过滤 usage 基线,只能按内容指纹匹配。
实际影响: usage 基线机制整体失效:token 估算 100% 回退到启发式 estimated,token 模式下 auto_compact_threshold_tokens/blocking_threshold_tokens 被系统性低估,压缩触发过晚;且当系统指令/工具配置变化时本应失效的 usage 基线(_latest_usage_baseline 中的指纹比较试图防护的场景)不会失效。record_request_context 写入的 state key 成为无消费方的死状态,随会话状态永久累积。
修正方向: 二选一:恢复 writer——在绑定/事件落库路径(如 AdvancedMemoryService 或 SessionService 后置钩子)中将 state["advanced_memory_pending_request_context_fingerprint"] 写入事件 custom_metadata["advanced_memory_request_context_fingerprint"](参考旧 _session_service.py 实现);或移除 record_request_context 与 _latest_usage_baseline 中的指纹分支并删除 state 写入,明确该能力已废弃。补充写→读两端到端的测试覆盖匹配/不匹配两条路径。
| ) | ||
| if not result: | ||
| return None | ||
| invocation_ctx.end_invocation = True |
There was a problem hiding this comment.
问题: AdvancedAutoCompactSummarizerFilter._before(默认 compact_trigger=BEFORE_MODEL,也是官方示例 examples/memory_service_with_advanced_memory 唯一使用的配置)在阻断本次模型调用时设置 end_invocation=True(本行),但 end_invocation 在整个 SDK 中没有任何消费者(全仓库 grep 仅 _callback.py 两类回调过滤器设置它,runner、cancel、dsl、server、agents 均无读取方)。阻断实际只靠 rsp.is_continue=False 使 model filter 链短路;被短路的链不会把阻断响应事件 append_event 到 session service,也不会进入 runner 的 _schedule_post_turn_processing。而 _before 中紧接着调用的 extract_if_needed(force=False) 及随后的 update_session_state(InMemory/Redis/Sql 三个后端的新增覆写)仍会把 session memory 状态写回存储。
触发条件: 任意 compact 压力达到 blocker 阈值(token 模式下 request_tokens_before >= blocking_threshold_tokens 且连续失败数达 max_failures)使 create_session_summary_by_request 返回 blocked 响应的 BEFORE_MODEL 回合。
实际影响: 阻塞响应(如 ADVANCED_AUTOCOMPACT_BLOCKED_MESSAGE 或重放记录)不会持久化到会话事件流,调用方只看到被过滤掉的响应;同时坏局面被加固:每次 BEFORE_MODEL 会话都在 state 写 SESSION_MEMORY_STATE_KEY,但压缩永不落库——在 SQL/Redis 后端,被压缩删除的原始事件不会进入 historical_events(update_session 全量写才持久化 events,阻塞时不走),且后续任意 update_session 全量覆盖会用内存态把先前 update_session_state 写回的 state 分层覆盖丢失,session memory 检查点与事件窗口在持久化后端不一致。
修正方向: 让阻塞路径走持久化链路:在 _before 中于 extract_if_needed 成功后通过 session_service 持久化阻塞事件(或保证调用方在短路后仍 append_event),并评估 create_session_summary_by_request 阻塞前先完成 update_session(session) 提交压缩窗口;同时为 end_invocation 补充真实消费者或移除该赋值以避免误导。
feature: advanced memory 服务化,支持 redis/sql
优化框架 session compact 实现逻辑
多用户实现
组件支持 redis 存储
组件支持 sql 存储
组件支持简单自定义记忆