Skip to content

docs(rfc): state the activation guard's refusal boundary - #7695

Merged
huangruiteng merged 1 commit into
loopx-project:mainfrom
xiaoan17:codex/record-codex-activation-seam-v2
Oct 30, 2026
Merged

huangruiteng merged 1 commit into
loopx-project:mainfrom
xiaoan17:codex/record-codex-activation-seam-v2

Conversation

@xiaoan17

@xiaoan17 xiaoan17 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Goal and source

The monorepo distribution split RFC's Section 5 records the Codex
provider-routing host's supported integration edge. The seam is correct, but
the document does not say what the activation guard actually refuses, so a
reader cannot tell whether a pending ingress receipt is covered.

The real managed_chat_activation_guard takes the ingress admission mutex and
then reads chat_runtime_activation_state(...), which enumerates Sessions and
Turns and never reads the ingress receipt ledger. A pending ingress receipt
reserves external input before the provider delivers it, so it is not a Turn,
and it passes through. A queued Turn is refused with chat_work_pending.

This is the same boundary an earlier review of a superseded pull request
required. That pull request was replaced by upstream
#7684, which already
recorded the seam and the package's supported import pin. This PR adds only
the part that is still missing: the guard's refusal boundary, and the
execution-ledger entry for it.

What this PR delivers

  • Section 5 (both mirrors) now state the boundary exactly as the runtime
    holds it: the guard takes the ingress admission lock and refuses to continue
    while a non-terminal Turn exists. A pending loopx_queue or loopx_inbox
    receipt is not a Turn, and the guard does not read the ingress receipt
    ledger, so it passes through; a queued Turn is refused with
    chat_work_pending.
  • Appendix A (both mirrors) records this delivery: baseline, what was
    delivered, the boundary, the evidence and what remains open.
  • Two cases pin both sides of the boundary against the real guard, using a
    real temporary Session and the real ingress store:
    • test_a_pending_ingress_receipt_alone_does_not_defer_the_restart — a
      pending loopx_queue or loopx_inbox receipt alone does not defer the
      restart.
    • test_a_queued_turn_still_defers_the_restart_with_a_pending_ingress_receipt
      — the same receipt next to a queued Turn still defers it.

No import, pin, permission, default, persisted format or runtime behavior
changes. The package's supported edge stays the single pinned
loopx.extensions.chat_service_guard import that
#7684 already recorded.

What remains open

A pending ingress receipt is still uncovered by the guard. Covering it would
be a runtime change with its own acceptance — the guard would have to read the
ingress receipt ledger — not a documentation change. This PR states the
boundary; it does not change it.

Validation

pytest tests/test_chat_runtime_activation.py                                  28 passed
pytest tests/architecture/test_package_kernel_import_ratchet.py               2 passed
pytest tests/architecture/test_rfc_status_index.py                           13 passed
python scripts/check_rfc_bilingual.py --base origin/main --head HEAD          passed
python scripts/generate_rfc_status_index.py --check                          clean
mkdocs build --strict                                                          built, 0 errors
loopx canary premerge --from-git-diff --git-diff-base origin/main             14 checks, 0 failures, 0 warnings
loopx check --scan-path <changed paths>                                       clean

Base: 10c84d1c0c (main, carrying #7645 and #7684).

CI state at this head

windows-powershell is red at this head, on the four self-update cases that
#7675 fixes. They are a
base-red inherited from main: this PR changes no self-update code, and the
file it changes is byte-identical between this head and main. #7675 merges
first, then this PR rebases onto the new main and goes green.
Every other lane
— browser, stage2c, typescript-core, kernel-contracts, build, Sign-off —
passes at this head.

The test-shard lanes are queued, not failing: the repository currently has ten
Python Tests runs waiting for an ubuntu-latest runner, and no shard in
either of our runs has been assigned one yet.

The combined result is already verified

Landing this pull request together with the self-update fix was simulated on top
of the current main: both apply without a conflict, and the combined tree passes
pytest on tests/test_chat_runtime_activation.py,
tests/architecture/test_package_kernel_import_ratchet.py,
tests/architecture/test_rfc_status_index.py,
tests/test_self_update_runtime_activation.py and
tests/test_self_update_download.py (105 passed, 3 skipped), the RFC
bilingual check, the derived-index check, and canary premerge with 17
selected checks and no failure or warning.

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 <anbc1224@gmail.com>
@xiaoan17
xiaoan17 force-pushed the codex/record-codex-activation-seam-v2 branch from 5a97a11 to 061f408 Compare October 29, 2026 18: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

没有发现当前完整提案的阻断问题。审查 head 061f4080c83c8a9d38e27b99367f664b4c043a1d,实际基线 a36e01c32b0a290a96466c766629a0b01850f823。

动机 / Motivation

集成 Codex provider-routing host 的维护者需要判断服务重启时哪些已接收输入受到保护。

原文只说观察 accepted work,容易让读者把 pending ingress receipt 当作受保护 Turn;补充后明确仅有收据会通过,queued Turn 会延期。

独立真实存储检查在两种 ingress mode 下均观察到 receipt-only 通过、queued Turn 拒绝、完成 Turn 后再次通过,且原输入和记录保持。

本次交付是当前守卫边界的双语说明和耐久测试;pending receipt 覆盖、发行物拆分、真实服务和已安装宿主资格仍由各自运行时验收。

改动思路 / Approach

这是一项有完整消费者的文档与回归维护:沿 #7684 已记录的单一公开接缝说明实际 protection,保留原 runtime owner 的决策和 ingress lock,没有把 pending receipt 的存在解释成新执行权。现有 Section5 和 activation fixture 已足够表达该结果,另加模块、开关或测试框架都没有必要。

实际调用是 NativeHost.restart_guard → 公开 chat_service_guard facade → 原 managed_chat_activation_guard → chat_runtime_activation_state。后者严格枚举 Session/Turn,不读取 ingress receipt ledger。reservation 发生在 provider 投递前,所以 receipt-only 能通过;真实 queued Turn 仍会阻止继续。说明这个已有限制不等于批准新增安全保证,也不需要本 PR 提前实现整套发行物拆分。

具体改动 / Changes

独立接受依据:docs/architecture/rfcs/monorepo-distribution-split-v0.md,spec_revision a36e01c32b0a290a96466c766629a0b01850f823。先读该不可变版本的 §5、I2/I6、迁移与接受边界,再核对当前修改;没有用新写的文字反向证明规范。

  • I2 — implemented: kernel truth 和执行 owner 保留。完整 production/package/config tree 对照无变化;实际 guard 读同一 Session/Turn store。
  • I6 — implemented: import、权限、schema、public/private 和 licence 均无改动;公开边界与既有 import pin 检查通过。
  • Supported host integration decision — implemented: facade 仍只转发原 guard 与 RuntimeActivationDeferred,package 仍消费单个受支持公共边;实际 ingress lock 和非终态检查保留在原 owner。
  • Distribution acceptance — out_of_scope: M1–M5、D1–D4 及零私有 import/独立发行物资格继续按原 RFC;此维护任务没有关闭这些父级接受项。

关键内容讲解

  • §5 新增拒绝边界 及中文镜像明确区分 reservation receipt 与非终态 Turn,两种语言保留同一范围、失败和未覆盖边界。
  • 非规范 execution ledger 记录本次交付和 Remaining,没有提升 RFC 的 Proposal 成熟度。ledger 的 10c84d1c0c 是历史基线;本轮独立验证用上面的实际 PR 基线,未用短基线代替当前精确来源。
  • pending-only 参数化测试 使用真实临时 Session/ingress store,对 loopx_queue 与 loopx_inbox 均检查实际 restart outcome;queued+pending 测试 同时验证原 Turn 仍触发 chat_work_pending。production 未改,scope为双语文档 +54 与原测试模块 +59/-1,共3文件 +113/-1。

对主干的风险 / Risks

最强反例是文本夸大保护,或测试把待证明的计数直接mock好。本轮直接调用公开 facade,以两种真实 receipt 模式分别走 pending-only → queued → completed,共6个阶段:通过 → chat_work_pending → 再通过;每次 guard 前后原 Session/Turn/ingress 内容一致。完成由原 store owner 写入,guard 没有替业务重放或改变输入。

另在隔离解释器中做两种故意错误:把拒绝扩大为“只要有 Session”时,两个 pending-only 实例均在实际结果处失败;去掉非终态拒绝时,queued+pending 实例在实际结果处失败。原源码的完整 activation28 + architecture15,共 43项通过。这些负控制证明新测试可区分边界,不是把 mutation 失败当成正常验证失败。

PASS:Ruff/compile、双语 committed diff、RFC index、严格 MkDocs 构建与英中 HTML 新段落读回、当前 exact-scope CQR、标准 premerge 5 direct +11 selected 全通过,0失败/0warning/0manual hold。原 test 环境没有 mkdocs 的工具启动失败保留;按仓库 docs/requirements-docs.txt 提供构建依赖后原严格构建完成,没有改产品依赖或降低严格级别。未获取、轮询或等待远端 CI。

有界检索既有 coverage 及作者最近6项后,没有发现这些新边界实例是同形重复或无消费者脚手架;#7642 的 seam 已被 #7684 替代,当前只补确有缺口的拒绝范围。其它临时 Windows probe 不由此评审认证。

我的整体评价 / Verdict

APPROVE。 当前具体维护任务已完成。long_horizon preserved:原非终态承诺和恢复 owner 保留,没有把未覆盖 receipt 写成已保护;user_experience improved:集成者可直接理解通过、等待和恢复条件,没有新增输入准备或确认轮次。

当前边界是如实说明已有行为并把对照放在既有测试位置;有界整理已应用,不需要新框架、重复 authority 或人为后继任务。既有 pending ingress 仍未覆盖,是明确留下的运行时边界;若未来扩大保护,需由原 runtime owner 独立设计和验收。

检查对象是 RFC 深层技术正文与测试;首屏、导航、布局和可操作界面未变,已严格构建并核对英中 HTML 新增段落,视觉审查不适用。 视觉未验证范围:未做浏览器视口或真实服务停止测试;未改变页面布局或执行交互。实际 launchctl 在测试中模拟,本轮没有停止真实服务、切换已安装 host 或认证跨平台/持续工作收益。批准这项文档维护不等于这些更广资格已经完成。

English verdict: APPROVE — 061f408. Both RFC mirrors accurately state the existing activation guard: pending ingress receipts alone pass, while a queued Turn defers. Six real-store guard stages, 43 focused tests, two mutation controls, strict docs build and all current standard premerge checks pass. Live service/installed-host and broader distribution qualifications remain separate.

@xiaoan17

Copy link
Copy Markdown
Contributor Author

合并顺序

本 PR 的 windows-powershell lane 目前是红的,但失败的不是本 PR 的改动:

FAILED tests/test_self_update_runtime_activation.py::test_update_preserves_selected_route_through_install_and_readback[python_pip]
FAILED tests/test_self_update_runtime_activation.py::test_update_preserves_selected_route_through_install_and_readback[python_pipx]
FAILED tests/test_self_update_runtime_activation.py::test_python_distribution_activation_ignores_archive_bin_override[python_pip]
FAILED tests/test_self_update_runtime_activation.py::test_python_distribution_activation_ignores_archive_bin_override[python_pipx]
4 failed, 415 passed, 20 skipped

这 4 个用例是主干既有的失败,与本 PR 无关——本 PR 只改
docs/architecture/rfcs/monorepo-distribution-split-v0{,.zh-CN}.md 和
tests/test_chat_runtime_activation.py,不碰 self-update。

修复在 #7698:那些用例用共享的
doctor fixture 描述 release snapshot 属主,而 Windows 上 release snapshot 的 apply
归 install-windows.ps1 所有,于是计划阶段就被拒绝,driver 从未运行。#7698 让每个
driver 使用自己能执行的属主报告,已在 Windows runner 上验证(419 passed, 20 skipped,
零失败)。

因此:#7698 合并后,本 PR rebase 到新的 main 即可全绿。 本 PR 的其他 check
(浏览器 lane、stage2c、typescript-core、kernel-contracts、test-shard)在当前 head
上均已通过。

@xiaoan17

Copy link
Copy Markdown
Contributor Author

更新:合并顺序依赖现在指向 #7675

先前这条评论指向 #7698。那个 PR 已被上游 #7675
取代并关闭——两者修的是同一批用例、同一处根因、同一文件,#7675 的覆盖还略强。

所以顺序不变、依赖对象变了:

已核实:本 PR 的 head 与 main 的 tests/test_self_update_runtime_activation.py 逐字相同,
所以这个红灯是纯继承的主干基线红,与本 PR 的改动无关。

另外,本 PR 与 #7675 的合并顺序已在 main 上模拟验证过:零冲突,105 passed / 3 skipped,
双语检查通过,canary premerge 17 项 0 失败 0 警告。

@xiaoan17

Copy link
Copy Markdown
Contributor Author

更新:占死 6 条 lane 的 7 个 shard 里,有一个是本 PR 的

上一条评论说占死 lane 的是本 PR 的 run 加上 main 的 run。重新全量扫描(这次覆盖了
更早的 run,之前只扫最近 100 个漏掉了)后,准确的分布是 6 条 lane 各被一个
in_progress 的 shard 占着
:

2026-10-30T01:15:06Z test-shard (3)  run=40929089454   ← 本 PR
2026-10-30T01:21:48Z test-shard (4)  run=40929089454   ← 本 PR
2026-10-30T01:23:55Z test-shard (1)  run=40929089454   ← 本 PR
2026-10-30T01:28:05Z test-shard (5)  run=40929089454   ← 本 PR
2026-10-30T01:47:57Z test-shard (2)  run=40929089454   ← 本 PR
2026-10-30T01:47:46Z test-shard (6)  run=40930663039   ← #7695

也就是说,除本 PR 的 run 之外,#7695 的 run 也有一个 shard 卡在同一分钟。
main 的 run 那个已经不在列表里了。

这些 job 不是测试卡住,而是 runner 中途失联:

runner 断开后 job 永远停在 in_progress,而 job 自己的 timeout-minutes: 60 没有
清理它们。由于 test-shard 用跨 PR 共享的 concurrency group
(loopx-python-test-shard-<N>,queue: max),这 6 个死 job 占满 lane,仓库里每个
PR 的 shard 都排在后面——包括 #7695 的另外 5 个 shard,它们从 18:28 排到现在。

解除办法(需要上游 admin;fork 身份是 403)

POST /repos/loopx-project/loopx/actions/runs/40929089454/cancel   ← 本 PR 的 run,5 个 shard
POST /repos/loopx-project/loopx/actions/runs/40930663039/cancel   ← #7695 的 run,1 个 shard

取消后需要给这两个 PR 重新触发一次 CI。#7695 的另外 5 个 shard 一旦有 lane 就会
开始跑。

这不是本 PR 的改动造成的:本 PR 只改 tests/test_self_update_runtime_activation.py,
而卡住的 shard 分别停在 tests/control_plane/test_todo_decision_scope_runtime_boundary.py
和 tests/architecture/test_source_session_registry_denial.py / test_top_level_module_budget.py,
都与它无关。

@xiaoan17 xiaoan17 closed this Oct 30, 2026
@xiaoan17 xiaoan17 reopened this Oct 30, 2026
@huangruiteng
huangruiteng merged commit 61e10de into loopx-project:main Oct 30, 2026
57 of 73 checks passed
@xiaoan17

Copy link
Copy Markdown
Contributor Author

结论:不是启动预算问题,Windows 上测量证明预算充裕

我用一个一次性诊断在 Windows runner 上实测(PR #7715,测量代码已从本分支撤掉,
提交留在 codex/measurement-archive):

STARTUP_MEASURE platform='nt' ready_seconds=0.406 live_lock_seconds=5.813
                 live_lock_code='mutation_lock_timeout'
                 ready_budget=15.0 mutation_lock_budget=5.0

对照 macOS:ready_seconds=0.215 live_lock_seconds=5.155。

所以:

  • Windows 上 node 冷启动只有 0.41 秒,不是 10 秒。
  • live_lock 总耗时 5.81 秒 = 0.4 秒冷启动 + 5 秒锁等待,远在 15 秒 ready 预算内。
  • 这次运行里 TypeScript 侧成功发布了 mutation_lock_timeout 精确诊断,Python 侧也
    正确读到了它。

我此前推断的"node 冷启动 + 5s 锁等待 > 15s 预算"是错的,本条用实测推翻它。
Python 侧的 15 秒 ready 预算和 TypeScript 侧的 5 秒 mutation lock 预算都不需要动。

真正的问题:Windows 上锁属主的存活判定不稳定

test_locator_publication_failure_surfaces_safe_typed_startup_diagnostic[live_lock]
在 Windows 上有时得到 runtime_startup_timeout(期望 mutation_lock_timeout),有时通过。
本次运行它就通过了。

失败时的实际形态是 TypeScript 侧已经算出了 mutation_lock_timeout,但 Python 侧
没读到那份诊断:

  • loopx/control_plane/effect_runtime_server.ts 的 failStartup(code, message) 用
    writeSync(2, ...) 写 stderr 信封后 process.exit(2);
  • loopx/control_plane/effect_runtime.py 只在 process.poll() 显示子进程已退出时才读
    stderr 取精确诊断,否则到 15 秒预算就 terminate() 并报通用的 runtime_startup_timeout。

也就是说,精确诊断能否送达,取决于 Python 的轮询循环有没有在预算内观察到子进程退出。

而 mutation_lock_timeout 出现在 Windows 上说明 TS 侧的锁等待发生了:TS 认为锁被
一个活属主占住,等满 5 秒后放弃。属主存活判定在
loopx/control_plane/effect_runtime_io.ts:

function processIsAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch (error) {
    return (error as NodeJS.ErrnoException).code === "EPERM";
  }
}

在 Windows 上,process.kill(pid, 0) 对已死但 PID 被复用的进程、以及对无权探测
的进程都可能返回成功或 EPERM,两种情况都会被判成"属主活着"。于是本该被回收的陈旧
锁要等满 5 秒才放弃,而 Python 侧的诊断读取窗口与这个 5 秒叠加后,在慢的 Windows
runner 上就会错过子进程退出的时机。

每次 Windows run 的失败集合都不同,且都在同一类操作上

四个 Windows run 的失败清单:

run 失败
123649332960 4 个 self-update 用例
123724103427 4 个 self-update 用例
123893591690 2 个 shadow 用例 + 4 个 self-update 用例
123644731872 2 个 test_chat_manager_inspection 超时

test_shadow_observation_retirement 的两个用例报的是
could not publish its startup locator (mutation_lock_timeout)——同样是启动 Effect
runtime 时取锁失败。test_chat_manager_inspection 报 TimeoutError: chat turn wait timed out。

共同的形状:都是"启动 Effect runtime 并等它就绪"这一步在不同负载下要么慢到越过
预算、要么撞上陈旧锁。这不是一个分类 bug,是 Windows 上启动路径的时序不稳。

建议的下一步(都需要 owner 判断,本 PR 不做)

  1. 不要动两个预算。 实测证明 15s 与 5s 都有充裕余量(0.41s 与 5.81s)。
  2. 值得看的是 TS 侧的属主存活判定。 Windows 上 process.kill(pid, 0) 的 EPERM/
    PID 复用语义与 POSIX 不同;一个能区分"这个 PID 仍是那个进程"的判据(在 Windows 上
    记录进程创建时间或句柄,而不是只记 PID)会消除陈旧锁要等满 5 秒的路径。这会同时
    收紧 5 秒锁等待和 Python 侧的诊断送达窗口。
  3. 诊断送达本身可以更稳。 Python 侧在预算耗尽时若 terminate() 之后仍能读到
    stderr 信封,应当优先采用那份精确诊断——它比通用的 runtime_startup_timeout
    信息更多。

第 2、3 点都改动运行时行为,需要各自的质量验收;本 PR 只交付结论,不在这里改。

(顺带说明:#7698 已关闭,它被上游 #7675 取代;那份 Windows 实测证据已经交接给
#7675。这条评论记录的是 effect-runtime 启动路径的另一个问题。)

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