Skip to content

fix(chat): keep the provider reply projection in the composition root - #7631

Merged
huangruiteng merged 1 commit into
loopx-project:mainfrom
xiaoan17:codex/fix-lark-core-import-boundary-20261029
Oct 29, 2026
Merged

huangruiteng merged 1 commit into
loopx-project:mainfrom
xiaoan17:codex/fix-lark-core-import-boundary-20261029

Conversation

@xiaoan17

@xiaoan17 xiaoan17 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

动机 / 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 里加了

from ...extensions.lark.private_conversations import project_private_reply_history

也就是让一个 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.py 移除 core → extension import 与调用
loopx/chat_server.py 在 _session_snapshot 里 join provider history
tests/test_manager_context_roundtrip.py 三个 private-reply 测试改到 HTTP composition boundary 验证(并断言 core projection 不再带 reply_context)
loopx/canary/module_metric_baseline.json loopx/chat_server.py 的 reviewed 行数上限 2020 → 2025

composition 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 变为 PASS
  • tests/test_manager_context_roundtrip.py tests/architecture/test_control_plane_import_boundaries.py — 110 passed
  • tests/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 passed
  • tests/test_stage2c_dependency_lock.py tests/architecture/test_top_level_module_budget.py — 7 passed
  • examples/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

  • 投影调用点从 core capability 移到 HTTP 层。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)

  • Base: main at b201da8bab305d053866aa0d4516b97ba4a4499e
  • Head: 9b001b61be212e324d5533a60ca48dd9558a3674
  • The change itself is unchanged; only the parent moved.

This is one of six pull requests that together restore main to green. On main at b201da8bab305d053866aa0d4516b97ba4a4499e the 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_layer

The 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 onto b201da8bab305d053866aa0d4516b97ba4a4499e) and merged together in a local worktree. The merged state passes tests/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.py and tests/control_plane/test_host_adapter_settlement_partial_steps.py — 1805 passed, 0 failed. python scripts/generate_rfc_status_index.py --check is clean.

Merge sequence (2026-10-29)

  1. docs(rfc): confirm the C4.3 settlement vocabulary question #7617 — CI is green end to end; merge it first. It must precede test(settlement): characterize the host adapter's partial-step boundary #7645.
  2. test(settlement): characterize the host adapter's partial-step boundary #7645 — chained on docs(rfc): confirm the C4.3 settlement vocabulary question #7617's head; merge it after docs(rfc): confirm the C4.3 settlement vocabulary question #7617.
  3. feat(control-plane): register the host-adapter settlement step vocabulary #7619 — independent of the two above.
  4. test(chat): assert the preserved operator material in the native input #7632, test(semantics): derive the output-evidence guard from the registry #7640, docs(rfc): record the Codex provider-routing activation seam #7642, fix(canary): lower the stale collaboration_mcp module ceiling #7646, fix(chat): keep the provider reply projection in the composition root #7631 — the five that restore main to green. Each one fixes only its own share of the six places where main is currently red, so until all five land every one of them still shows the others' red tests in its pytest / test-shard lanes. 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.

@xiaoan17
xiaoan17 force-pushed the codex/fix-lark-core-import-boundary-20261029 branch from 1d36597 to b513b0d Compare October 29, 2026 06:28
xiaoan17 added a commit to xiaoan17/loopx that referenced this pull request Oct 29, 2026
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>
xiaoan17 added a commit to xiaoan17/loopx that referenced this pull request Oct 29, 2026
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>
@xiaoan17
xiaoan17 force-pushed the codex/fix-lark-core-import-boundary-20261029 branch 3 times, most recently from 8f975e9 to 3fa5c2d Compare October 29, 2026 08:10
xiaoan17 added a commit to xiaoan17/loopx that referenced this pull request Oct 29, 2026
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>
xiaoan17 pushed a commit to xiaoan17/loopx that referenced this pull request Oct 29, 2026
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>
@xiaoan17
xiaoan17 force-pushed the codex/fix-lark-core-import-boundary-20261029 branch 2 times, most recently from d71e8ac to 1aab61e Compare October 29, 2026 09:25

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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>
@xiaoan17
xiaoan17 force-pushed the codex/fix-lark-core-import-boundary-20261029 branch from 1aab61e to 9b001b6 Compare October 29, 2026 11:04
@huangruiteng
huangruiteng merged commit 6d6681b into loopx-project:main Oct 29, 2026
5 of 6 checks passed
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