Skip to content

docs(rfc): record the Windows startup delivery window - #7728

Open
xiaoan17 wants to merge 2 commits into
loopx-project:mainfrom
xiaoan17:codex/record-windows-startup-delivery-window
Open

xiaoan17 wants to merge 2 commits into
loopx-project:mainfrom
xiaoan17:codex/record-windows-startup-delivery-window

Conversation

@xiaoan17

@xiaoan17 xiaoan17 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Goal and source

The windows-powershell lane fails intermittently on operations that start the
Effect runtime and wait for it to become ready. The failing set differs per run:
test_effect_runtime_integration's live_lock case,
test_shadow_observation_retirement, test_chat_manager_inspection. Across six Windows lane runs
whose job logs are still retrievable, the failing file set was
test_self_update_runtime_activation alone (three runs), that file together
with test_shadow_observation_retirement (one run), that pair together with
test_effect_runtime_integration (one run), and test_chat_manager_inspection
alone (one run).

The
S2/S12 startup diagnostic checkpoint
of 2026-10-04 recorded that a live mutation lock must fail with the exact
mutation_lock_timeout code, and explicitly did not qualify Linux or Windows CI
as passing. This entry records what that checkpoint did not establish on
Windows, and corrects the premise that produced an earlier wrong conclusion
about the startup budgets.

Measured

The startup cost was measured on the Windows runner at 2f544f9692 and on
macOS at d3d9ad209d:

Host Ready startup Live-lock total Diagnostic delivered
Windows (nt) 0.406 s 5.813 s mutation_lock_timeout
macOS (posix) 0.170 s 5.140 s mutation_lock_timeout

The two revisions differ in effect_runtime.py, but only in the
runtime-source metadata read, _control_plane_root and _request_with_info.
The startup path itself — _start_runtime, STARTUP_READY_TIMEOUT_SECONDS,
_read_startup_stderr and the process.poll() check — is unchanged between
them, so both hosts are measured on the same startup code.

The live-lock total is the ready startup plus the TypeScript mutation-lock wait.
Both budgets have ample headroom: STARTUP_READY_TIMEOUT_SECONDS is 15.0 and
MUTATION_LOCK_TIMEOUT_MS is 5,000.

This corrects an earlier conclusion. The hypothesis that node cold start
plus the 5 s lock wait exceeds the 15 s ready budget on Windows is false. The
cold start is 0.406 s, and the live-lock case returned the exact
mutation_lock_timeout code on that same run. Neither budget should be
widened to explain this failure.

Where the exact diagnostic is lost

TypeScript computes the typed code and publishes it:
effect_runtime_server.ts failStartup writes the startup envelope to stderr
with writeSync and then calls process.exit. Python reads that envelope only
when process.poll() shows the child has already exited
(effect_runtime.py); otherwise it terminates the child at the budget and
reports the generic runtime_startup_timeout.

So the exact code is produced but not delivered when the poll loop does not
observe the exit inside the budget.

The lock in this path is not a stale lock. The fixture writes the current
process's own PID as the lock owner, so the owner is alive and
reclaimStaleMutationLock correctly refuses to reclaim it; waiting out the
5 s mutation-lock budget is the intended behaviour on every platform. The
measured live-lock total is 5.813 s on Windows and 5.140 s on macOS against a
15 s ready budget, and the exact code was delivered on both, so the budget is
not the constraint in the measured runs.

The intermittent Windows failure at 1e3401591 is a different situation.
Its job log is retrievable and shows
assert 'runtime_startup_timeout' == 'mutation_lock_timeout'. That code is
produced whenever the parent does not observe the child's exit inside the
ready loop. The baseline reads the published envelope only when a poll
within that loop reports an exit; after the loop it polls once more, but
that poll only decides whether to terminate the child, and the function then
reports the generic timeout without reading the capture again. The historical
run therefore does not distinguish two situations: the child had not published
yet when the budget expired, or the child had already published and exited
and the parent had not observed that exit before it left the loop. The same
job's pytest step ran 439 cases in 5 min 31 s, and the chunk holding the
failure took two minutes against six seconds for the chunk before it, so the
runner was heavily loaded — a plausible contributor to a late publication,
not a demonstrated cause.

Reading the capture at the moment the budget expires recovers the envelope in
the second situation only. At one second, three seconds and five seconds the
child is still running and stderr holds zero bytes, and the envelope appears
only once the child exits, at about 5.8 s, together with exit code 2: a child
that has not exited has published nothing, and a child that has exited leaves
the envelope in the capture for the baseline to leave unread.

It can be recovered by letting the child finish first, or by reading the
capture again once the child has exited. Waiting for the exit instead of
terminating at the budget yields the exact code in every measured case, and
the extra wait is bounded rather than open-ended: the child's exit lands at
5.13 s to 5.17 s regardless of whether the ready budget was 0.5 s, 1 s or 2 s,
because what the child waits for is the TypeScript mutation-lock budget. A
repair would therefore give the child a bounded grace window after the ready
budget, read the envelope if it appears, and report the generic timeout only
when the child does not finish inside that window. Such a repair covers both
situations above, so it does not depend on knowing which one the recorded
failure was. That repair is not made here.

What this PR does and does not do

It adds one bilingual execution-ledger entry under
ledger/loopx-overall-roadmap-v0/, plus the derived RFC status index, and
changes no runtime code.

No budget change is proposed or qualified — the measurements show both budgets
are sufficient. No owner-liveness change is proposed here: the lock in this
path is held by a live owner, so that check behaves correctly for it.

The intermittent Windows failure is now attributed to a child that did not
exit inside the ready budget on a loaded runner, and a bounded grace window
after that budget is the repair this entry identifies. The repair itself is
not made here: giving the child a grace window changes how long a failed
startup blocks its caller, and that needs its own acceptance and a caller
that tolerates the longer wait.

Two acceptance points follow from it. The caller now waits longer, so the
grace window has to be bounded by something the repository already owns
rather than by a new constant. And the user-visible guidance changes with
the code: loopx/cli_commands/start_goal.py gives runtime_startup_timeout
its own remediation text, while mutation_lock_timeout falls through to the
shared default, so a caller that receives the exact code would be told to
repair or reinstall LoopX where it was previously told that startup timed
out. That difference is the intended consequence of recovering the exact
code, and it has to be read as such rather than as a regression.

The delivered typed code is a diagnostic improvement, not a correctness repair:
the runtime still fails to publish its locator, and the caller still fails to
start. Windows CI is not qualified as passing by this measurement, and the
intermittent failure set is preserved rather than reclassified.

The measurement itself was a temporary diagnostic, removed after the numbers
were read; it is not a qualification artifact in this repository. The commit
that produced the numbers is reachable as codex/measurement-archive on the
fork, so the two figures can be re-derived rather than taken on trust: it
raises a deliberate AssertionError carrying the measurement, because the
Windows lane runs pytest without -s and would otherwise capture the output.

Validation

python examples/docs-governance-smoke.py                                  ok
python scripts/generate_rfc_status_index.py --check                       clean
scripts/check_rfc_bilingual.py --base origin/main --head HEAD             passed
mkdocs build --strict                                                       built, 0 errors
loopx canary premerge --from-git-diff --git-diff-base origin/main          0 failures, 0 warnings

Ledger entries are append-only history, so this is a new entry that states what
the 2026-10-04 checkpoint did not establish, rather than an edit to that entry.

@xiaoan17
xiaoan17 force-pushed the codex/record-windows-startup-delivery-window branch 11 times, most recently from 715a565 to 36d0033 Compare October 30, 2026 12:41

@songoow songoow 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 Astra Pro | OpenAI | self_reported

REQUEST_CHANGES — 1 项 P2:历史失败的排他性归因不成立。 本次审阅固定在 36d0033bad3efe49181d475c3075627ba851feee,对比主干基线 0ad08b15dd7331051c93566075561a390a9cd7e2,完整阅读了全部 4 个改动文件及相关启动实现。

动机 / Motivation

受影响的是排查 LoopX 启动失败的维护者和运行 Windows 自动化的操作者。

原先一次启动失败只留下通用超时,排查者不知道精确错误丢在何处;这份文档尝试记录诊断产生和送达的时序,减少后续重复调查。

文档确实整理了两个宿主的成功测量与锁语义,但尚未证明历史失败一定发生在子进程仍运行时。

本PR只增加双语调查记录及状态索引,不修改运行时预算、锁属主判定或生产启动行为,也不构成 Windows 原生资格认证。

改动思路 / Approach

把成功测量、类型化错误信封的产生、Python 的观察时机和后续修复建议放在同一份双语记录中,有实际维护价值。live-lock fixture 的属主就是当前存活进程,不能为了让测试快一些把它当陈旧锁回收;文档在这一点上保留了正确的边界。没有必要为本次调查额外引入运行时框架或扩大到完整 Windows 认证。

但“子进程写出错误”与“父进程在预算内观察到退出”是不同事件。基线 _start_runtime 在 ready 循环结束后调用 poll(),这个结果只决定是否 terminate();随后始终抛出 runtime_startup_timeout。因此最终 poll() 已经得到退出码 2,也可能返回同一个通用错误。成功样本的 5.813/5.140 秒只能说明那些样本按时送达,不能确定另一历史失败时 child 的实际状态。

具体改动 / Changes

关键内容讲解

两个 2026-10-30-s2-s12-windows-startup-delivery ledger 分别记录英文和中文调查;两个 STATUS 是相应的派生索引更新。没有生产、测试实现或配置改动。正文把测量方法、live owner 的锁语义、失败传播窗口和非目标放在一起,结构上是一个完整的调查切片。

采用的既有合同是 docs/architecture/rfcs/ledger/loopx-overall-roadmap-v0/2026-10-04-s2-s12-startup-diagnostic.md,固定修订 0ad08b15dd7331051c93566075561a390a9cd7e2。原文没有独立编号,以下只是本次映射标签:diagnostic-transport 要求保留有界、类型化启动诊断,缺信封时不伪造原因;qualification-boundary 明确该交付不认证 Windows 或全套 CI。这两项边界都应保留,不能用新 ledger 对历史时序的推断取代实际观测。

P2:请修改英文第 85–102 行及中文对应段落。 “该码只在 ready 截止时 child 仍运行才产生,所以历史 child 必定超过 15 秒未退出”与源代码不符;“截止时补读一定拿不到信封”也超出了测量所能支持的范围。最小修复是分别描述:child 当时确实尚未发布;或者 child 已发布/退出但 parent 尚未观察到。历史运行缺少区分这两者的记录,应保留为未确定,而不是认定高负载已经证明根因。

对主干的风险 / Risks

我通过真实 Python 子进程和真实 stderr 临时文件构造了截止边界,仅对父进程的 monotonic 调度使用合成时钟:ready deadline 为 1,child 在最后一次等待内发布合法 mutation_lock_timeout 信封并以 2 退出,parent 在 1.1 恢复。实际基线函数返回 runtime_startup_timeout,最终 child 已退出,生产 capture 读取次数为 0,启动锁正常清理。这是代码可达性的反例,不是对历史 Windows 运行的重演。

相同探针在 #7743 当前 grace 实现上返回精确码,因此这个 finding 不否认 grace 方案可以修复传播缺口;它要求调查记录不要把一种可能时序写成已证事实。更小且足够的改动是校正文案,不需要在这份纯文档 PR 里扩展预算、重写 PID liveness 或夹带实现。

验证:generate_rfc_status_index.py --check、对固定基线的 check_rfc_bilingual.py 和 docs-governance-smoke.py 均 PASS;原生产文件相对基线无变化。有界调度反例 PASS(成功否证唯一归因)。历史失败的实际 child/parent 时间线 UNVERIFIED,本轮没有原生 Windows 执行。

我的整体评价 / Verdict

文档作为调查资料有价值,当前主要问题是结论强于证据。长程执行本身未改变;维护者体验的改善仍受错误归因限制,修正后才是可信的排查参考。这里不要求删掉测量或先完成 #7743,只要求将可观察事实、推断和剩余未知写清楚。本次检查的 docs/architecture/rfcs/ledger/loopx-overall-roadmap-v0/2026-10-30-s2-s12-windows-startup-delivery.md 不改变已渲染的产品页面;无需用截图证明内部行为。 未进行产品页面视觉检查,因为本次没有页面改动。

English verdict: REQUEST_CHANGES — 36d0033bad3efe49181d475c3075627ba851feee has one P2 documentation finding: the historical timeout does not prove the child was still running at the deadline. Documentation checks passed; a real-child/synthetic-clock counterexample confirms the omitted branch. Historical Windows timing remains unverified.

@xiaoan17

Copy link
Copy Markdown
Contributor Author

Review response: the attribution is corrected

The finding is accepted, and the correction is on the branch (6ceeefcde).

What was wrong. The entry stated that runtime_startup_timeout is
produced only when the ready budget expires while the child is still running,
and inferred from that the recorded Windows failure was a child that had not
exited inside 15 s. The source does not support the exclusive form. The
baseline reads the published envelope only when a poll within the ready loop
reports an exit; after the loop it polls once more, but that poll only decides
whether to terminate the child, and the function then raises
runtime_startup_timeout without reading the capture again. A child that has
published and exited leaves the envelope in the capture, and the baseline
leaves it unread.

What changed (both the English entry and its Chinese mirror, and the PR
body):

  • The recorded failure is now attributed to a parent that did not observe the
    child's exit inside the ready loop. The entry states the two situations the
    historical run cannot separate — a child that had not published yet, and a
    child that had published and exited unobserved — and keeps the attribution
    undetermined. The heavily loaded runner is described as a plausible
    contributor to a late publication rather than a demonstrated cause.
  • "The envelope cannot be recovered by reading the capture at the moment the
    budget expires" is replaced by the narrower, supported statement: reading
    the capture at that moment recovers the envelope in the second situation
    only, because a child that has not exited has published nothing while a
    child that has exited leaves the envelope in the capture.
  • The section that identifies the repair now says that a bounded grace window
    covers both situations, so the repair does not depend on knowing which one
    the recorded failure was.
  • The "where the exact diagnostic is lost" section now describes the post-loop
    poll precisely: it decides whether to terminate, and the capture is not read
    again.

Unchanged. The entry still changes no runtime behaviour, still widens no
budget, still leaves the owner-liveness check alone, and still does not qualify
Windows CI as passing. No measurement is relabelled and no frozen criterion is
touched.

xiaoan17 added 2 commits October 31, 2026 00:59
The Windows lane fails intermittently on operations that start the Effect
runtime and wait for it to become ready. Measuring the startup cost on
the Windows runner shows node cold start at 0.406 s against a 15 s ready
budget, and the live-lock case returning the exact mutation_lock_timeout
code on that same run.

That measurement refutes the earlier conclusion that node cold start plus
the 5 s lock wait exceeds the ready budget on Windows. Neither budget
should be widened.

The failing run's own job log is retrievable and shows
runtime_startup_timeout, which is produced only when the ready budget
expires while the child still runs; that step ran 439 cases in 5 min 31 s
and the chunk holding the failure took two minutes against six seconds for
the chunk before it, so the runner was loaded and the child could not
finish its mutation-lock wait inside the budget.

Tracing the child shows the envelope appears only at exit, about 5.8 s in,
so reading the capture at the budget would find nothing. Waiting for the
exit instead yields the exact code every time, and the extra wait is
bounded: the child exits at 5.13-5.17 s whatever the ready budget was,
because what it waits for is the mutation-lock budget. That identifies a
bounded grace window as the repair, with the acceptance points it would
have to satisfy. The repair itself is not made here, and the measurement
that produced the figures stays reachable so they can be re-derived.

Signed-off-by: xiaoan17 <anbc1224@gmail.com>
The review is right that the entry's attribution was exclusive where the
source does not support it. The baseline reads the published envelope only
when a poll *within* the ready loop reports an exit; after the loop it polls
once more, but that poll only decides whether to terminate the child, and the
function then reports the generic timeout without reading the capture again.

So the recorded Windows failure does not distinguish a child that had not
published yet from a child that had published and exited with the parent not
observing it. State both possibilities, keep the attribution undetermined, and
describe the loaded runner as a plausible contributor rather than a
demonstrated cause. Reading the capture at the budget recovers the envelope in
the second situation only, and a repair with a bounded grace window covers
both, so it does not depend on knowing which one occurred.

Signed-off-by: xiaoan17 <anbc1224@gmail.com>
@xiaoan17
xiaoan17 force-pushed the codex/record-windows-startup-delivery-window branch from 6ceeefc to 7f4edaf Compare October 30, 2026 16:59

This branch has not been deployed

No deployments
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.

2 participants