From 061f4080c83c8a9d38e27b99367f664b4c043a1d Mon Sep 17 00:00:00 2001 From: xiaoan17 Date: Fri, 30 Oct 2026 01:33:35 +0800 Subject: [PATCH] docs(rfc): state the activation guard's refusal boundary The Section 5 seam records the Codex provider-routing host's supported edge, but it does not say what the guard actually refuses. The guard holds the ingress admission lock and refuses while a non-terminal Turn exists; it does not read the ingress receipt ledger, so a pending loopx_queue or loopx_inbox receipt is not a Turn and passes through. Record that boundary in both mirrors and in the execution ledger, and pin both sides with cases that read a real temporary Session and ingress store against the real guard. Signed-off-by: xiaoan17 --- .../rfcs/monorepo-distribution-split-v0.md | 30 ++++++++++ .../monorepo-distribution-split-v0.zh-CN.md | 24 ++++++++ tests/test_chat_runtime_activation.py | 60 ++++++++++++++++++- 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index 53ff3618a7..86daaa0750 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -312,6 +312,13 @@ decision. Missing or unsupported Core defers the service restart. The no-service path remains independent of Core. This one supported public edge is pinned; it grants no authority to stop a service or add other kernel imports. +The guard's refusal is exactly as narrow as that observation. It holds the +ingress admission lock and refuses to continue while a non-terminal Turn +exists. A pending ingress receipt reserves external input before the provider +delivers it, so it is not a Turn, and the guard does not read the ingress +receipt ledger. A pending `loopx_queue` or `loopx_inbox` receipt therefore +passes through, while a queued Turn is refused with `chat_work_pending`. + 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 @@ -706,6 +713,29 @@ 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-30 — Codex provider-routing activation guard boundary + +- **Baseline:** `10c84d1c0c`. +- **Delivered:** the Section 5 seam for the Codex provider-routing host now + states what its guard actually refuses. It holds the ingress admission lock + and refuses to continue while a non-terminal Turn exists. A pending ingress + receipt reserves external input before the provider delivers it, so it is not + a Turn, and the guard does not read the ingress receipt ledger. +- **Boundary repair:** a pending `loopx_queue` or `loopx_inbox` receipt passes + through, while a queued Turn is refused with `chat_work_pending`. The + package's supported edge stays the single pinned + `loopx.extensions.chat_service_guard` import; this entry changes no import, + permission, default or persisted format. +- **Evidence:** `tests/test_chat_runtime_activation.py:: + test_a_pending_ingress_receipt_alone_does_not_defer_the_restart` and + `test_a_queued_turn_still_defers_the_restart_with_a_pending_ingress_receipt` + read both sides of the boundary through a real temporary Session and ingress + store, against the real guard. +- **Remaining:** the wider M1–M5 distribution and zero-private-import acceptance + is not closed; D1–D4 stay open. A pending ingress receipt remains uncovered + by the guard, and covering it would be a runtime change with its own + acceptance, not a documentation change. + ## Appendix B: Decision log | Date | Decision | Owner / approval | Alternatives | Normative sections changed | 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 caf9dc4c0d..7471d15138 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -216,6 +216,11 @@ MemT 只使用这两个公开模块,不能直接 import executor 或其他内 也不复制决策。Core 缺失或不支持时延期重启;没有托管服务的路径仍不依赖 Core。 import ratchet 只记录这一条受支持公开边;它不授予停止服务或增加其他内核 import 的权限。 +该栅栏的拒绝范围与那份观察一样窄:它持有入口准入锁,并在仍有非终态 Turn 时 +拒绝继续。待处理的入口收据是在 provider 投递之前预留的外部输入,因此还不是 +Turn,而该栅栏也不读取入口收据账本。于是只有一条待处理的 `loopx_queue` 或 +`loopx_inbox` 收据会被放行,而 queued Turn 会以 `chat_work_pending` 被拒绝。 + stdio relay 在父进程 EOF、SIGTERM 或 SIGINT 时取消所持进程组,并等待已有 监督器完成回收再退出。POSIX 进程组行为与 Windows 尽力而为的进程树回收仍沿用 原资格边界。这是进程监督,不是沙箱;操作者选择的工具只保有原先获准的效果。 @@ -506,6 +511,25 @@ R0 可以先于目录移动交付,但不代表内核合格。R3/R4 可选, - **剩余:** 不关闭更广的 M1–M5 发行物与零私有 import 验收;D1–D4 仍未决。 不改变打包方案或权威决策。 +### 2026-10-30 — Codex provider-routing 激活栅栏的边界 + +- **基线:** `10c84d1c0c`。 +- **交付:** 第 5 节中 Codex provider-routing host 的接缝现在写明该栅栏实际 + 拒绝什么:它持有入口准入锁,并在仍有非终态 Turn 时拒绝继续。待处理的入口 + 收据是在 provider 投递之前预留的外部输入,因此还不是 Turn,而该栅栏也不 + 读取入口收据账本。 +- **边界修复:** 只有一条待处理的 `loopx_queue` 或 `loopx_inbox` 收据会被 + 放行,而 queued Turn 会以 `chat_work_pending` 被拒绝。该包的受支持依赖边仍是 + 唯一被 pin 的 `loopx.extensions.chat_service_guard` import;本条目不改变任何 + import、权限、默认值或持久格式。 +- **证据:** `tests/test_chat_runtime_activation.py:: + test_a_pending_ingress_receipt_alone_does_not_defer_the_restart` 与 + `test_a_queued_turn_still_defers_the_restart_with_a_pending_ingress_receipt` + 通过真实临时 Session 与 ingress store、针对真实栅栏,读出该边界的两侧。 +- **剩余:** 不关闭更广的 M1–M5 发行物与零私有 import 验收;D1–D4 仍未决。 + 待处理的入口收据仍不被该栅栏覆盖;要覆盖它是一次带自有验收的运行时变更, + 而不是文档变更。 + ## 附录 B:决策日志 | 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 | diff --git a/tests/test_chat_runtime_activation.py b/tests/test_chat_runtime_activation.py index 59d70c1f10..8d244d92fc 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,64 @@ 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 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 + + +def test_a_queued_turn_still_defers_the_restart_with_a_pending_ingress_receipt( + services, +): + """The same receipt next to a non-terminal Turn still defers the restart. + + The receipt alone does not defer; a queued Turn does. Recording both + sides keeps the guard's contract readable: it reads accepted work, and + only accepted work. + """ + runtime, _agents, calls = services + store = ChatSessionStore(runtime) + sid = session(store) + receipt, created = store.create_ingress_receipt( + sid, + client_ingress_id="client-queued", + mode="loopx_queue", + message="reserved input", + ) + assert created is True + store.create_queued_turn( + sid, client_turn_id="queued", message="later input" + ) + assert chat_runtime_activation_state(runtime)["nonterminal_turns"] == 1 + + result = activate() + assert result["restart_status"] == "deferred" + assert result["restart_reason"] == "chat_work_pending" + assert not calls + + @pytest.mark.parametrize( "damage", ["session", "turn", "active_missing", "unknown_status", "invalid_status"] )