Skip to content

Feature/advanced memory service - #335

Closed
CongkeChen wants to merge 7 commits into
mainfrom
feature/advanced_memory_service
Closed

CongkeChen wants to merge 7 commits into
mainfrom
feature/advanced_memory_service

Conversation

@CongkeChen

Copy link
Copy Markdown
Contributor

Advance Memory 服务化

  • 基于 Session Service 的advanced compact实现

  • 支持 local,sql,redis 的 advanced long-term memory

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

不通过

审查范围:f05797d..debbecd(6 个提交,111 个文件,+6187/−6574 行),计划 Feature/advanced memory service。计划符合性:将 Advanced Memory 重构为 tools/advanced_memory/ 组件加 sessions/compact/(default+advanced)双 summarizer 管理器,新增组件、别名兼容与示例迁移基本符合计划。主要风险:本变更引入的高置信缺陷集中在 advanced 记忆链路 —— (1) AdvancedAutoCompactSummarizer(config=None) 构造即抛 AttributeError,新公开 API 默认用法直接不可用(SEVERE);(2) session-memory 分支 compaction 后 boundary 签名无法再匹配,_apply_record replay 失效并在下一轮请求触发"未减少请求大小"异常,consecutive_failures 累积到 max_failures 后硬阻断会话(SEVERE);(3) checkpoint 边界事件被 compact 归档后 _persist_checkpoint 抛出 ValueError,Session Memory 增量游标永久停滞(SEVERE);(4) local 文件后端 read_topic 完全不检查 memory_ttl_seconds 过期,与 Redis/SQL 后端行为不一致(SEVERE);(5) _latest_usage_baseline 的 fingerprint 读写方是已被删除的旧类,导致 fingerprint 永不命中且每次异常重建(MODERATE);(6) _before 短路模型调用只设置 end_invocation 而未设置 start_invocation,assert start_invocation and end_invocation 校验不通过导致后续调用被阻断(MODERATE)。另:审查中发现 SessionOperationCoordinator.guard 的 timeout 模式存在锁缺陷(asyncio.wait_for 超时只取消轮询协程、底层线程锁被抢占者持有不放,之后该 session 所有 guard 调用永久自旋),但 _coordination.py 在基提交 f05797d 已存在且本次仅原样移动(内容 0 行差异,base 的 advanced_memory/_coordination.py 与 head 的 sessions/compact/advanced/_coordination.py 逐字节相同),该缺陷并非本次变更引入,故不列入评论。测试充分性:tests/advanced_memory/ 新增 23 个测试文件覆盖了组件基本路径,但未覆盖上述构造缺省参数、compaction replay、checkpoint 归档后持久化、TTL 过期读取及过滤器短路场景。门禁结论:存在多个阻断性正确性问题,state=FAILED 不予合入。

发现的问题

严重

trpc_agent_sdk/sessions/compact/advanced/_auto_compact.py:118-133

问题: AdvancedAutoCompactSummarizer.__init__ 将 config 声明为 config: AdvancedAutoCompactSummarizerConfig | None = None,但第 128 行 self._auto_compact_config = config.auto_compact 直接对可能为 None 的 config 解引用;config or AdvancedAutoCompactSummarizerConfig() 只用于 self._runtime。

触发条件: 以 AdvancedAutoCompactSummarizer(model=model) 或不传 config 的方式构造(公开签名与默认值均允许省略 config)。

实际影响: 构造阶段立即抛出 AttributeError: 'NoneType' object has no attribute 'auto_compact',该新公开 API 在合法默认用法下直接不可用,迁移自旧 auto-summarizer 风格的调用方都会受影响。

修正方向: 改为 self._auto_compact_config = (config or AdvancedAutoCompactSummarizerConfig()).auto_compact,或在入口校验并抛出带指引的 ValueError。

严重

trpc_agent_sdk/sessions/compact/advanced/_auto_compact.py:234-320

问题: _apply_record 用 _find_signature_index 按 boundary_occurrence 精确匹配旧 compaction 记录,但 session-memory 分支的 _persist_session_compaction 经 compact_events 已把 boundary 事件本身替换为 summary 事件,active 窗口与后续模型请求中不再存在该 boundary 签名;下次请求 _apply_record 必然返回 False(签名仅在第一次请求中可匹配一次)。

触发条件: 会话第二次进入 _apply_scoped 且 state.latest_compaction 非空(source 为 session-memory 或 legacy);此时 request 已含摘要前缀,reapplied=False,_compact_with_summary 只替换摘要之后的少量内容,request_chars_after >= request_chars_before 触发 raise ValueError("Advanced Auto Compact did not reduce request size")。

实际影响: 每次成功 compaction 后的下一轮请求都会计入一次失败,consecutive_failures 递增,达到 max_failures 后会话被 ADVANCED_AUTOCOMPACT_BLOCKED_MESSAGE 硬阻断(阻断消息经模型过滤器直接返回给用户),正常对话流程被永久打断,且阻断状态仅保存在进程内 _states。

修正方向: replay 时对已摘要化的请求跳过再次 compaction 的尺寸校验(例如识别 reapplied==True 且请求前缀已是摘要时直接返回),并让 _find_signature_index 在匹配失败时退回 _find_last_signature_index/摘要锚点而非计入失败。

严重

trpc_agent_sdk/sessions/compact/advanced/_compaction_memory_extractor.py:604-676

问题: SessionMemoryExtractor._persist_checkpoint 通过 _boundary_for_event 将 checkpoint 的 last_event_id 反查为 (boundary_signature, boundary_occurrence),当 checkpoint 边界事件已被 compact_events 移入 historical_events 后(advanced compaction 正是把旧事件归档的手段),该事件在 active 窗口不可见,_boundary_for_event 返回 None 并抛出 ValueError;_event_records_after_checkpoint 的 timestamp 恢复分支返回的新事件又会被再次 _persist_checkpoint 依赖同一丢失的边界。

触发条件: 会话先完成一次 session-memory 提取(checkpoint 落在较早事件上),随后 advanced compaction 归档了该 checkpoint 事件;后续每次 extract_if_needed 都因边界不可解析而抛错(被 catch 仅记日志),checkpoint 永不前移。

实际影响: Session Memory 的增量游标永久停滞,之后的会话记忆不再更新(新记录与提取状态陈旧),同时每次提取都留下一条异常日志;该行为由本变更引入(旧实现以 last_event_id 直接比较、不依赖事件仍然可见)。

修正方向: 归档 compaction 事件时保留边界映射(如给 compact_events 增加返回被归档边界的元数据,或在 checkpoint 中直接持久化 boundary_signature/boundary_occurrence),使 _boundary_for_event 在事件不可见时也能解析 checkpoint。

严重

trpc_agent_sdk/tools/advanced_memory/_file_storage.py:134-137

问题: LongTermMemoryStore.read_topic(local 文件后端)直接 path.read_text 返回文件内容,完全不检查 memory_ttl_seconds 过期;而 Redis(_redis_stores.py)与 SQL(_sql_stores.py)后端在创建时都进行 TTL 过期检查,同一 read_topic 入口因后端不同行为不一致。

触发条件: 用户配置 memory_ttl_seconds 且以 StorageProvider.LOCAL 读取 topic 文件。

实际影响: 过期记忆在 local 后端仍被当作有效长期记忆注入代理上下文,可能把陈旧/错误信息当作事实使用,且同一配置在 Redis/SQL 后端会正常过期删除、行为跨后端不一致。

修正方向: 在 read_topic 返回前对照 memory_ttl_seconds 校验文件修改时间(或读取文件头部的 updated_at),过期时删除文件并返回 None,与 Redis/SQL 后端语义对齐。

中等

trpc_agent_sdk/sessions/compact/advanced/_token_budget.py:242-251

问题: _latest_usage_baseline 读取的 advanced_memory_request_context_fingerprint(写入方是旧 AdvancedMemorySessionService,本变更将其重构/删除后无任何代码再写入该 key),因此 fingerprint 与 last_usage 序列永远不会来自同一次写入,baseline 永久不命中,函数每次都会重建 RequestMetrics 并重新评估整个历史调用序列。

触发条件: advanced_memory 被启用且 metrics_threshold/baseline_window 类门控条件配置了需要 _latest_usage_baseline 生效的预算管理。

实际影响: 内存预算门控退化:每次请求都重新计算基线、无法跨请求保持稳定基线,预算控制与节流行为不可预期,且每次调用都丢弃历史、评估结果不稳定。

修正方向: 将 fingerprint 的写入方迁移到新的 advanced memory 会话服务/提取器写入路径(与 last_usage 同一写入点),或改为按会话 id 直接读取而不依赖 fingerprint。

中等

trpc_agent_sdk/sessions/compact/advanced/_filters.py:40-60

问题: AdvancedAutoCompactSummarizerFilter._before 在压缩返回非空结果时设置 invocation_ctx.end_invocation = True 并短路模型调用,但没有设置 start_invocation;全仓库(含 _base.py 的断言与调用方过滤链)要求 start_invocation and end_invocation 校验通过才放行,短路路径使校验不通过,后续调用被过滤链继续拦截。

触发条件: 通过 AdvancedAutoCompactSummarizerFilter 且本次请求触发了压缩过滤链(压缩结果非空时短路)。

实际影响: 正常触发压缩之后的下一次模型调用被该过滤器意外拦截/阻断,代理无法继续对话或任务执行,行为与设计意图(压缩后应放行模型调用)相悖。

修正方向: 在 _before 里与 end_invocation 同时设置 start_invocation = True(或让短路不走过滤器、直接返回未设置状态),保证校验语义一致。

Comment on lines +118 to +133
def __init__(
self,
config: AdvancedAutoCompactSummarizerConfig | None = None,
*,
model: LLMModel | None = None,
session_memory_extractor: SessionMemoryExtractor | None = None,
) -> None:
"""Initialize the compressor, summary generator, and session locks."""
self._model = model
self._runtime = AdvancedAutoCompactSummarizerRuntime(config=config or AdvancedAutoCompactSummarizerConfig())
self._auto_compact_config = config.auto_compact
self._session_memory_extractor = self._create_extractor(session_memory_extractor)
self._states: dict[str, AdvancedAutoCompactState] = {}
self._session_locks: dict[str, asyncio.Lock] = {}
self._scoped_processors: dict[object, "AdvancedAutoCompactSummarizer"] = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: AdvancedAutoCompactSummarizer.__init__ 将 config 声明为 config: AdvancedAutoCompactSummarizerConfig | None = None,但第 128 行 self._auto_compact_config = config.auto_compact 直接对可能为 None 的 config 解引用;config or AdvancedAutoCompactSummarizerConfig() 只用于 self._runtime。

触发条件: 以 AdvancedAutoCompactSummarizer(model=model) 或不传 config 的方式构造(公开签名与默认值均允许省略 config)。

实际影响: 构造阶段立即抛出 AttributeError: 'NoneType' object has no attribute 'auto_compact',该新公开 API 在合法默认用法下直接不可用,迁移自旧 auto-summarizer 风格的调用方都会受影响。

修正方向: 改为 self._auto_compact_config = (config or AdvancedAutoCompactSummarizerConfig()).auto_compact,或在入口校验并抛出带指引的 ValueError。

Comment on lines +234 to +320
def _find_signature_index(
self,
contents: list[Content],
signature: str,
occurrence: int,
) -> int | None:
"""Locate a persisted compaction boundary by signature occurrence."""
seen = 0
for index, content in enumerate(contents):
if content_signature(contents[index]) == signature:
seen += 1
if seen == occurrence:
return index
return None

def _find_last_signature_index(
self,
contents: list[Content],
signature: str,
) -> int | None:
"""Find the newest matching boundary after an earlier replay."""
for index in range(len(contents) - 1, -1, -1):
if content_signature(contents[index]) == signature:
return index
return None

def _signature_occurrence(
self,
contents: list[Content],
signature: str,
boundary_index: int,
) -> int:
"""Count a boundary signature's occurrences from the request start."""
return sum(1 for content in contents[:boundary_index + 1] if content_signature(content) == signature)

def _adjust_start_for_tool_pairing(self, contents: list[Content], start: int) -> int:
"""Extend the retained range to keep calls paired with responses."""
if start <= 0 or start >= len(contents):
return max(0, start)
response_ids = {
getattr(part.function_response, "id", None)
for content in contents[start:]
for part in content.parts or [] if part.function_response is not None
}
response_ids.discard(None)
if not response_ids:
return start
for index in range(start - 1, -1, -1):
call_ids = {
getattr(part.function_call, "id", None)
for part in contents[index].parts or [] if part.function_call is not None
}
if call_ids & response_ids:
start = index
response_ids -= call_ids
if not response_ids:
break
return start

def _compaction_start(self, contents: list[Content], boundary_index: int) -> int:
"""Return the retained-content start for a legacy compaction."""
start = min(
boundary_index + 1,
len(contents) - self._auto_compact_config.keep_recent_contents,
)
return self._adjust_start_for_tool_pairing(contents, start)

def _session_memory_compaction_start(self, boundary_index: int) -> int:
"""Drop everything through the session-memory checkpoint boundary."""
return boundary_index + 1

def _apply_record(self, request: LlmRequest, record: AdvancedAutoCompactRecord) -> bool:
"""Replay a persisted compaction record into a rebuilt request."""
boundary_index = self._find_signature_index(
request.contents,
record.boundary_signature,
record.boundary_occurrence,
)
if boundary_index is None:
return False
start = (self._session_memory_compaction_start(boundary_index)
if record.source == "session-memory" else self._compaction_start(request.contents, boundary_index))
request.contents = [
self._summary_content(record.summary),
*request.contents[start:],
]
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: _apply_record 用 _find_signature_index 按 boundary_occurrence 精确匹配旧 compaction 记录,但 session-memory 分支的 _persist_session_compaction 经 compact_events 已把 boundary 事件本身替换为 summary 事件,active 窗口与后续模型请求中不再存在该 boundary 签名;下次请求 _apply_record 必然返回 False(签名仅在第一次请求中可匹配一次)。

触发条件: 会话第二次进入 _apply_scoped 且 state.latest_compaction 非空(source 为 session-memory 或 legacy);此时 request 已含摘要前缀,reapplied=False,_compact_with_summary 只替换摘要之后的少量内容,request_chars_after >= request_chars_before 触发 raise ValueError("Advanced Auto Compact did not reduce request size")。

实际影响: 每次成功 compaction 后的下一轮请求都会计入一次失败,consecutive_failures 递增,达到 max_failures 后会话被 ADVANCED_AUTOCOMPACT_BLOCKED_MESSAGE 硬阻断(阻断消息经模型过滤器直接返回给用户),正常对话流程被永久打断,且阻断状态仅保存在进程内 _states。

修正方向: replay 时对已摘要化的请求跳过再次 compaction 的尺寸校验(例如识别 reapplied==True 且请求前缀已是摘要时直接返回),并让 _find_signature_index 在匹配失败时退回 _find_last_signature_index/摘要锚点而非计入失败。

Comment on lines 604 to +676
@@ -634,39 +649,62 @@ async def _persist_checkpoint(
document.key_results,
document.worklog,
)
await self._runtime.transcripts.append_unique(
session_id,
{
"schema_version": SESSION_MEMORY_CHECKPOINT_SCHEMA_VERSION,
"kind": "session-memory-checkpoint",
"checkpoint_id": f"session-memory:{last_event_id}",
"first_event_id": first_event_id,
"last_event_id": last_event_id,
"processed_events": len(included_records),
"non_empty_sections": sum(1 for value in values if value.strip()),
"session_memory_chars": len(document.to_markdown()),
"context_tokens": context_tokens,
},
unique_key="checkpoint_id",
boundary = self._boundary_for_event(session, last_event_id)
if boundary is None:
raise ValueError(f"Session Memory boundary Event {last_event_id} has no visible content")
signature, occurrence = boundary
checkpoint = {
"first_event_id": first_event_id,
"last_event_id": last_event_id,
"recorded_at": included_records[-1].get("recorded_at"),
"last_event_timestamp": included_records[-1].get("event", {}).get("timestamp"),
"boundary_signature": signature,
"boundary_occurrence": occurrence,
"processed_events": len(included_records),
"non_empty_sections": sum(1 for value in values if value.strip()),
"updated_at": datetime.now(timezone.utc).isoformat(),
}
payload = build_session_memory_state(
document,
checkpoint=checkpoint,
context_tokens=context_tokens,
)
state_delta = {SESSION_MEMORY_STATE_KEY: payload}
session_service = getattr(ctx, "session_service", None)
if session_service is None:
session.state.update(state_delta)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: SessionMemoryExtractor._persist_checkpoint 通过 _boundary_for_event 将 checkpoint 的 last_event_id 反查为 (boundary_signature, boundary_occurrence),当 checkpoint 边界事件已被 compact_events 移入 historical_events 后(advanced compaction 正是把旧事件归档的手段),该事件在 active 窗口不可见,_boundary_for_event 返回 None 并抛出 ValueError;_event_records_after_checkpoint 的 timestamp 恢复分支返回的新事件又会被再次 _persist_checkpoint 依赖同一丢失的边界。

触发条件: 会话先完成一次 session-memory 提取(checkpoint 落在较早事件上),随后 advanced compaction 归档了该 checkpoint 事件;后续每次 extract_if_needed 都因边界不可解析而抛错(被 catch 仅记日志),checkpoint 永不前移。

实际影响: Session Memory 的增量游标永久停滞,之后的会话记忆不再更新(新记录与提取状态陈旧),同时每次提取都留下一条异常日志;该行为由本变更引入(旧实现以 last_event_id 直接比较、不依赖事件仍然可见)。

修正方向: 归档 compaction 事件时保留边界映射(如给 compact_events 增加返回被归档边界的元数据,或在 checkpoint 中直接持久化 boundary_signature/boundary_occurrence),使 _boundary_for_event 在事件不可见时也能解析 checkpoint。

Comment on lines +134 to +137
async def read_topic(self, topic_name: str) -> str | None:
path = self._paths.memory_topic_path(topic_name)
return await asyncio.to_thread(lambda: path.read_text(encoding=self._config.encoding)
if path.exists() else None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: LongTermMemoryStore.read_topic(local 文件后端)直接 path.read_text 返回文件内容,完全不检查 memory_ttl_seconds 过期;而 Redis(_redis_stores.py)与 SQL(_sql_stores.py)后端在创建时都进行 TTL 过期检查,同一 read_topic 入口因后端不同行为不一致。

触发条件: 用户配置 memory_ttl_seconds 且以 StorageProvider.LOCAL 读取 topic 文件。

实际影响: 过期记忆在 local 后端仍被当作有效长期记忆注入代理上下文,可能把陈旧/错误信息当作事实使用,且同一配置在 Redis/SQL 后端会正常过期删除、行为跨后端不一致。

修正方向: 在 read_topic 返回前对照 memory_ttl_seconds 校验文件修改时间(或读取文件头部的 updated_at),过期时删除文件并返回 None,与 Redis/SQL 后端语义对齐。

Comment on lines 242 to 251
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: _latest_usage_baseline 读取的 advanced_memory_request_context_fingerprint(写入方是旧 AdvancedMemorySessionService,本变更将其重构/删除后无任何代码再写入该 key),因此 fingerprint 与 last_usage 序列永远不会来自同一次写入,baseline 永久不命中,函数每次都会重建 RequestMetrics 并重新评估整个历史调用序列。

触发条件: advanced_memory 被启用且 metrics_threshold/baseline_window 类门控条件配置了需要 _latest_usage_baseline 生效的预算管理。

实际影响: 内存预算门控退化:每次请求都重新计算基线、无法跨请求保持稳定基线,预算控制与节流行为不可预期,且每次调用都丢弃历史、评估结果不稳定。

修正方向: 将 fingerprint 的写入方迁移到新的 advanced memory 会话服务/提取器写入路径(与 last_usage 同一写入点),或改为按会话 id 直接读取而不依赖 fingerprint。

Comment on lines +40 to +60

@override
async def _before(self, ctx: AgentContext, req: Any, rsp: FilterResult):
"""Run the advanced auto compact summarizer filter."""
if ctx.get_metadata(INTERNAL_COMPACTION_METADATA_KEY, False):
return None
invocation_ctx: InvocationContext = get_invocation_ctx()
summarizer_manager = self.get_summarizer_manager(invocation_ctx)
result = await summarizer_manager.create_session_summary_before_model(
req,
invocation_ctx,
)
if not result:
return None
invocation_ctx.end_invocation = True
rsp.rsp = result
rsp.is_continue = False
rsp.error = None
session_memory_extractor = summarizer_manager.get_session_memory_extractor()
if session_memory_extractor is not None:
await session_memory_extractor.extract_if_needed(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: AdvancedAutoCompactSummarizerFilter._before 在压缩返回非空结果时设置 invocation_ctx.end_invocation = True 并短路模型调用,但没有设置 start_invocation;全仓库(含 _base.py 的断言与调用方过滤链)要求 start_invocation and end_invocation 校验通过才放行,短路路径使校验不通过,后续调用被过滤链继续拦截。

触发条件: 通过 AdvancedAutoCompactSummarizerFilter 且本次请求触发了压缩过滤链(压缩结果非空时短路)。

实际影响: 正常触发压缩之后的下一次模型调用被该过滤器意外拦截/阻断,代理无法继续对话或任务执行,行为与设计意图(压缩后应放行模型调用)相悖。

修正方向: 在 _before 里与 end_invocation 同时设置 start_invocation = True(或让短路不走过滤器、直接返回未设置状态),保证校验语义一致。

@CongkeChen CongkeChen closed this Sep 15, 2026
@CongkeChen CongkeChen reopened this Sep 15, 2026
@CongkeChen
CongkeChen force-pushed the feature/advanced_memory_service branch from debbecd to c4504e9 Compare September 15, 2026 02:29
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

通过

审查综述

范围与计划符合性: 审查覆盖 f05797d..debbecd(Feature/advanced memory service,111 文件 +6187/-6574,23 个测试文件,0 生成文件)。变更将旧 advanced_memory/* 代码迁移重构为 sessions/compact/{default,advanced}/ 与 tools/advanced_memory/,实现 Session 压缩双轨制(默认 AFTER_TURN 摘要 + 高级 BEFORE_MODEL 自动压缩)、Session Memory 提取、long-term memory 三存储后端(local/Redis/SQL)与请求指纹机制。整体结构与计划一致:compact_events、update_session_state、get_for_update、store_historical_events 等新接口均有配套实现与示例;旧 SessionSummarizer/SessionSummary 等以别名保留,ClawSummarizerSessionManager 兼容(继承别名且默认 AFTER_TURN)。

主要风险(由本次变更引入):

  1. 请求指纹断链(MODERATE):record_request_context 只把指纹写入 session.state,而 _latest_usage_baseline 读取 Event custom_metadata 中的 advanced_memory_request_context_fingerprint,全仓无任何自动写入方(仅测试手工写入),usage-baseline 退化为仅按内容指纹匹配,同内容不同上下文请求被错误基线,token 模式预算偏差、熔断判断失真。
  2. Session Memory 全量注入(MODERATE):advanced 管理器 get_session_summary 每次请求把整份 Session Memory markdown 拼入 system instructions(经 _request_processor 注入点),与 summary event 及 advanced-memory index 内容重叠,长会话下上下文窗口与 token 计费线性放大。
  3. 历史事件不可恢复(MODERATE):InMemory 服务 _set_session 在默认 store_historical_events=False 下每次写入清零 historical_events(update_session 亦经 _set_session),与示例 README"被替换的原始 events 移入 historical_events/原始事件仍可完整追溯"承诺矛盾,且与 SQL/Redis 后端行为不一致。
  4. 每轮强制 memory 提取(LOW):_apply_scoped 每次压缩尝试强制 extract_if_needed(force=True),绕过阈值,失败重试期重复触发 LLM 提取。
  5. 熔断状态进程本地(LOW):consecutive_failures/latest_compaction 仅存进程内存,重启后熔断保护丢失。
  6. 剪枝状态进程本地(LOW):snipped_ids/cleared_ids/result_hashes 仅存进程内存,重启后已剪枝的大段 tool 结果重新完整注入。
  7. 默认摘要器计数语义变化(LOW):conversation_count 重置从 threshold 谓词移到 manager 成功分支,总结失败时计数不再重置,行为从"每 N 轮总结"变为"失败后每轮重试"。
  8. SQL expire_on_commit=False 全局生效(LOW):仅 4 条内部路径补 refresh(),外部读 ORM 对象时机可见 stale 数据。
  9. Redis 读路径逐 key 往返(LOW):_refresh_ttl_group 逐 key expire + read_index 逐条目 exists,热路径 O(N)~O(N²) 网络往返。

测试充分性: 23 个测试文件覆盖 token budget、auto-compact、history snip、提取器、三个存储后端与默认压缩器;test_token_budget.py 手工写入 custom_metadata 恰好反向印证指纹断链(无自动写入方);尚无针对 get_session_summary 注入体积、熔断跨重启持久化、conversation_count 失败重试语义、historical_events 默认配置下可恢复性的测试。

门禁结论: 无 SEVERE 级缺陷;存在 3 个 MODERATE(指纹断链、Session Memory 全量注入、历史事件不可恢复)与 6 个 LOW,均不影响主流程正确性但有真实成本/数据可追溯性影响,建议合入前优先修复 MODERATE 项。审查方式为静态只读分析(git 范围 diff + 调用链追踪 + 全局 grep),未运行测试(环境无法导入 pydantic 依赖),评论均锚定到本次变更的变更后行。

发现的问题

中等

trpc_agent_sdk/sessions/compact/advanced/_token_budget.py:242-251

问题: 请求指纹写读断链:record_request_context(_token_budget.py:242-251)只把指纹写入 session.state["advanced_memory_pending_request_context_fingerprint"],而 _latest_usage_baseline 读取的是 Event custom_metadata 中的 advanced_memory_request_context_fingerprint。全仓 grep 无任何自动写入方(旧 advanced_memory/_session_service.py 的 stamp 逻辑随本次迁移删除),仅测试文件手工写入该字段。

触发条件: 启用 token 模式(TokenContextTrackerConfig.enabled=True 且设置 model_context_window_tokens)后发起请求;凡是请求内容与历史 usage event 内容指纹相同(如重复提问、相同 tool 结果)的请求都会命中基线匹配。

实际影响: usage-baseline 退化为仅按内容指纹匹配,把不同上下文(不同 system 指令、不同历史)但同内容的请求错误归并为同一基线,ContextTokenEstimate 的 token 预算偏差,进而影响 warning/blocking 阈值判断:上下文被低估时可能越过阻塞阈值引发熔断误触发,或高估时压缩不及时。

修正方向: 在响应 Event 持久化时把 state 中暂存的指纹 stamp 到 event.custom_metadata["advanced_memory_request_context_fingerprint"](恢复旧 _session_service.py 行为),或在 _latest_usage_baseline 中读取 state 暂存值。

中等

trpc_agent_sdk/sessions/compact/advanced/_manager.py:96-101

问题: advanced 压缩器通过既有摘要 API 把 Session Memory 全文注入每次模型请求:get_session_summary(_manager.py:96-101)返回 session.state[SESSION_MEMORY_STATE_KEY] 的全部 section markdown(to_markdown 连空 section 也会渲染),而调用链上 _request_processor 每次请求都会把 get_session_summary 的结果拼进 system instructions。

触发条件: 使用 AdvancedAutoCompactSummarizerManager(BEFORE_MODEL)且提取器已产出过 Session Memory;每个请求(含 tool 调用后的再次请求)都会把整份 700k 字符量级的 memory 全文拼入 instructions。

实际影响: 与压缩后的 summary event、advanced-memory index 注入内容三重重叠,长会话下上下文窗口与 token 计费随请求数线性放大,且覆盖了原有 summary 的"简要历史"语义;全量记忆注入还引入与模型响应再压缩之间的循环干扰。

修正方向: 对 Session Memory 做截断/分层(如仅注入 index 或截取最相关 section),或在请求内容已含 memory 信息时跳过注入。

中等

trpc_agent_sdk/sessions/_in_memory_session_service.py:272-291

问题: 本次新增的 update_session_state(_in_memory_session_service.py:272-291)只 patch stored.session.state,未更新 stored.session.events/historical_events/conversation_count,而 compact_events 换入的原始事件依赖 historical_events 持久化;且服务内仍存在的 _set_session 在默认 store_historical_events=False 下每次写入清零 historical_events(update_session 亦经 _set_session),与本次示例 README 承诺"被替换的原始 events 移入 historical_events/原始事件仍可完整追溯"矛盾。

触发条件: 使用 InMemorySessionService 且未显式设置 SessionServiceConfig(store_historical_events=True)(examples/session_summarizer_with_advanced/run_agent.py 正是此默认配置)时触发一次压缩换出。

实际影响: 默认配置下压缩换出的原始事件无法恢复,与 SQL/Redis(强制 store_historical_events=True)后端行为不一致,用户无法追溯压缩前原始对话细节;同时 update_session_state 与 update_session 的语义分裂会让 state 与 events 更新路径不一致。

修正方向: 让 _set_session 在存储时保留已存在的 historical_events(仅初始化时空置),或使 update_session_state 与新压缩链路同步持久化 historical_events。

较低

trpc_agent_sdk/sessions/compact/advanced/_auto_compact.py:693-697

问题: _apply_scoped 在每次压缩尝试时无条件执行 extract_if_needed(ctx, force=True),force=True 会跳过 extractor 的 threshold/tool_condition 检查(_compaction_memory_extractor.py 中 if not force and (...) 分支),只保留 no-new-events/unsafe-boundary/coordination-timeout 守卫。

触发条件: 会话进入自动压缩窗口(char 模式 ≥700k)后的每个请求,包括 consecutive_failures 已超过 max_failures 的失败重试期(此时压缩尝试被提前返回,但 extraction 仍在循环内被执行)。

实际影响: 每次压缩尝试都触发一轮 Session Memory 提取 LLM 调用(绕过阈值),提取失败后每轮请求重复触发,token 成本与延迟随失败次数线性增长,与 max_failures 熔断设计的初衷(失败后停止重试)相悖。

修正方向: 仅在检查点有新增事件时才用 force=True 调用 LLM(或第一轮用 force=False、由 extraction 结果决定),熔断/无新事件时跳过 extraction。

较低

trpc_agent_sdk/sessions/compact/advanced/_auto_compact.py:210-216

问题: 熔断状态仅存进程内存:_load_state(_auto_compact.py:210-216)返回的 AdvancedAutoCompactState(latest_compaction, consecutive_failures) 只保存在 self._states 字典,从不持久化;_apply_record 重放与 consecutive_failures 计数均依赖该状态。

触发条件: 进程重启 / 进程内状态重置后,同一会话继续运行并再次达到 blocking_chars(默认 780k)。

实际影响: 重启后 consecutive_failures 归零、latest_compaction 丢失,之前连续失败的会话不再被 blocked=True 熔断保护,阻塞消息与重复压缩噪音重现,高成本压缩重试再次触发。

修正方向: 将 latest_compaction/consecutive_failures 持久化到 session.state,重启后从 state 恢复。

较低

trpc_agent_sdk/sessions/compact/advanced/_history_snip.py:106-117

问题: 工具结果剪枝状态仅存进程内存:HistorySnipState(snipped_ids, result_hashes)(_history_snip.py:106-117)与 MicroCompactState.cleared_ids(_micro_compact.py:94-104)都只存于进程内 self._states 字典。

触发条件: 进程重启后同一会话再次请求(tool 结果 id 仍相同,如相同工具调用重放)并超过 snip 阈值。

实际影响: 重启后已剪枝的大段 tool 结果(可达数十万字符)被重新完整注入模型上下文,上下文窗口压力骤增、token 计费放大;result_hashes 丢失同时使"id 复用不同内容"防篡改校验无法跨重启生效。

修正方向: 将 snipped_ids/cleared_ids/result_hashes 持久化(写入 session.state 或 memory 索引),重启后恢复;或给占位响应打上可识别标记使重放可自愈。

较低

trpc_agent_sdk/sessions/compact/default/_checker.py:138-141

问题: 默认摘要器的轮次计数语义被改变:base 版本(f05797d9 的 sessions/_summarizer_checker.py:140-141)在 set_summarizer_conversation_threshold 命中阈值时执行 session.conversation_count = 0 后再返回 True,而本次迁移后的 _checker.py:140-141 只返回谓词结果不再重置;重置被移入 _summarizer_manager.py:118 且仅在 create_session_summary 成功产出摘要后执行。

触发条件: 默认摘要器(AFTER_TURN)会话中每次总结失败(LLM 出错、摘要为空或 should_summarize 之外的异常路径)。

实际影响: 总结失败时计数不再归零,行为从"每 N 轮总结一次"退化为"失败后每轮都重试总结",失败的场景下每轮触发一次昂贵的摘要 LLM 调用;计数语义与历史行为不兼容。

修正方向: 在 should_summarize_session 判定后即重置计数(无论成败),或保留 threshold 谓词内的重置语义。

较低

trpc_agent_sdk/sessions/_sql_session_service.py:406

问题: SqlSessionService.__init__ 全局 kwargs.setdefault("expire_on_commit", False)(_sql_session_service.py:406)关闭了 ORM 每事务自动过期,但代码只在 append_event/update_session_state/_get_session/list 等少数内部路径补了 refresh()(约 783 行注释亦承认该约束),其余读写路径未覆盖。

触发条件: 外部调用方在服务方法返回后继续读取同一 ORM 对象(如 update_session 后读 storage_session.state、或经 _get_storage_session 复用对象),期间另一并发事务已提交新值。

实际影响: 读到 stale 属性而非检测到过期重载:并发更新下读到旧 state/timestamp,get_for_update 加锁路径的 stale 检测依赖调用方自行传对对象,容易漏掉更新。

修正方向: 在每个改写路径统一 refresh(),或在关键读路径显式 expire()/查询时 populate_existing。

较低

trpc_agent_sdk/tools/advanced_memory/_redis_stores.py:100-116

问题: Redis 读路径逐 key 网络往返:_refresh_ttl_group(_redis_stores.py:100-116)对组内每个 key 单独发 expire 命令(N+1 往返),read_index(_redis_stores.py:137-157)对索引逐条目 exists 检查再 prune。

触发条件: 用户记忆条数 N 较大(数十/上百条)时每次 read_index/read_topic/topic 列表刷新都触发 O(N)~O(N²) 次串行 Redis 往返;且本次实现未使用 pipeline。

实际影响: 记忆读取热路径(每轮注入 advanced-memory index 前)延迟随记忆规模线性放大,长会话用户延迟明显,且每个请求都重复刷新 TTL,放大负载。

修正方向: 用 pipeline 合并 expire/exists 批处理,或仅在写入路径维护 TTL、读路径用一次 EXISTS 查询剪枝。

Comment on lines 242 to 251
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: 请求指纹写读断链:record_request_context(_token_budget.py:242-251)只把指纹写入 session.state["advanced_memory_pending_request_context_fingerprint"],而 _latest_usage_baseline 读取的是 Event custom_metadata 中的 advanced_memory_request_context_fingerprint。全仓 grep 无任何自动写入方(旧 advanced_memory/_session_service.py 的 stamp 逻辑随本次迁移删除),仅测试文件手工写入该字段。

触发条件: 启用 token 模式(TokenContextTrackerConfig.enabled=True 且设置 model_context_window_tokens)后发起请求;凡是请求内容与历史 usage event 内容指纹相同(如重复提问、相同 tool 结果)的请求都会命中基线匹配。

实际影响: usage-baseline 退化为仅按内容指纹匹配,把不同上下文(不同 system 指令、不同历史)但同内容的请求错误归并为同一基线,ContextTokenEstimate 的 token 预算偏差,进而影响 warning/blocking 阈值判断:上下文被低估时可能越过阻塞阈值引发熔断误触发,或高估时压缩不及时。

修正方向: 在响应 Event 持久化时把 state 中暂存的指纹 stamp 到 event.custom_metadata["advanced_memory_request_context_fingerprint"](恢复旧 _session_service.py 行为),或在 _latest_usage_baseline 中读取 state 暂存值。

Comment on lines +96 to +101
async def get_session_summary(self, session: Session) -> str | None:
"""Read compact Session Memory through the existing summary API."""
parsed = parse_session_memory_state(session.state.get(SESSION_MEMORY_STATE_KEY))
if parsed is not None:
return parsed[0].to_markdown()
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: advanced 压缩器通过既有摘要 API 把 Session Memory 全文注入每次模型请求:get_session_summary(_manager.py:96-101)返回 session.state[SESSION_MEMORY_STATE_KEY] 的全部 section markdown(to_markdown 连空 section 也会渲染),而调用链上 _request_processor 每次请求都会把 get_session_summary 的结果拼进 system instructions。

触发条件: 使用 AdvancedAutoCompactSummarizerManager(BEFORE_MODEL)且提取器已产出过 Session Memory;每个请求(含 tool 调用后的再次请求)都会把整份 700k 字符量级的 memory 全文拼入 instructions。

实际影响: 与压缩后的 summary event、advanced-memory index 注入内容三重重叠,长会话下上下文窗口与 token 计费随请求数线性放大,且覆盖了原有 summary 的"简要历史"语义;全量记忆注入还引入与模型响应再压缩之间的循环干扰。

修正方向: 对 Session Memory 做截断/分层(如仅注入 index 或截取最相关 section),或在请求内容已含 memory 信息时跳过注入。

Comment on lines +272 to +291
@override
async def update_session_state(
self,
session: Session,
state_delta: dict[str, Any],
) -> None:
"""Patch stored session state without replacing its Event window."""
if not state_delta:
return
session.state.update(state_delta)

app_sessions = self._sessions.get(session.app_name)
user_sessions = app_sessions.get(session.user_id) if app_sessions else None
stored = user_sessions.get(session.id) if user_sessions else None
if stored is None:
logger.warning("Session %s not found while updating state", session.id)
return
stored.session.state.update(state_delta)
stored.ttl.update_expired_at()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: 本次新增的 update_session_state(_in_memory_session_service.py:272-291)只 patch stored.session.state,未更新 stored.session.events/historical_events/conversation_count,而 compact_events 换入的原始事件依赖 historical_events 持久化;且服务内仍存在的 _set_session 在默认 store_historical_events=False 下每次写入清零 historical_events(update_session 亦经 _set_session),与本次示例 README 承诺"被替换的原始 events 移入 historical_events/原始事件仍可完整追溯"矛盾。

触发条件: 使用 InMemorySessionService 且未显式设置 SessionServiceConfig(store_historical_events=True)(examples/session_summarizer_with_advanced/run_agent.py 正是此默认配置)时触发一次压缩换出。

实际影响: 默认配置下压缩换出的原始事件无法恢复,与 SQL/Redis(强制 store_historical_events=True)后端行为不一致,用户无法追溯压缩前原始对话细节;同时 update_session_state 与 update_session 的语义分裂会让 state 与 events 更新路径不一致。

修正方向: 让 _set_session 在存储时保留已存在的 historical_events(仅初始化时空置),或使 update_session_state 与新压缩链路同步持久化 historical_events。

Comment on lines +693 to +697
if self._session_memory_extractor is not None:
await self._session_memory_extractor.extract_if_needed(
ctx,
force=True,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: _apply_scoped 在每次压缩尝试时无条件执行 extract_if_needed(ctx, force=True),force=True 会跳过 extractor 的 threshold/tool_condition 检查(_compaction_memory_extractor.py 中 if not force and (...) 分支),只保留 no-new-events/unsafe-boundary/coordination-timeout 守卫。

触发条件: 会话进入自动压缩窗口(char 模式 ≥700k)后的每个请求,包括 consecutive_failures 已超过 max_failures 的失败重试期(此时压缩尝试被提前返回,但 extraction 仍在循环内被执行)。

实际影响: 每次压缩尝试都触发一轮 Session Memory 提取 LLM 调用(绕过阈值),提取失败后每轮请求重复触发,token 成本与延迟随失败次数线性增长,与 max_failures 熔断设计的初衷(失败后停止重试)相悖。

修正方向: 仅在检查点有新增事件时才用 force=True 调用 LLM(或第一轮用 force=False、由 extraction 结果决定),熔断/无新事件时跳过 extraction。

Comment on lines +210 to +216
async def _load_state(self, session_id: str) -> AdvancedAutoCompactState:
"""Restore process-local compaction state."""
state_key = self._runtime.session_key(session_id)
state = self._states.get(state_key)
if state is not None:
return state
state = AdvancedAutoCompactState(latest_compaction=None, consecutive_failures=0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: 熔断状态仅存进程内存:_load_state(_auto_compact.py:210-216)返回的 AdvancedAutoCompactState(latest_compaction, consecutive_failures) 只保存在 self._states 字典,从不持久化;_apply_record 重放与 consecutive_failures 计数均依赖该状态。

触发条件: 进程重启 / 进程内状态重置后,同一会话继续运行并再次达到 blocking_chars(默认 780k)。

实际影响: 重启后 consecutive_failures 归零、latest_compaction 丢失,之前连续失败的会话不再被 blocked=True 熔断保护,阻塞消息与重复压缩噪音重现,高成本压缩重试再次触发。

修正方向: 将 latest_compaction/consecutive_failures 持久化到 session.state,重启后从 state 恢复。

Comment on lines 106 to 117
async def _load_state(self, session_id: str) -> HistorySnipState:
"""Restore prior history-snip decisions from the transcript."""
state = self._states.get(session_id)
"""Return process-local history-snip state."""
state_key = self._runtime.session_key(session_id) if hasattr(self._runtime, "session_key") else session_id
state = self._states.get(state_key)
if state is not None:
return state
records = await self._runtime.transcripts.read_all(session_id)
snipped_ids: set[str] = set()
result_hashes: dict[str, str] = {}
for record in records:
result_id = record.get("result_id")
if record.get("kind") != "history-snip" or not isinstance(result_id, str):
continue
snipped_ids.add(result_id)
original_sha256 = record.get("original_sha256")
if isinstance(original_sha256, str):
result_hashes[result_id] = original_sha256
state = HistorySnipState(
snipped_ids=snipped_ids,
result_hashes=result_hashes,
snipped_ids=set(),
result_hashes={},
)
self._states[session_id] = state
self._states[state_key] = state
return state

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: 工具结果剪枝状态仅存进程内存:HistorySnipState(snipped_ids, result_hashes)(_history_snip.py:106-117)与 MicroCompactState.cleared_ids(_micro_compact.py:94-104)都只存于进程内 self._states 字典。

触发条件: 进程重启后同一会话再次请求(tool 结果 id 仍相同,如相同工具调用重放)并超过 snip 阈值。

实际影响: 重启后已剪枝的大段 tool 结果(可达数十万字符)被重新完整注入模型上下文,上下文窗口压力骤增、token 计费放大;result_hashes 丢失同时使"id 复用不同内容"防篡改校验无法跨重启生效。

修正方向: 将 snipped_ids/cleared_ids/result_hashes 持久化(写入 session.state 或 memory 索引),重启后恢复;或给占位响应打上可识别标记使重放可自愈。

Comment on lines 138 to 141

def _decorator(session: Session) -> bool:
if session.conversation_count > conversation_count:
session.conversation_count = 0
return True
return False
return session.conversation_count > conversation_count

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: 默认摘要器的轮次计数语义被改变:base 版本(f05797d9 的 sessions/_summarizer_checker.py:140-141)在 set_summarizer_conversation_threshold 命中阈值时执行 session.conversation_count = 0 后再返回 True,而本次迁移后的 _checker.py:140-141 只返回谓词结果不再重置;重置被移入 _summarizer_manager.py:118 且仅在 create_session_summary 成功产出摘要后执行。

触发条件: 默认摘要器(AFTER_TURN)会话中每次总结失败(LLM 出错、摘要为空或 should_summarize 之外的异常路径)。

实际影响: 总结失败时计数不再归零,行为从"每 N 轮总结一次"退化为"失败后每轮都重试总结",失败的场景下每轮触发一次昂贵的摘要 LLM 调用;计数语义与历史行为不兼容。

修正方向: 在 should_summarize_session 判定后即重置计数(无论成败),或保留 threshold 谓词内的重置语义。

# AsyncSession cannot perform an implicit refresh when an ORM
# attribute is accessed after commit. Keep committed values available
# because this service reads StorageSession state after committing.
kwargs.setdefault("expire_on_commit", False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: SqlSessionService.__init__ 全局 kwargs.setdefault("expire_on_commit", False)(_sql_session_service.py:406)关闭了 ORM 每事务自动过期,但代码只在 append_event/update_session_state/_get_session/list 等少数内部路径补了 refresh()(约 783 行注释亦承认该约束),其余读写路径未覆盖。

触发条件: 外部调用方在服务方法返回后继续读取同一 ORM 对象(如 update_session 后读 storage_session.state、或经 _get_storage_session 复用对象),期间另一并发事务已提交新值。

实际影响: 读到 stale 属性而非检测到过期重载:并发更新下读到旧 state/timestamp,get_for_update 加锁路径的 stale 检测依赖调用方自行传对对象,容易漏掉更新。

修正方向: 在每个改写路径统一 refresh(),或在关键读路径显式 expire()/查询时 populate_existing。

Comment on lines +100 to +116
ttl: int | None,
skip_prefixes: tuple[str, ...] = (),
) -> None:
"""Track and refresh every key in one logical memory group."""
if ttl is None:
return
if keys:
await self._command("sadd", registry, *keys)
tracked = await self._command("smembers", registry) or []
tracked_keys = {self._text(value) for value in tracked}
tracked_keys.update(keys)
for key in tracked_keys:
if key and not key.startswith(skip_prefixes):
await self._command("expire", key, ttl)
await self._command("expire", registry, ttl)

async def _refresh_memory_ttl(self, *keys: str) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: Redis 读路径逐 key 网络往返:_refresh_ttl_group(_redis_stores.py:100-116)对组内每个 key 单独发 expire 命令(N+1 往返),read_index(_redis_stores.py:137-157)对索引逐条目 exists 检查再 prune。

触发条件: 用户记忆条数 N 较大(数十/上百条)时每次 read_index/read_topic/topic 列表刷新都触发 O(N)~O(N²) 次串行 Redis 往返;且本次实现未使用 pipeline。

实际影响: 记忆读取热路径(每轮注入 advanced-memory index 前)延迟随记忆规模线性放大,长会话用户延迟明显,且每个请求都重复刷新 TTL,放大负载。

修正方向: 用 pipeline 合并 expire/exists 批处理,或仅在写入路径维护 TTL、读路径用一次 EXISTS 查询剪枝。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

不通过

本次审查覆盖 base_commit f05797d..head_commit c4504e9(计划:Feature/advanced memory service,111 个文件、+6182/-6578),重点检查 advanced memory 重构(advanced_memory 包拆分为 tools/advanced_memory 与 sessions/compact/advanced)及 Session Compact 的默认/高级两条路径。

审查方法:直接阅读变更前后代码对比(git show/diff)验证每个候选的触发条件与实际影响,并交叉核对各 finder 代理的输出,排除伪报(Redis list_topics 空、ToolResultBudget 重启后误抛 ValueError、SQL update_session_state 锁覆盖不足、circuit-breaker 死分支等均已被证据否定)。

核心结论:

  1. 严重缺陷(SEVERE):新实现删除了 base 版本的 transcript 持久化机制(compaction 记录、失败计数、session-memory checkpoint 均依靠进程内 _states),同时 Session.compact_events 的幂等键 compaction_id 每次调用都重新生成 UUID、永不命中持久化事件。进程重启/崩溃后会导致对已压缩上下文再次压缩(双重压缩、嵌套 summary、信息丢失),且 circuit-breaker 失效。
  2. 中等缺陷:session-memory 提取只读活跃事件窗口(base 从 append-only transcript 读取),compaction 后未提取的增量永久不可达;set_summarizer_conversation_threshold 移除触发时重置,summarize 失败后每轮无限重试;BEFORE_MODEL(主事件循环)与 AFTER_TURN(post-turn 独立线程循环)跨循环共享 asyncio.Lock 导致并发保护失效。
  3. 低严重度:token fingerprint 写入无消费者(dead state)、SQL update_session 无行锁重写事件与并发 append 竞争(已按校验锚定到 553-556 的变更行)。

测试影响:base 的 tests/advanced_memory/(454+516+334 行覆盖 circuit-breaker、重启重放、checkpoint 恢复、路径穿越防护)被删除,新 tests/sessions/compact/test_session_compact.py(268 行)未覆盖这些行为,重启重放与熔断回归无测试保护。

门禁结论:FAILED——存在已验证的数据丢失高风险缺陷(跨重启重复压缩),应予修复后合入。

发现的问题

严重

trpc_agent_sdk/sessions/compact/advanced/_auto_compact.py:210-218

问题: 新实现删除了 base 版本通过 transcript 持久化 compaction 状态的能力:base 的 AdvancedAutoCompact._load_state 从转录记录恢复 latest_compaction 与 consecutive_failures(_persist_success/_persist_failure 写盘),新代码 _load_state 仅查进程内 self._states,两次 persist 相关方法与 transcript 读写均被移除。

触发条件: 进程重启/崩溃/多实例切换后继续同一 session:state.latest_compaction 为空且无持久化记录可恢复;携带 session_compaction_id 的 summary 事件虽已持久化在 events 中,但不会被识别为已 compact。

实际影响: (a) 下一轮 _apply_scoped 无记录可重放(_apply_record 返回 False),基于已含 summary 的恢复窗口再次执行 compaction——对已摘要内容二次压缩,嵌套 summary,历史细节永久丢失;(b) consecutive_failures 归零,circuit-breaker 熔断保护失效,模型上下文超限的请求会被直接发给模型导致调用失败。

修正方向: 恢复持久化恢复机制:将 compaction 成功/失败记录(boundary signature/occurrence、summary、compaction_id、失败计数)写入持久化会话状态(例如 session.state 下的版本化键,或恢复 transcript 写入),_load_state 在 _states 未命中时从该持久化源重建。

中等

trpc_agent_sdk/sessions/_session.py:157-163

问题: Session.compact_events 的幂等保护是死代码:_compact_with_summary 每次生成新 compaction_id = f"advanced_autocompact:{uuid.uuid4().hex}",_persist_session_compaction 中 record.compaction_id or 新 uuid 恒取前者,因此每次压缩携带全新 id,compact_events 中 metadata.get("session_compaction_id") == compaction_id 不可能匹配任何已持久化 summary 事件。

触发条件: 任何跨对象/跨进程的重复压缩场景(一次 update_session 成功但调用方未观察到、或 session 从存储重建后再次 compact),幂等检查都不会拦截第二次压缩。

实际影响: “使重试幂等”的文档承诺无法兑现;配合状态不持久化缺陷,重试/重建场景会对同一个前缀执行两次压缩,产生嵌套 summary 并丢失中间细节。

修正方向: 让 compaction_id 从持久化且确定的源派生(例如 sha256(session_id + 边界签名 + 边界 occurrence),或复用被替换前缀中首个事件的 id),使重试时生成与首次相同的 id,幂等分支才能真正命中。

中等

trpc_agent_sdk/sessions/compact/advanced/_compaction_memory_extractor.py:303-330

问题: session-memory 提取的事件来源从 base 的 append-only transcript(含全部历史事件)改为仅构造 session.events 活跃窗口(_session_event_records 遍历 session.events 并显式跳过 summary 事件),而 checkpoint 只存放在 session.state["_trpc_agent:summary"]。一旦 compaction 把 checkpoint 边界之前的事件移入 historical_events,_event_records_after_checkpoint 在活跃窗口找不到 checkpoint_event_id,只能退回按 recorded_at(实际是事件时间戳)近似恢复,找不到时直接返回空列表并跳过提取。

触发条件: BEFORE_MODEL 路径中 extract_if_needed(force=True) 提取失败(LLM 生成错误、预算超限等)后 compaction 仍继续执行,或 checkpoint 边界事件已被 compaction 移出活跃窗口的正常流程。

实际影响: checkpoint 之后、compaction 之前尚未提取的事件随 compaction 进入 historical_events 后永远无法被提取,会话记忆永久遗漏该段信息;recorded_at 时间戳近似恢复还可能重复提取已处理内容。

修正方向: 提取的增量边界不应依赖易失的活跃窗口:将 checkpoint(last_event_id 等)持久化到独立于 events 重排的源(如 session.state 中不与 compaction 冲突的键),并让 _session_event_records 同时纳入 historical_events 中 checkpoint 之后的事件,或恢复 transcript 机制。

中等

trpc_agent_sdk/sessions/compact/default/_checker.py:137-140

问题: set_summarizer_conversation_threshold 从 base 的“超过阈值时把 session.conversation_count = 0 并返回 True”改为仅返回 session.conversation_count > conversation_count,触发时重置逻辑被删除;而 DefaultSessionSummarizerManager.create_session_summary 只在 summary_text 成功时重置 conversation_count = 0,runner 每轮无条件 conversation_count += 1。

触发条件: 会话轮数超过阈值后,summarize 失败(LLM 调用异常、返回空摘要或 create_session_summary_by_events 捕获异常返回 None):计数不会归零,下一轮 should_summarize_session 依旧返回 True。

实际影响: 每次失败后的每一轮对话都会重复触发一次 summarize LLM 调用(base 版本失败后计数被 checker 清零,需重新累积到阈值才再次尝试),模型持续故障时产生无界的重复调用与日志噪音。

修正方向: 在 manager 的 summarize 失败路径(summary_text 为空/异常)也重置 conversation_count = 0,或恢复 checker 触发时的重置语义,保证失败后至少间隔一个阈值周期再重试。

中等

trpc_agent_sdk/sessions/compact/advanced/_auto_compact.py:201-208

问题: _apply_scoped/ToolResultBudget/HistorySnip/MicroCompact 的 _session_lock 使用 asyncio.Lock,但新 manager 同时支持 BEFORE_MODEL(主事件循环内)与 AFTER_TURN(_PostTurnWorkerThread 独立线程+独立事件循环,见 runners.py L49-52)两条调用路径;base 版本 autocompact 只经 before_model_callback 单循环执行,不存在该跨循环共享。

触发条件: defer_post_turn_processing=True 时,上一个 turn 的 AFTER_TURN 压缩在 worker 循环异步执行期间,用户发起同一 session 的下一 turn,BEFORE_MODEL 压缩在主循环并发进入临界区(Python 3.10+ 的 asyncio.Lock 不绑定循环但状态非线程安全,3.6-3.9 则直接抛跨循环错误)。

实际影响: 同一 session 的两次压缩/写操作并发执行,_states/session.events 竞态,可能产生重复压缩或持久化事件窗口不一致。

修正方向: 与 SessionOperationCoordinator 一致改用跨循环安全的线程锁(如 CrossLoopLock)作为 session 级互斥,或仅在单一循环下执行压缩。

较低

trpc_agent_sdk/sessions/compact/advanced/_token_budget.py:242-252

问题: TokenContextTracker.record_request_context 把 fingerprint 写入 session.state["advanced_memory_pending_request_context_fingerprint"],但全仓库没有任何代码把这些 pending fingerprint 迁移到事件的 custom_metadata["advanced_memory_request_context_fingerprint"];_latest_usage_baseline 读取的 recorded_fingerprint 恒为 None,静态 fingerprint 不一致过滤分支永远不会执行。

触发条件: 每次模型请求都会向 session.state 写入该键并随 state 持久化;_latest_usage_baseline 在匹配最近带 usage_metadata 的事件时跳过 fingerprint 校验。

实际影响: (a) usage 基线可能匹配到模型/系统指令/工具配置不同时期的旧 usage 事件,token 预算估计偏差,导致提前或延迟触发压缩;(b) .state 中残留无消费者的失效键,污染持久化状态。

修正方向: 在事件落库时把 pending fingerprint 写入事件 custom_metadata 并清理 state 中该键,或删除 record_request_context 及指纹过滤分支,仅依赖内容 fingerprint 匹配。

较低

trpc_agent_sdk/sessions/_sql_session_service.py:553-556

问题: 本次变更给 append_event/update_session_state 新增了 get_for_update 行锁(本文件 553-556、629-662 行的变更),但同为会话写路径的 update_session 仍用普通 get 读取(不带锁),而 advanced compaction 通过 _persist_session_compaction → ctx.session_service.update_session 全量重写会话事件(delete 全部行后重新插入),与加了行锁的 append_event 并发执行时缺少同一把锁。

触发条件: 多实例/多事件循环下,一个请求的 compaction 执行 update_session(删除并重写 events 行)的同时,另一个请求对该 session 执行 append_event 插入新事件。

实际影响: 事务交错时并发 append 的新事件行可能被 compaction 的 delete-all 删除,或 compaction 写入的窗口覆盖并发 append 的事件,活动窗口静默丢失事件(base 版本 advanced compact 只写 transcript、不重写 events,此写路径为本变更新引入)。

修正方向: update_session 改用 get_for_update 并在事务内完成删除与重写,或为 compaction 持久化提供补丁式更新(仅替换被压缩前缀对应的事件行),避免全量重写与并发 append 竞争。

Comment on lines +210 to +218
async def _load_state(self, session_id: str) -> AdvancedAutoCompactState:
"""Restore process-local compaction state."""
state_key = self._runtime.session_key(session_id)
state = self._states.get(state_key)
if state is not None:
return state
state = AdvancedAutoCompactState(latest_compaction=None, consecutive_failures=0)
self._states[state_key] = state
return state

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: 新实现删除了 base 版本通过 transcript 持久化 compaction 状态的能力:base 的 AdvancedAutoCompact._load_state 从转录记录恢复 latest_compaction 与 consecutive_failures(_persist_success/_persist_failure 写盘),新代码 _load_state 仅查进程内 self._states,两次 persist 相关方法与 transcript 读写均被移除。

触发条件: 进程重启/崩溃/多实例切换后继续同一 session:state.latest_compaction 为空且无持久化记录可恢复;携带 session_compaction_id 的 summary 事件虽已持久化在 events 中,但不会被识别为已 compact。

实际影响: (a) 下一轮 _apply_scoped 无记录可重放(_apply_record 返回 False),基于已含 summary 的恢复窗口再次执行 compaction——对已摘要内容二次压缩,嵌套 summary,历史细节永久丢失;(b) consecutive_failures 归零,circuit-breaker 熔断保护失效,模型上下文超限的请求会被直接发给模型导致调用失败。

修正方向: 恢复持久化恢复机制:将 compaction 成功/失败记录(boundary signature/occurrence、summary、compaction_id、失败计数)写入持久化会话状态(例如 session.state 下的版本化键,或恢复 transcript 写入),_load_state 在 _states 未命中时从该持久化源重建。

Comment on lines +157 to +163

boundary_index = next(
(index for index, event in enumerate(self.events) if event.id == boundary_event_id),
None,
)
if boundary_index is None:
raise ValueError(f"Session compaction boundary Event {boundary_event_id!r} "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: Session.compact_events 的幂等保护是死代码:_compact_with_summary 每次生成新 compaction_id = f"advanced_autocompact:{uuid.uuid4().hex}",_persist_session_compaction 中 record.compaction_id or 新 uuid 恒取前者,因此每次压缩携带全新 id,compact_events 中 metadata.get("session_compaction_id") == compaction_id 不可能匹配任何已持久化 summary 事件。

触发条件: 任何跨对象/跨进程的重复压缩场景(一次 update_session 成功但调用方未观察到、或 session 从存储重建后再次 compact),幂等检查都不会拦截第二次压缩。

实际影响: “使重试幂等”的文档承诺无法兑现;配合状态不持久化缺陷,重试/重建场景会对同一个前缀执行两次压缩,产生嵌套 summary 并丢失中间细节。

修正方向: 让 compaction_id 从持久化且确定的源派生(例如 sha256(session_id + 边界签名 + 边界 occurrence),或复用被替换前缀中首个事件的 id),使重试时生成与首次相同的 id,幂等分支才能真正命中。

Comment on lines +303 to +330
def _session_event_records(self, session: Session) -> list[dict[str, Any]]:
"""Convert the authoritative Session Events into extraction records."""
records: list[dict[str, Any]] = []
seen: set[str] = set()
# Archived Events are no longer addressable in the active model
# request. Their information is already represented by the active
# summary Event included in the extraction context.
events = list(session.events or [])
for event in events:
if event.is_summary_event and event.is_summary_event():
continue
if event.id in seen:
continue
seen.add(event.id)
timestamp = float(event.timestamp or 0.0)
records.append({
"kind":
"event",
"event_id":
event.id,
"recorded_at":
datetime.fromtimestamp(
timestamp,
tz=timezone.utc,
).isoformat(),
"event":
event.model_copy(deep=True).model_dump(
mode="json",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: session-memory 提取的事件来源从 base 的 append-only transcript(含全部历史事件)改为仅构造 session.events 活跃窗口(_session_event_records 遍历 session.events 并显式跳过 summary 事件),而 checkpoint 只存放在 session.state["_trpc_agent:summary"]。一旦 compaction 把 checkpoint 边界之前的事件移入 historical_events,_event_records_after_checkpoint 在活跃窗口找不到 checkpoint_event_id,只能退回按 recorded_at(实际是事件时间戳)近似恢复,找不到时直接返回空列表并跳过提取。

触发条件: BEFORE_MODEL 路径中 extract_if_needed(force=True) 提取失败(LLM 生成错误、预算超限等)后 compaction 仍继续执行,或 checkpoint 边界事件已被 compaction 移出活跃窗口的正常流程。

实际影响: checkpoint 之后、compaction 之前尚未提取的事件随 compaction 进入 historical_events 后永远无法被提取,会话记忆永久遗漏该段信息;recorded_at 时间戳近似恢复还可能重复提取已处理内容。

修正方向: 提取的增量边界不应依赖易失的活跃窗口:将 checkpoint(last_event_id 等)持久化到独立于 events 重排的源(如 session.state 中不与 compaction 冲突的键),并让 _session_event_records 同时纳入 historical_events 中 checkpoint 之后的事件,或恢复 transcript 机制。

Comment on lines 137 to +140
"""

def _decorator(session: Session) -> bool:
if session.conversation_count > conversation_count:
session.conversation_count = 0
return True
return False
return session.conversation_count > conversation_count

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: set_summarizer_conversation_threshold 从 base 的“超过阈值时把 session.conversation_count = 0 并返回 True”改为仅返回 session.conversation_count > conversation_count,触发时重置逻辑被删除;而 DefaultSessionSummarizerManager.create_session_summary 只在 summary_text 成功时重置 conversation_count = 0,runner 每轮无条件 conversation_count += 1。

触发条件: 会话轮数超过阈值后,summarize 失败(LLM 调用异常、返回空摘要或 create_session_summary_by_events 捕获异常返回 None):计数不会归零,下一轮 should_summarize_session 依旧返回 True。

实际影响: 每次失败后的每一轮对话都会重复触发一次 summarize LLM 调用(base 版本失败后计数被 checker 清零,需重新累积到阈值才再次尝试),模型持续故障时产生无界的重复调用与日志噪音。

修正方向: 在 manager 的 summarize 失败路径(summary_text 为空/异常)也重置 conversation_count = 0,或恢复 checker 触发时的重置语义,保证失败后至少间隔一个阈值周期再重试。

Comment on lines +201 to +208
def _session_lock(self, session_id: str) -> asyncio.Lock:
"""Return the unique compaction lock for a session."""
key = self._runtime.session_key(session_id)
lock = self._session_locks.get(key)
if lock is None:
lock = asyncio.Lock()
self._session_locks[key] = lock
return lock

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: _apply_scoped/ToolResultBudget/HistorySnip/MicroCompact 的 _session_lock 使用 asyncio.Lock,但新 manager 同时支持 BEFORE_MODEL(主事件循环内)与 AFTER_TURN(_PostTurnWorkerThread 独立线程+独立事件循环,见 runners.py L49-52)两条调用路径;base 版本 autocompact 只经 before_model_callback 单循环执行,不存在该跨循环共享。

触发条件: defer_post_turn_processing=True 时,上一个 turn 的 AFTER_TURN 压缩在 worker 循环异步执行期间,用户发起同一 session 的下一 turn,BEFORE_MODEL 压缩在主循环并发进入临界区(Python 3.10+ 的 asyncio.Lock 不绑定循环但状态非线程安全,3.6-3.9 则直接抛跨循环错误)。

实际影响: 同一 session 的两次压缩/写操作并发执行,_states/session.events 竞态,可能产生重复压缩或持久化事件窗口不一致。

修正方向: 与 SessionOperationCoordinator 一致改用跨循环安全的线程锁(如 CrossLoopLock)作为 session 级互斥,或仅在单一循环下执行压缩。

Comment on lines 242 to 252
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: TokenContextTracker.record_request_context 把 fingerprint 写入 session.state["advanced_memory_pending_request_context_fingerprint"],但全仓库没有任何代码把这些 pending fingerprint 迁移到事件的 custom_metadata["advanced_memory_request_context_fingerprint"];_latest_usage_baseline 读取的 recorded_fingerprint 恒为 None,静态 fingerprint 不一致过滤分支永远不会执行。

触发条件: 每次模型请求都会向 session.state 写入该键并随 state 持久化;_latest_usage_baseline 在匹配最近带 usage_metadata 的事件时跳过 fingerprint 校验。

实际影响: (a) usage 基线可能匹配到模型/系统指令/工具配置不同时期的旧 usage 事件,token 预算估计偏差,导致提前或延迟触发压缩;(b) .state 中残留无消费者的失效键,污染持久化状态。

修正方向: 在事件落库时把 pending fingerprint 写入事件 custom_metadata 并清理 state 中该键,或删除 record_request_context 及指纹过滤分支,仅依赖内容 fingerprint 匹配。

Comment on lines +553 to +556
storage_session: Optional[StorageSession] = await self._sql_storage.get_for_update(
sql_session,
session_key,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: 本次变更给 append_event/update_session_state 新增了 get_for_update 行锁(本文件 553-556、629-662 行的变更),但同为会话写路径的 update_session 仍用普通 get 读取(不带锁),而 advanced compaction 通过 _persist_session_compaction → ctx.session_service.update_session 全量重写会话事件(delete 全部行后重新插入),与加了行锁的 append_event 并发执行时缺少同一把锁。

触发条件: 多实例/多事件循环下,一个请求的 compaction 执行 update_session(删除并重写 events 行)的同时,另一个请求对该 session 执行 append_event 插入新事件。

实际影响: 事务交错时并发 append 的新事件行可能被 compaction 的 delete-all 删除,或 compaction 写入的窗口覆盖并发 append 的事件,活动窗口静默丢失事件(base 版本 advanced compact 只写 transcript、不重写 events,此写路径为本变更新引入)。

修正方向: update_session 改用 get_for_update 并在事务内完成删除与重写,或为 compaction 持久化提供补丁式更新(仅替换被压缩前缀对应的事件行),避免全量重写与并发 append 竞争。

from ._runtime import AdvancedMemoryRuntime


def install_staged_callback(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

这个是否可以删除?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

可以删,已经删除

session_config: Optional[SessionServiceConfig] = None,
is_async: bool = False,
**kwargs: Any):
self._db_url = db_url

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

self._db_url 这个是否有使用到?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已删除

# AsyncSession cannot perform an implicit refresh when an ORM
# attribute is accessed after commit. Keep committed values available
# because this service reads StorageSession state after committing.
kwargs.setdefault("expire_on_commit", False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

kwargs.setdefault("expire_on_commit", False) 这个是否需要?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已删除

from .utils import from_function_with_options
from .utils import get_required_fields
from .utils import parse_schema_from_parameter
from .utils import register_checker

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

建议导入“create_advanced_memory_toolset” 方便用户使用

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

不建议导入

Comment thread trpc_agent_sdk/memory/__init__.py Outdated
This module provides memory/RAG functionality including:
- Abstract memory service interfaces
- In-memory memory service implementation
- In-memory, Redis, and SQL memory implementations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

还原一下

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已还原

from typing import Any
from uuid import uuid4

from trpc_agent_sdk.storage import RedisCommand, RedisExpire, RedisStorage

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

每一行只能导入一个包,不要一行导入多个包

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已修改

import asyncio
from contextlib import asynccontextmanager
from dataclasses import replace
from datetime import datetime, timezone

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

每一行只能导入一个包,不要一行导入多个包

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已修改

from trpc_agent_sdk.types import Ttl

from ._config import AdvancedMemoryConfig
from ._formats import AdvancedMemoryDocument, AdvancedMemoryIndexEntry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

每一行只能导入一个包,不要一行导入多个包

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已修改

from ._formats import AdvancedMemoryDocument, AdvancedMemoryIndexEntry
from ._formats import limit_memory_index
from ._storage_key import AdvancedMemoryPaths
from ._file_storage import parse_memory_index, prune_memory_index

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

按照字典序导入,这个放在_formats上面,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已修改

from typing import Any

from ._config import AdvancedMemoryConfig
from trpc_agent_sdk.sessions.compact.advanced._coordination import CrossLoopLock

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

这个导入是否需要在这里?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

建议保留

@CongkeChen CongkeChen closed this Sep 15, 2026
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

通过

审查范围:f05797d9..fdc52ae(plan 4426「优化 advanced memory 与框架相关内容实现」,5 个提交,约 112 个文件、+6182/-6678)。计划符合性:Session Compact 统一收敛到 sessions/compact/(advanced/default 两个包),CompactTrigger、_filters.py 的 before-model 短路与 INTERNAL_COMPACTION_METADATA_KEY 防递归守卫均已落地;update_session_state 三后端(InMemory/SQL/Redis)一致实现「仅 patch stored state、不重写 event window」并有测试佐证;advanced_memory/ 工具链与框架解耦、示例同步更新。主要风险已在审查中逐项排除:compact_events 幂等性/边界/时间戳锚定/回滚、SQL/Redis 排序设计、三后端 update_session_state 语义、filter 短路与递归守卫、CrossLoopLock 轮询锁、EvalSessionService 走 ABC 默认 full-update 视为可接受回退。确认 2 条 LOW 问题:1) fingerprint 元数据链断裂——base 中唯一向事件写入 advanced_memory_request_context_fingerprint 的 advanced_memory/_session_service.py::append_event 在本范围被删除且无替代 writer,_token_budget.py 的指纹 guard 与 pending 写入失去消费者,token-mode 下 usage 基线可能匹配过期 usage 事件;2) _request_processor.py 删除 summary 注入但未同步删除 tests/agents/core/test_request_processor_ext.py::test_session_summary_appended,该测试断言必然失败。测试充分性:三后端 compact/patch-state 测试与实现静态吻合,token 预算 fallback 测试系列与实现逻辑一致;本环境(Python 3.6.8、无 pydantic≥2.11)无法实际运行测试,全部为静态对拍验证。门禁结论:无 SEVERE 或 MODERATE 问题,允许合入,附 2 条 LOW 建议跟进。

发现的问题

较低

trpc_agent_sdk/sessions/compact/advanced/_auto_compact.py:581

问题: base 版本向事件 custom_metadata 写入 advanced_memory_request_context_fingerprint 的唯一写入方是在本范围内被删除的 trpc_agent_sdk/advanced_memory/_session_service.py 的 append_event(从 state["advanced_memory_pending_request_context_fingerprint"] 取指纹写入事件元数据),head 全库已无任何代码写入该元数据键;因此本文件中 _latest_usage_baseline 的指纹 guard 和第 251 行 record_request_context 对 pending 指纹的写入均失去消费者,形成断裂的元数据链。

触发条件: 启用 token-mode(model_context_window_tokens 非空)且会话内模型/指令配置发生变化后,usage 事件仍会被 _latest_usage_baseline 接受为基线。

实际影响: 指纹 guard 恒不生效,过期 usage 事件被当作有效基线,token 估算与真实上下文偏差,自动压缩/告警阈值误判;record_request_context 的 pending 写入成为死代码。该功能默认关闭,影响有界。

修正方向: 在响应 Event 落盘路径上恢复消费 pending 指纹并写入事件元数据(或在 _auto_compact.py 第 581 行的 record_request_context 调用点直接写入),与删除写入方二选一;若该机制不再需要,则应同时删除本文件的指纹 guard 与 record_request_context 的 pending 写入,避免死代码。

较低

trpc_agent_sdk/agents/core/_request_processor.py:264

问题: 本次变更删除了 _request_processor.py 中原有的 session summary 注入块(原先调用 ctx.session_service.get_session_summary 并把结果拼进系统指令),但未同步更新 tests/agents/core/test_request_processor_ext.py 中的 test_session_summary_appended,该测试仍断言 "previous summary text" in str(request.config.system_instruction)。

触发条件: 在 CI 或本地运行 tests/agents/core/test_request_processor_ext.py。

实际影响: 该测试确定性失败,测试套件变红;同时它已不能反映新行为,会误导后续维护者以为 summary 仍注入系统指令。

修正方向: 删除 test_session_summary_appended,或将其改为断言 summary 不再注入系统指令(例如断言结果中不含 summary 文本),以与新实现保持一致。


result = await self.apply(request, ctx=ctx, force=force)
if not result.blocked:
TokenContextTracker.record_request_context(request, ctx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: base 版本向事件 custom_metadata 写入 advanced_memory_request_context_fingerprint 的唯一写入方是在本范围内被删除的 trpc_agent_sdk/advanced_memory/_session_service.py 的 append_event(从 state["advanced_memory_pending_request_context_fingerprint"] 取指纹写入事件元数据),head 全库已无任何代码写入该元数据键;因此本文件中 _latest_usage_baseline 的指纹 guard 和第 251 行 record_request_context 对 pending 指纹的写入均失去消费者,形成断裂的元数据链。

触发条件: 启用 token-mode(model_context_window_tokens 非空)且会话内模型/指令配置发生变化后,usage 事件仍会被 _latest_usage_baseline 接受为基线。

实际影响: 指纹 guard 恒不生效,过期 usage 事件被当作有效基线,token 估算与真实上下文偏差,自动压缩/告警阈值误判;record_request_context 的 pending 写入成为死代码。该功能默认关闭,影响有界。

修正方向: 在响应 Event 落盘路径上恢复消费 pending 指纹并写入事件元数据(或在 _auto_compact.py 第 581 行的 record_request_context 调用点直接写入),与删除写入方二选一;若该机制不再需要,则应同时删除本文件的指纹 guard 与 record_request_context 的 pending 写入,避免死代码。

@@ -262,11 +262,6 @@ async def _add_instructions_to_request(self, agent: BaseAgent, ctx: InvocationCo
instructions_parts.append(code_executor_instruction)
logger.debug("Added code executor instruction for agent: %s", agent.name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

问题: 本次变更删除了 _request_processor.py 中原有的 session summary 注入块(原先调用 ctx.session_service.get_session_summary 并把结果拼进系统指令),但未同步更新 tests/agents/core/test_request_processor_ext.py 中的 test_session_summary_appended,该测试仍断言 "previous summary text" in str(request.config.system_instruction)。

触发条件: 在 CI 或本地运行 tests/agents/core/test_request_processor_ext.py。

实际影响: 该测试确定性失败,测试套件变红;同时它已不能反映新行为,会误导后续维护者以为 summary 仍注入系统指令。

修正方向: 删除 test_session_summary_appended,或将其改为断言 summary 不再注入系统指令(例如断言结果中不含 summary 文本),以与新实现保持一致。

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.

3 participants