fix(memory): outbox 不再固化本进程猜测的 locale - #2519
Conversation
app/memory_server/routes.py 的四个写路由把 _activate_request_language() 的返回值 同时用于两处:包住本次请求的 language_context,以及交给 _spawn_outbox_post_turn_signals 持久化进 outbox.ndjson。 但这个返回值在请求未声明 locale 时是回落值 get_global_language_full()——本进程 的探测结果。而 _spawn_outbox_post_turn_signals 里 `if language:` 恒真,于是这个 「猜测」被写进 payload 永久冻结:重启后 replay 一直复用它,即使探测本身后来修好 也不会自愈。改动前(outbox 引入之前)replay 读的是当时的进程全局,探测修好后旧 条目能跟着变。 改为只持久化客户端真正声明的值: - 四个调用点从 language=memory_language 改成 language=request.language - memory_language 继续用于 language_context(本次请求内的回落是正确的) - 未声明时 payload 不含 language 键,replay 走 language_context(None) 空转 → 回落当时的进程语言 进程内直跑路径不受影响:_spawn_outbox_post_turn_signals 在 with 块内调用, spawn 出的 task 会复制当前 context,仍继承 memory_language。 测试: - test_cache_hands_outbox_the_undeclared_language_as_none:走真实 handler, 断言未声明时交给 outbox 的是 None、声明了则原样透传 - test_outbox_enqueue_persists_only_client_declared_language:AST 守卫,盯住 四个调用点传的是 request.language 而非 memory_language - test_spawn_outbox_omits_language_when_request_declared_none:契约测试 - test_replay_without_recorded_language_falls_back_to_process_locale:补上此前 缺失的「存量 outbox 条目无 language 键」replay 覆盖(升级用户唯一会走的路径) 其中 AST 守卫是必要的:只测 _spawn_outbox_post_turn_signals 自身只能证明「传 None 就不写 payload」,证明不了路由真的传了 None——第一版测试就是这么写的, 变异验证时回退四个调用点它照样全绿。 验证:变异验证——把四处改回 memory_language 后上述前两条立刻变红,还原后 17 passed; uv run pytest tests/unit -q → 6794 passed, 26 skipped, 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 31 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Comment |
Greptile Summary四个 memory 写路由现在仅将客户端明确声明的 locale 持久化到 outbox,同时保留当前请求的 locale 回落行为喵。
Confidence Score: 5/5此 PR 未发现阻塞或非阻塞问题,看起来可以安全合并喵。 显式 locale 在 replay 时仍由 language context 正常校验和规范化,缺省或不受支持的值则安全回落到进程语言,新增测试也覆盖了四个调用点及存量条目路径喵。
|
| Filename | Overview |
|---|---|
| app/memory_server/routes.py | 四个写路由改为向 outbox 传递客户端原始声明值,同时继续使用解析后的 locale 处理当前请求喵。 |
| app/memory_server/post_turn.py | 补充注释,明确持久化声明 locale 与当前请求回落 locale 的边界喵。 |
| tests/unit/test_memory_request_language_context.py | 新增 handler 行为与四个调用点的回归守卫,确保未声明 locale 不会被替换后落盘喵。 |
| tests/unit/test_outbox_wiring.py | 覆盖缺省 locale 的 payload 省略行为以及旧 outbox 条目的 replay 回落路径喵。 |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Memory write request] --> B{Client declared language?}
B -->|Yes| C[Persist declared language in outbox]
B -->|No| D[Omit language from outbox payload]
C --> E[Replay under recorded language context]
D --> F[Replay under current process locale]
A --> G[Resolve request locale with process fallback]
G --> H[Handle current request under resolved context]
Reviews (1): Last reviewed commit: "fix(memory): outbox 不再固化本进程猜测的 locale" | Re-trigger Greptile
| task = await memory_server._spawn_outbox_post_turn_signals( | ||
| "小天", [HumanMessage(content="喵")], language=None, | ||
| ) | ||
| await task |
* perf(i18n): 缓存 macOS locale 探测结果,避免冷启动重复 spawn defaults _get_macos_locale() 每次未命中都要 subprocess.run(['/usr/bin/defaults', ...]), 超时 1s,且原实现无缓存。initialize_global_language() 会经 _is_china_region 和 _get_system_language 各调它一次,每次最多试 AppleLocale / AppleLanguages 两个 key——最坏一次冷启动 spawn 4 次、约 4 秒,而整个初始化过程持 _global_language_lock。 get_global_language() / get_global_language_full() 又大量出现在 async 请求路径上, 首次触发若落在事件循环里就会把它阻塞住,与 #2466「async 路径同步配置读挪出事件 循环」的方向相悖。 改为进程级缓存(双检 + 锁),OS locale 在进程运行期间不会变。真正的查询逻辑拆到 _read_macos_locale_uncached(),另提供 _reset_macos_locale_cache() 供测试使用。 测试: - test_macos_locale_is_read_once_per_process:连续三次调用只 spawn 一次 - test_macos_locale_reads_apple_locale 补 monkeypatch 清缓存——否则它会读到别的 用例留下的缓存值(非 macOS CI 上是 None),断言永远看不到 fake_run 的结果。 这个顺序污染是实测出来的:先跑 test_language_region_override.py 再跑它就红。 顺带清掉 #2519 遗留的 4 处中文 docstring(DOCSTRING_CJK 门要求英文),背景说明 移到注释里。 验证:变异验证——去掉写缓存那行后 test_macos_locale_is_read_once_per_process 变红, 还原后 13 passed;check_docstring_no_cjk --base origin/main 退出码 0; uv run pytest tests/unit -q → 6812 passed, 26 skipped, 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(review): macOS locale 探测失败不再被永久缓存 Greptile P1 指出得对:原实现无论成功失败都写缓存,于是 macOS 上首次 defaults 超时(或非零退出 / 空输出)会把 None 钉死一整个进程周期——区域判定掉到 non-china、语言判定掉到英文,而「launcher 子进程继承中性 locale、其它信号都不 可靠」恰恰是这个 helper 要兜的场景。 改为只缓存确定性结论: - 非 Darwin:确定性,缓存(省掉后续每次的 platform 判断) - Darwin 且探测到 locale:缓存 - Darwin 但探测失败:不写缓存,下次调用重试 顺带把哨兵从 (bool, value) 元组换成独立的 _MACOS_LOCALE_UNSET 对象——原来的 bool 恒为 True,是冗余状态。 测试: - test_macos_locale_probe_failure_is_retried_not_cached:前两次 defaults 抛 TimeoutExpired → 返回 None 且不缓存;下一次调用重新探测拿到真实 locale - test_macos_locale_non_darwin_is_cached:非 macOS 只判一次 platform.system() - 既有两条的 monkeypatch 从 None 改为 _MACOS_LOCALE_UNSET(None 现在表示 「已确认非 macOS」,不再是「未缓存」) 验证:变异验证——改回「失败也缓存」后 probe_failure 用例变红,去掉非 macOS 缓存后 non_darwin 用例变红;uv run pytest tests/unit -q → 6813 passed, 26 skipped, 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#1542 收尾第 5 批。
问题
app/memory_server/routes.py的四个写路由(/cache/process/renew/settle)把_activate_request_language()的返回值同时用于两处:language_context(memory_language)_spawn_outbox_post_turn_signals(..., language=memory_language)持久化进outbox.ndjson但这个返回值在请求未声明 locale 时是回落值
get_global_language_full()—— 本进程的探测结果。而_spawn_outbox_post_turn_signals里if language:恒真,于是这个「猜测」被写进 payload 永久冻结:重启后 replay 一直复用它,即使探测本身后来修好也不会自愈。改动前(outbox 引入之前)replay 读的是当时的进程全局语言,探测修好后旧条目能跟着变。
改动
只持久化客户端真正声明的值:
language=memory_language→language=request.languagememory_language继续用于language_context—— 本次请求内的回落是正确的,问题只在于不该落盘language键,replay 走language_context(None)空转 → 回落当时的进程语言进程内直跑路径不受影响:
_spawn_outbox_post_turn_signals在with块内调用,spawn 出的 task 会复制当前 context,仍然继承memory_language。关于测试(一个值得记录的返工)
第一版我只加了
_spawn_outbox_post_turn_signals的契约测试(传None就不写 payload)。变异验证时把四个调用点全改回memory_language,测试照样全绿 —— 因为它测的是那个函数自己的行为,证明不了路由真的传了None。这正是「只测谓词不测调用点」。补上之后是四条:
test_cache_hands_outbox_the_undeclared_language_as_noneNone、声明了则原样透传test_outbox_enqueue_persists_only_client_declared_languagerequest.language而非memory_languagetest_spawn_outbox_omits_language_when_request_declared_noneNone不写 payloadtest_replay_without_recorded_language_falls_back_to_process_localelanguage键」replay 覆盖 —— 这是升级用户唯一会走的路径AST 守卫用自动发现(遍历四个 handler 找
_spawn_outbox_post_turn_signals调用),漏掉任一调用点会直接断言失败,不是硬编码清单。回归报告 / Regression Report
language时行为完全不变(原值透传)。请求不带时,改动前 payload 会存入探测值,改动后不存该键 —— replay 回落到当时的进程语言,与 outbox 引入之前一致。当前 in-flight 请求的语言解析完全不变。_outbox_post_turn_signals_handler本就用payload.get('language'),已覆盖并新增测试;(b) 进程内直跑路径依赖 task 继承 context,已由既有的test_concurrent_post_turn_tasks_keep_their_recorded_language覆盖。memory_language后行为测试与 AST 守卫立刻变红,还原后 17 passed;uv run pytest tests/unit -q→ 6794 passed, 26 skipped, 0 failed。不拆分理由 / Why Not Split
生产改动(4 个调用点 + 注释)与其守卫测试必须同 PR:调用点很容易在后续重构里被"顺手统一"回
memory_language,AST 守卫就是为此存在的。共 4 个文件、+151/-5。