diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index 6a4b46ff83..48cd622f6f 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -302,6 +302,18 @@ records both edges. Existing Ark/JEV private edges remain migration debt and receive no new exemption. The facade forwards existing behavior; it must not copy candidate, acceptance, process-lifetime or locking decisions. +One more public module joins that seam. `loopx.runtime_activation` owns the +transition of services that are already installed, and its +`managed_chat_activation_guard` holds the accepted-Chat-work fence and the +ingress lock while a managed client restarts. `loopx-codex-provider-routing` +uses that guard, and no other entry point of that module, to fence accepted Chat +work before it restarts the Chat services it manages. The guard grants no work +authority: it holds the ingress admission lock and refuses to continue while a +non-terminal Turn is pending. A pending ingress receipt is not part of that +check. The import ratchet records that edge as well. This +is a third public module in the seam and does not widen it: a package still may +not import the executor or another kernel-private module. + The stdio relay cancels its owned process group on parent EOF, SIGTERM or SIGINT and waits for the existing supervisor's cleanup before exiting. POSIX process-group behavior and Windows best-effort tree cleanup retain their @@ -696,11 +708,39 @@ Approval needs a reviewed decision, not implementation progress or silence. - **Remaining:** the wider M1–M5 distribution and zero-private-import acceptance is not closed; D1–D4 stay open. No packaging or authority decision changes. +### 2026-10-29 — Codex provider-routing activation seam + +- **Baseline:** `5d7f0a61b`. +- **Delivered:** the supported host integration seam in Section 5 names one more + public module. `loopx.runtime_activation` owns the transition of services that + are already installed, and `loopx-codex-provider-routing` uses its + `managed_chat_activation_guard` to hold the accepted-Chat-work fence and the + ingress lock while it restarts the Chat services it manages. The import + ratchet records that edge. +- **Boundary repair:** the guard grants no work authority — it holds the ingress + admission lock and refuses to continue while a non-terminal Turn is pending, + and a pending ingress receipt is not part of that check — and + the package uses no other entry point of that module. The seam stays closed: a + package still may not import the executor or another kernel-private module, + and the pinned Ark/JEV edges remain migration debt. +- **Evidence:** the import ratchet pins the edge instead of leaving it + unrecorded, and the Codex provider-routing native host smoke exercises the + guarded restart path. `tests/test_chat_runtime_activation.py:: + test_a_pending_ingress_receipt_alone_does_not_defer_the_restart` pins the + narrowed boundary: a pending `loopx_queue` or `loopx_inbox` receipt on its own + does not defer the restart, because the guard reads accepted work through the + Turn state. +- **Remaining:** the wider M1–M5 distribution and zero-private-import acceptance + is not closed; D1–D4 stay open. The seam records one module for one caller, + not a general package-to-kernel permission. + ## Appendix B: Decision log | Date | Decision | Owner / approval | Alternatives | Normative sections changed | | --- | --- | --- | --- | --- | -| — | — | — | — | — | +| 2026-10-29 | `loopx.runtime_activation` joins the supported host integration seam, so the Codex provider-routing package can hold the accepted-Chat-work fence while it restarts the Chat services it manages | Maintainers, through RFC review and merge | Restart the Chat services without the fence; copy the lock and state check into the package; keep the edge unrecorded | Section 5 | + +No other decision is approved. D1–D4 remain open proposals. ## Appendix C: Evidence registry diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md index 9d64e1d4d2..1a6b97e012 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -208,6 +208,15 @@ MemT 只使用这两个公开模块,不能直接 import executor 或其他内 边。Ark/JEV 的既有私有边仍是迁移积压,不新增豁免。facade 只转发现有行为, 不能复制候选、验收、进程生命周期或文件锁决策。 +该接缝再增加一个公开模块。`loopx.runtime_activation` 负责已安装服务的切换, +其 `managed_chat_activation_guard` 在受管客户端重启期间持有"已受理 Chat 工作" +栅栏与入口锁。`loopx-codex-provider-routing` 只使用该栅栏、不使用该模块的其他 +入口,在重启自己管理的 Chat 服务之前为已受理的 Chat 工作加栅栏。该栅栏不授予 +工作权限:它持有入口准入锁,并在仍有非终态 Turn 时拒绝继续。待处理的入口收据 +不在该检查范围内。import ratchet +同样钉住这条边。这是接缝中的第三个公开模块,并没有放宽接缝:包依旧不能 import +executor 或其他内核私有模块。 + stdio relay 在父进程 EOF、SIGTERM 或 SIGINT 时取消所持进程组,并等待已有 监督器完成回收再退出。POSIX 进程组行为与 Windows 尽力而为的进程树回收仍沿用 原资格边界。这是进程监督,不是沙箱;操作者选择的工具只保有原先获准的效果。 @@ -498,11 +507,33 @@ R0 可以先于目录移动交付,但不代表内核合格。R3/R4 可选, - **剩余:** 不关闭更广的 M1–M5 发行物与零私有 import 验收;D1–D4 仍未决。 不改变打包方案或权威决策。 +### 2026-10-29 — Codex provider-routing 激活接缝 + +- **基线:** `5d7f0a61b`。 +- **交付:** 第 5 节的受支持宿主接入接缝再增加一个公开模块。 + `loopx.runtime_activation` 负责已安装服务的切换, + `loopx-codex-provider-routing` 使用其 `managed_chat_activation_guard`,在重启 + 自己管理的 Chat 服务期间持有"已受理 Chat 工作"栅栏与入口锁。import ratchet + 钉住这条边。 +- **边界修复:** 该栅栏不授予工作权限——它持有入口准入锁,并在仍有非终态 Turn 时 + 拒绝继续;待处理的入口收据不在该检查范围内——且该包不使用该模块的其他入口。 + 接缝仍然闭合:包依旧不能 + import executor 或其他内核私有模块,已钉住的 Ark/JEV 私有边仍是迁移积压。 +- **证据:** import ratchet 钉住这条边,而不是让它处于未记录状态;Codex + provider-routing 的 native host smoke 覆盖受保护的重启路径。 + `tests/test_chat_runtime_activation.py::test_a_pending_ingress_receipt_alone_does_not_defer_the_restart` + 钉住收窄后的边界:只有一条待处理的 `loopx_queue` 或 `loopx_inbox` 收据并不会 + 推迟重启,因为该栅栏是通过 Turn 状态读取已受理工作的。 +- **剩余:** 不关闭更广的 M1–M5 发行物与零私有 import 验收;D1–D4 仍未决。 + 该接缝只为一个调用方记录一个模块,不是通用的包到内核许可。 + ## 附录 B:决策日志 | 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 | | --- | --- | --- | --- | --- | -| — | — | — | — | — | +| 2026-10-29 | `loopx.runtime_activation` 加入受支持的宿主接入接缝,使 Codex provider-routing 包能在重启自己管理的 Chat 服务期间持有"已受理 Chat 工作"栅栏 | maintainers,经 RFC 评审与合并 | 不带栅栏重启 Chat 服务;把锁与状态检查复制进包;让这条边保持未记录 | 第 5 节 | + +除此以外没有已批准的决策。D1–D4 仍是未决提案。 ## 附录 C:证据登记 diff --git a/tests/architecture/package_kernel_import_pins.json b/tests/architecture/package_kernel_import_pins.json index a1f468f70c..c8bc011238 100644 --- a/tests/architecture/package_kernel_import_pins.json +++ b/tests/architecture/package_kernel_import_pins.json @@ -1,12 +1,15 @@ { "schema": "loopx_package_kernel_import_pins_v0", - "baseline_commit": "70bc98198ce9e8c33e461cc41fe77243d713dcf0", - "note": "Exact pin of every `loopx.*` module imported by a co-located package. Pre-existing private edges remain migration debt. The supported-host-integration decision in the monorepo-distribution-split RFC Section 5 permits MemT's two public integration imports; it does not permit private kernel imports. Remove an entry in the PR that removes the import. Adding an entry needs a recorded design decision.", + "baseline_commit": "5d7f0a61b0b79876801c05e1d26352687f270794", + "note": "Exact pin of every `loopx.*` module imported by a co-located package. Pre-existing private edges remain migration debt. The supported-host-integration decision in the monorepo-distribution-split RFC Section 5 permits MemT's two public integration imports and the Codex provider-routing activation guard; it does not permit private kernel imports. Remove an entry in the PR that removes the import. Adding an entry needs a recorded design decision.", "imports": { "loopx-ark-turn": [ "loopx.control_plane.turn_driver.host_candidate", "loopx.file_lock" ], + "loopx-codex-provider-routing": [ + "loopx.runtime_activation" + ], "loopx-finance-value-discovery": [ "loopx.extensions.lark.presentation.message_card", "loopx.extensions.presentation" diff --git a/tests/architecture/test_package_kernel_import_ratchet.py b/tests/architecture/test_package_kernel_import_ratchet.py index 227d56fc4d..177c28bafd 100644 --- a/tests/architecture/test_package_kernel_import_ratchet.py +++ b/tests/architecture/test_package_kernel_import_ratchet.py @@ -5,9 +5,10 @@ contracts and CLI subcommands through `CapabilityRegistry`; it never imports kernel-private modules." Three packages already do. The Section 5 supported host integration decision adds MemT's public facade and shared file-lock API, -not another private-kernel exception. This guard pins those -edges exactly so a new one needs a recorded decision and a removed one -lowers the pin in the same PR, following `test_top_level_module_budget`. +plus the Codex provider-routing activation guard, not another private-kernel +exception. This guard pins those edges exactly so a new one needs a recorded +decision and a removed one lowers the pin in the same PR, following +`test_top_level_module_budget`. """ from __future__ import annotations diff --git a/tests/test_chat_runtime_activation.py b/tests/test_chat_runtime_activation.py index 59d70c1f10..83b6e8a394 100644 --- a/tests/test_chat_runtime_activation.py +++ b/tests/test_chat_runtime_activation.py @@ -14,7 +14,7 @@ import pytest from loopx.chat_store import ChatSessionStore -from loopx.chat_ingress import chat_runtime_activation_lock +from loopx.chat_ingress import chat_runtime_activation_lock, chat_runtime_activation_state from loopx.runtime_activation import restart_services_for_runtime_activation from loopx.self_update import execute_update_plan, render_update_plan_markdown @@ -112,6 +112,34 @@ def test_queue_without_an_active_pointer_still_defers_until_original_work_finish assert store.load_turn(sid, queued["turn_id"])["message"] == "later input" +@pytest.mark.parametrize("mode", ["loopx_queue", "loopx_inbox"]) +def test_a_pending_ingress_receipt_alone_does_not_defer_the_restart(services, mode): + """The guard fences non-terminal Turns, not pending ingress receipts. + + A pending ingress receipt reserves external input before the provider + delivers it, so it is not a Turn yet. The activation guard reads the + accepted work through the Turn state, so a pending receipt on its own + does not defer the restart. This is the boundary the guard actually + holds, not a promise that pending receipts are covered. + """ + runtime, _agents, calls = services + store = ChatSessionStore(runtime) + sid = session(store) + receipt, created = store.create_ingress_receipt( + sid, + client_ingress_id=f"client-{mode}", + mode=mode, + message="reserved input", + ) + assert created is True + assert receipt["status"] == "pending" + assert chat_runtime_activation_state(runtime)["nonterminal_turns"] == 0 + + result = activate() + assert result["restart_status"] == "restarted" + assert len(calls) == 2 + + @pytest.mark.parametrize( "damage", ["session", "turn", "active_missing", "unknown_status", "invalid_status"] )