Repository navigation
fix(chat): keep the provider reply projection in the composition root - #7631
Conversation
1d36597 to
b513b0d
Compare
Two turn_route fixtures in tests/architecture/test_semantic_production.py still named sites that no longer exist. The parametrized return-producer case named 'return LoopXTurnRoute.CONTRACT_ERROR' in driver.py. The driver now assigns the route to a local and returns a call, so the Python producer has no literal left to mutate and the test failed on its own precondition. The route vocabulary's owner is the TypeScript producer, so the case now mutates turn_envelope.ts::turnEnvelopeRoute, whose resolved returns are the route values themselves. The mutation still replaces one returned value with an unregistered one and still expects the scan to reject it, so the invariant the test proves is unchanged; only the fixture that had gone stale moves. The one-hop re-export case named loop_controller.py::_envelope_route. PR loopx-project#7523 moved that decision into loop_controller.ts and dropped the site from the turn_route producers, so the assertion collected nothing. driver.py still re-exports LoopXTurnRoute from the generated owner and _typed_route is still a registered return producer, so the case now resolves through that hop. No production code, schema, wire or default change. Validation: tests/architecture/test_semantic_production.py 42 passed on origin/main 0a9a3a3. The remaining failures in tests/architecture are pre-existing on that base: the lark import boundary (PR loopx-project#7631) and two package/vocabulary ratchets this change does not touch. Signed-off-by: xiaoan17 <anbc1224@gmail.com>
Two turn_route fixtures in tests/architecture/test_semantic_production.py still named sites that no longer exist. The parametrized return-producer case named 'return LoopXTurnRoute.CONTRACT_ERROR' in driver.py. The driver now assigns the route to a local and returns a call, so the Python producer has no literal left to mutate and the test failed on its own precondition. The route vocabulary's owner is the TypeScript producer, so the case now mutates turn_envelope.ts::turnEnvelopeRoute, whose resolved returns are the route values themselves. The mutation still replaces one returned value with an unregistered one and still expects the scan to reject it, so the invariant the test proves is unchanged; only the fixture that had gone stale moves. The one-hop re-export case named loop_controller.py::_envelope_route. PR loopx-project#7523 moved that decision into loop_controller.ts and dropped the site from the turn_route producers, so the assertion collected nothing. driver.py still re-exports LoopXTurnRoute from the generated owner and _typed_route is still a registered return producer, so the case now resolves through that hop. No production code, schema, wire or default change. Validation: tests/architecture/test_semantic_production.py 42 passed on origin/main 0a9a3a3. The remaining failures in tests/architecture are pre-existing on that base: the lark import boundary (PR loopx-project#7631) and two package/vocabulary ratchets this change does not touch. Signed-off-by: xiaoan17 <anbc1224@gmail.com>
8f975e9 to
3fa5c2d
Compare
Two turn_route fixtures in tests/architecture/test_semantic_production.py still named sites that no longer exist. The parametrized return-producer case named 'return LoopXTurnRoute.CONTRACT_ERROR' in driver.py. The driver now assigns the route to a local and returns a call, so the Python producer has no literal left to mutate and the test failed on its own precondition. The route vocabulary's owner is the TypeScript producer, so the case now mutates turn_envelope.ts::turnEnvelopeRoute, whose resolved returns are the route values themselves. The mutation still replaces one returned value with an unregistered one and still expects the scan to reject it, so the invariant the test proves is unchanged; only the fixture that had gone stale moves. The one-hop re-export case named loop_controller.py::_envelope_route. PR loopx-project#7523 moved that decision into loop_controller.ts and dropped the site from the turn_route producers, so the assertion collected nothing. driver.py still re-exports LoopXTurnRoute from the generated owner and _typed_route is still a registered return producer, so the case now resolves through that hop. No production code, schema, wire or default change. Validation: tests/architecture/test_semantic_production.py 42 passed on origin/main 0a9a3a3. The remaining failures in tests/architecture are pre-existing on that base: the lark import boundary (PR loopx-project#7631) and two package/vocabulary ratchets this change does not touch. Signed-off-by: xiaoan17 <anbc1224@gmail.com>
Two turn_route fixtures in tests/architecture/test_semantic_production.py still named sites that no longer exist. The parametrized return-producer case named 'return LoopXTurnRoute.CONTRACT_ERROR' in driver.py. The driver now assigns the route to a local and returns a call, so the Python producer has no literal left to mutate and the test failed on its own precondition. The route vocabulary's owner is the TypeScript producer, so the case now mutates turn_envelope.ts::turnEnvelopeRoute, whose resolved returns are the route values themselves. The mutation still replaces one returned value with an unregistered one and still expects the scan to reject it, so the invariant the test proves is unchanged; only the fixture that had gone stale moves. The one-hop re-export case named loop_controller.py::_envelope_route. PR loopx-project#7523 moved that decision into loop_controller.ts and dropped the site from the turn_route producers, so the assertion collected nothing. driver.py still re-exports LoopXTurnRoute from the generated owner and _typed_route is still a registered return producer, so the case now resolves through that hop. No production code, schema, wire or default change. Validation: tests/architecture/test_semantic_production.py 42 passed on origin/main 0a9a3a3. The remaining failures in tests/architecture are pre-existing on that base: the lark import boundary (PR loopx-project#7631) and two package/vocabulary ratchets this change does not touch. Signed-off-by: xiaoan17 <anbc1224@gmail.com>
d71e8ac to
1aab61e
Compare
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · reasoning_effort=max
本次评审未发现阻塞问题,建议 APPROVE。评审对象为 1aab61e3277f9036d3ea72fc38ebcb053b873d97,基线为 9dcdc0bacd455a8cf6f6146f052ffc4df0f5d04a;未查询 GitHub CI。
动机 / Motivation
维护 Chat 与飞书私聊历史的开发者会在回归验证中遇到 core 反向依赖 provider 的错误。读取同一段带引用的私聊历史时,修正前由 core 直接调用飞书投影;修正后由既有 HTTP 组合入口连接该投影,用户看到的原文、引用与分页保持一致。本次实测恢复了依赖边界检查,并保留同一 HTTP 入口的消息读回和失败回退。范围限于历史读取的组合位置及对应回归测试,不新增能力、授权、消息发送或界面交互。
改动思路 / Approach
既有 HTTP 入口本来就负责 provider 组合;只搬移原投影调用即可恢复依赖方向,无需新增通用注入层或复制来源判定。本 PR 完整交付历史读取组合位置修正;真实飞书传输、模型执行及其它发布失败项由各自原 owner 继续处理。没有另建 decoder、provider 状态或通用 hook。删除投影调用会丢失引用信息,继续放在 core 又违反已有依赖方向;目前唯一生产调用方就是这个 HTTP 入口。
| 路径 | 修正前 | 本次 head |
|---|---|---|
| core snapshot | 组合 core 元数据并调用飞书投影 | 只组合 core 元数据与分页 |
| HTTP 读取 | 直接发送 core 结果 | 在同一选定页上连接原 provider 投影再发送 |
这是源码关系说明;等价性由下面的实际 HTTP 对照验证。
具体改动 / Changes
规范依据是 docs/reference/extensions.md,spec_revision 9dcdc0bacd455a8cf6f6146f052ffc4df0f5d04a。Runtime Responsibilities 与 Scope Boundaries 两项均 implemented:provider 私有来源解释仍留在扩展中,core 不再反向 import 飞书实现,也没有新增控制面权限。
关键代码讲解
project_chat_session_snapshot(roundtrip.py:288)继续从同一持久化会话读取原始消息,按冻结前缀分页,并组合 return/collaboration 元数据;只移走飞书私有投影。ChatRequestHandler._session_snapshot(chat_server.py:924)保留原查询校验和 400/404/503 路径,在取得选定页后调用原project_private_reply_history,然后发送原结构的 JSON。project_private_reply_history(private_conversations.py:100)未改实现:仍核对绑定、会话、Turn、native/provider 双记录及原文;缺失、损坏或不匹配记录保留完整原消息,历史恢复也必须有原始记录证明。
新增测试把既有引用断言移到真实回环 HTTP 路径,并继续核对存储字节不变。实测 handler 为 2020→2026 行,Any 数仍为25、dict[Any] 仍为0;同 diff 的上限改为2026,只覆盖本次连接调用,没有放宽其它模块或冻结资格指标。
对主干的风险 / Risks
最需要排除的是依赖检查转绿、实际界面却丢失引用,或分页与恢复中混入其它会话的上下文。我用同一份合成来源和同一脚本,在不可变基线与 head 上通过真实 socket HTTP、临时真实 transcript/receipt 文件运行了7组:正常引用、缺失记录、其它绑定、损坏 JSON、普通消息、13条历史逐页读取、失败后有记录的恢复。原文、引用、顺序和完整错误输出一致,GET 前后文件字节不变;拼接全部分页也等于完整读取。
敏感性检查在 head 内存中移除 HTTP 的连接调用,实际接口随即缺失 reply_context,使同一来源断言失败;基线的依赖边界用例也实际复现 core→Lark import 失败。没有用 mock 返回投影后置结果,只有进程恢复回调被隔离;本次不验证真实飞书网络和模型运行。
PASS:365项聚焦测试;标准 premerge 的6项直接检查、5项 catalog 与8项选定风险检查;Ruff 与 diff check。未运行全量发布资格。
语义与 CI 对齐
本 PR reuse_existing:沿用原字段与 provider decoder,不新增词汇。普通本地消息及无效 provider 来源均完成基线/head 反例对照。GitHub CI 按 Goal 的 wait_for_ci=false 未查询;本地 premerge 是本次验证依据。
我的整体评价 / Verdict
long_horizon 与 user_experience 均 preserved:失败恢复保留原始来源,分页与重复读取不改持久化输入,也不增加用户的设置或确认轮次。仅搬移服务端历史投影连接点;同一 HTTP 消息、引用与分页对照一致,未修改渲染模板、布局或样式。本次未重新验收已安装前端或真实飞书网络;本地实 HTTP 与临时文件后端覆盖本次搬移。
这是完整且可回滚的依赖修正,代码量与问题相称。更一般的注入框架目前没有必要。未发现阻塞 finding;剩余边界是已安装前端、真实 provider 传输和整体发布资格,本次结果不替代它们。评审批准与维护者的运行时合并授权仍是分开的步骤。
English verdict: APPROVE 1aab61e; the provider join is correctly relocated, with matched real-HTTP behavior, recovery and paging checks, 365 passing focused tests and local premerge validation.
PR loopx-project#7542 added a core capability import of loopx.extensions.lark.private_conversations, so tests/architecture/test_control_plane_import_boundaries.py:: test_lark_projection_sinks_are_owned_by_the_extension_layer fails on main. A core capability must not own an extension projection sink. Move the projection to the HTTP composition root that already joins provider history onto the selected page, and exercise it there. The core projection now carries no provider history at all. Joining the provider history in the composition root grows loopx/chat_server.py from 2020 to 2026 lines, so the reviewed module metric ceiling for that module is refreshed in the same change, as the maintainability ratchet requires. The growth is the composition call itself; no other module metric moved. Signed-off-by: xiaoan17 <anbc1224@gmail.com>
1aab61e to
9b001b6
Compare
动机 / Motivation
main当前在tests/architecture/test_control_plane_import_boundaries.py::test_lark_projection_sinks_are_owned_by_the_extension_layer上是红的。来源是 PR #7542(commit
42102e1e1):它在loopx/capabilities/manager_context/roundtrip.py里加了也就是让一个 core capability 直接 import extension projection sink。这条边界由
test_lark_projection_sinks_are_owned_by_the_extension_layer守住,core 侧(
loopx/capabilities/、loopx/presentation/)不允许反向依赖 extension。这个红项会同时拖红所有在开 PR 的 Python Tests lane。
当前缺口 / Current gap
#5677里确实删掉了这两行,但它是一个体量很大、当前DIRTY(有冲突)且收到多轮CHANGES_REQUESTED的 PR,不能作为主干恢复绿色的近期路径。改动思路 / Approach
把 provider projection 放回它的composition root:
loopx/capabilities/manager_context/roundtrip.py:删掉 core → extension 的 import 与调用,core projection 现在完全不带 provider history。
loopx/chat_server.py:在已经负责把 provider history 拼到当前分页上的 HTTP composition root(
_session_snapshot)里调用project_private_reply_history。行为等价:投影仍然作用在同一批 message 上,仍然只对当前页生效,仍然是 read-only。
具体改动 / Changes
loopx/capabilities/manager_context/roundtrip.pyloopx/chat_server.py_session_snapshot里 join provider historytests/test_manager_context_roundtrip.pyreply_context)loopx/canary/module_metric_baseline.jsonloopx/chat_server.py的 reviewed 行数上限 2020 → 2025composition root 里的 join 让
loopx/chat_server.py从 2020 行长到 2026 行,因此同一次改动里按 maintainability ratchet 的要求刷新该模块的 reviewed 上限;增长就是这次 join 调用本身,
其他 module metric 没有变化。
没有新增词汇、schema、wire 或默认值变化。
验证证据 / Evidence
基线:
origin/main=2a2323756。本分支已 rebase 到该基线。tests/architecture/test_control_plane_import_boundaries.py::test_lark_projection_sinks_are_owned_by_the_extension_layer— 从 FAIL 变为 PASStests/test_manager_context_roundtrip.py tests/architecture/test_control_plane_import_boundaries.py— 110 passedtests/architecture/ tests/extensions/— 3408 passed, 1 skipped;唯一失败是test_real_return_producer_rejects_an_unregistered_result[turn_route-...],那是 main 上已存在、由 PR test(semantics): point the stale turn_route fixtures at their current owners #7621 负责的另一个红项,本 PR 不涉及
tests/test_lark_private_conversations.py、tests/extensions/test_lark_reply_handoff.py— 通过python -m ruff check tests loopx/canary loopx/control_plane loopx/chat_server.py loopx/capabilities/manager_context/roundtrip.py— All checks passedtests/test_stage2c_dependency_lock.py tests/architecture/test_top_level_module_budget.py— 7 passedexamples/control_plane/control-plane-maintainability-ratchet-smoke.py— ok(unreviewed: 0)loopx canary premerge --from-git-diff --git-diff-base 2a2323756— green,0 failures对主干的风险 / Risks
project_chat_session_snapshot只有loopx/chat_server.py一个调用方,因此 Chat UI 读取到的内容不变。chat_server.py位于loopx/顶层,不在边界测试扫描的capabilities/、presentation/范围内,它的 extension import 是既有做法(同一文件已经 import
LarkPrivateConversations)。#5677在roundtrip.py上有重叠行;该 PR 需要 rebase 后再评估,本 PR 是可独立评审的最小切片。English verdict: the red lane is caused by a core capability importing an extension projection
sink. This PR moves the projection to the HTTP composition root that already owns provider
history, restores the import-boundary contract, and adds no new vocabulary or behavior.
评审请求 / Review request
请按 exact head
3fa5c2deb40f1eb6d5e5e89034f150df053b40aa评审。本 PR 与 #7621、#7632 一起修复 main 上剩余的 pytest 红项;合并后各在开 PR 的 pytest lane 才能恢复。
Rebase and re-validation (2026-10-29)
mainatb201da8bab305d053866aa0d4516b97ba4a4499e9b001b61be212e324d5533a60ca48dd9558a3674This is one of six pull requests that together restore
mainto green. Onmainatb201da8bab305d053866aa0d4516b97ba4a4499ethe repository is red in exactly six places, and this one owns the import-boundary red:tests/architecture/test_control_plane_import_boundaries.py::test_lark_projection_sinks_are_owned_by_the_extension_layerThe other five are owned by #7632, #7640, #7642 and #7646. None of the six is caused by a pull request in this set.
All nine pull requests in this set were rebased onto the then-current
main(this one ontob201da8bab305d053866aa0d4516b97ba4a4499e) and merged together in a local worktree. The merged state passestests/architecture/,tests/canary/,tests/test_manager_context_roundtrip.py,tests/test_lark_native_reply_context.py,tests/control_plane/test_host_adapter_settlement_step_vocabulary.pyandtests/control_plane/test_host_adapter_settlement_partial_steps.py— 1805 passed, 0 failed.python scripts/generate_rfc_status_index.py --checkis clean.Merge sequence (2026-10-29)
mainto green. Each one fixes only its own share of the six places wheremainis currently red, so until all five land every one of them still shows the others' red tests in itspytest/test-shardlanes. test(semantics): point the stale turn_route fixtures at their current owners #7621 was merged the same way, with a known base-red. Merge them in any order; each merge removes one share.