Skip to content

fix(turn): supervise managed Host process lifetimes in TypeScript - #5144

Merged
huangruiteng merged 8 commits into
mainfrom
codex/managed-host-process-supervision
Sep 27, 2026
Merged

huangruiteng merged 8 commits into
mainfrom
codex/managed-host-process-supervision

Conversation

@huangruiteng

@huangruiteng huangruiteng commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Goal And Delivered Outcome

Related to #4574 (S2/S4/R5), the TS migration RFC and shared-authority execution closure. A real timeout reproduction showed the generic Host leader exiting while its child kept changing a file. Codex CLI also skipped cleanup after leader exit. A lease write or late-result rejection cannot stop these processes.

Both shipped subprocess entrypoints now use one TS supervisor for timeout, parent loss, output drain and descendant cleanup. Python retains command transport, Codex session/result interpretation and existing settlement callers. No new capability, provider API, persistent format or CLI flag is introduced.

中文:统一通用命令和 Codex CLI 的进程生命周期,修复“LoopX 已返回、子进程还在干活”。本次不宣称租约续约/撤权或默认 provider 切换已经完成。

Scope And Continuation

Validation

  • Tested revision: 152bb52
  • Run state: finished for reported local checks; GitHub CI and native Windows qualification pending.
  • Input classes: synthetic, public_fixture, authorized read-only local source (aggregate evidence only).
Check kind Result Evidence and limits
Real subprocess regression passed 9 TS tests: timeout/abort, leader exit with inherited or closed pipes, ignored TERM, missing executable, streaming budget, UTF-8 and consumer failure.
Python integration passed 148 tests across Host transport, generic/Codex execution, session recovery, three-turn CLI smoke, lane fencing, transaction and Host failure semantics. Final rebased head rerun.
Native TS breadth passed before bounded refinement/rebase 3,135 passed, 30 existing environment-gated skips, zero failures. Final changed process suite and typecheck rerun; this is not represented as an exact-head full-suite run.
Real-source entrypoint rehearsal passed Read-only promoted-Goal Turn plan passed both actual Host adapters in a disposable runtime, with a local controllable executable. Residual child stopped. No live task execution, model call, writeback or quota spend; not an active cutover or paid-model qualification.
Differential reproduction passed Same child-write workload continues after baseline timeout return and stops after managed execution returns.
Static passed Repository Ruff, configured mypy, TS typecheck, public-boundary scan and diff checks.
Canary / exact-scope quality passed 19 selected premerge checks passed; exact-scope receipt valid; zero blockers, one documented boundary warning and one startup-cost advisory.

Cost disclosure: twelve trivial local invocations measured median 25.73 ms for the former direct process and 132.05 ms through supervision. Accept the per-job startup cost for process isolation; this PR makes no latency-improvement claim. The earlier concurrent development run hit the existing 30-second CLI smoke deadline; it passed alone and in both final serial suites without raising that deadline.

Frontend / Visual Evidence

UI impact: none. Existing turn run-once consumers adopt an internal execution change; no setting or visible configuration was added. Attached frontend/App sessions do not use this process owner. Public docs describe the changed behavior and limits; no public first-screen change.

Shared-authority RFC fixture impact

Persisted production-scale fixture schema and provider conformance are unchanged. Added fixtures exercise actual OS process lifetime and existing Turn/Codex callers, rather than fabricating provider success. Three-arm promotion/routing rehearsal is not applicable: no authority selector, migration, projection or store implementation changes. The isolated real-source rehearsal is described above; D2 capacity/soak and D3 cutover stay open.

Boundary Checklist

  • No private state, raw logs, credentials, internal links or machine paths in the diff or published evidence.
  • No benchmark ownership, release publication or live Goal mutation.
  • Runtime/regression and protocol/RFC work are separated into signed runtime, documentation and transport-naming commits.
  • Maintainer review/merge required; this PR is not self-merged.

The committed-scope structural gate initially counted the Python adapter and TS supervisor as an additional same-basename twin. Naming the adapter host_process_transport.py makes its existing transport-only responsibility explicit; all callers moved, the 43-module budget and validator remain unchanged, and the gate passed on the corrected scope.

Exact-scope quality: 0a95134e90b9fa987e2d7b0d62f37a3dbd26802901599f429b0b771262c6631a; receipt cqr_0a95134e90b9fa987e2d verified valid for 15 changed files. Safe-fix allowed/applied: one bounded refinement pass. No merge authority is inferred from this receipt.

Maintainer repair at the merged head

The reviewed head 0d9de02958d9f5e95a927e5f9feedbc3413fc126 was reached by merging origin/main into the branch twice as main advanced, then refreshing the derived RFC status index.

  • Conflicts were documentation-only. Six RFC files (shared-goal-authority-state-provider-v0.md/.zh-CN.md and typescript-control-plane-migration-v0.md/.zh-CN.md) hit "both sides edited the same current-delivery-frontier section" against main's newer commits (7 commits, then 16 including refactor(state): retire legacy Todo events and isolate supervisor history #5054's legacy Todo-event retirement). Each kept main's newer plan text and folded this PR's host-supervision slice in as a separate delivered paragraph, with the English and Chinese versions mirrored. No runtime file conflicted.
  • docs/architecture/rfcs/STATUS*.md went stale when this PR's new ledger entry (and then main's new ledger entries) appeared; regenerated with scripts/generate_rfc_status_index.py --write and committed as its own signed change so docs-governance-smoke and test_rfc_status_index stay green.
  • Validation at the merged head: npm run typecheck:control-plane; npm run test:control-plane (3262 tests, 3232 passed, 30 environment-gated skips, 0 failures); 317 related Python tests across host process, Codex CLI, executor, driver and the three architecture suites; RFC status index (13 passed); targeted Ruff; git diff --check; loopx check over all 17 changed paths (public boundary scan clean); and loopx canary premerge --from-git-diff (10 catalog canaries, 8 risk smokes, public boundary and direct checks; 0 manual holds).
  • Environment note: invoking loopx from a worktree resolves the editable install to the primary checkout, so a default premerge runs the primary repo's scripts. The primary checkout carries an untracked loopx/capabilities/cross_runtime/__pycache__ directory that reads as a namespace package, which made bounded-context-namespace-smoke and semantic-vocabulary-drift-smoke fail fast; running the gate with the worktree's own code passes. This is local drift, not a main or PR defect.
  • Merged with admin bypass because the protect main ruleset requires require_last_push_approval after the maintainer main-integration pushes made the maintainer the last pusher. The exact-head review is published at fix(turn): supervise managed Host process lifetimes in TypeScript #5144 (review).

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…copes

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…sion

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

Exact-head self-review: 5144@152bb5207b632dfbfae11d99e6caba54588b5978
Approval conclusion: APPROVE. Author-owned PR; recorded as a COMMENTED review.

动机

这里的实际缺口不是“lease 有记录”,而是 managed Host 返回之后,后代进程仍在执行效果。旧 generic Host 的 timeout 只杀 leader;旧 Codex cleanup 在 leader 已退出时直接返回。我用同一个独立合成工作负载,在真实 merge base cd32b28567216c9621bb86061ab1ae1b97724491 和当前 head 调用生产入口,对照复现了 generic timeout、正常 leader 退出、Codex 有效结果三种后代持续写入的情况。

本 PR 的可验收结果是收拢这两条 subprocess 路径的进程生命周期,不是整个 R5、lease fencing 或多 Host 取消能力完成。授权、独立任务验证和 exactly-once settlement 仍由现有 Turn owner 承担。

改动思路

每个 Host job 的 timeout、输出预算、pipe drain 和进程组终止统一归 TypeScript supervisor。Python 只保留 IO 转运以及 Codex 的结果/会话解释,不再维护两份 termination/thread-reader 规则。复用了已有 Node discovery 和 wheel package-data;没有新 capability、provider、持久化状态或用户 CLI 开关。现有 persistent authority RPC transport 的生命周期不同,不应被当作 disposable Host job 复用。

具体改动

我审阅的是完整 15 文件、+851/-139:5 个生产文件、3 个真实进程/原生测试文件、7 个中英文协议与 RFC/ledger 文档,而不是只看新增 supervisor。两条现有入口自动采用内部实现,现有配置/请求 schema 和 UI 不变,因此无需另增 frontend/Lark 开关或首屏修改。

关键代码讲解

  • runHostProcess(host_process.ts:49)用专有 POSIX process group 执行 Host;即使 leader 已退出仍发 group TERM,并在 300ms 后升级 KILL。stdout streaming budget、UTF8 decoder、背压和有限 drain 都在这一个 owner 中。超时/取消/输出超限是 typed outcome,不是 prose guidance。
  • run_host_process(host_process_transport.py:51)转运 private NDJSON,请求端 control pipe 的存活代表 Python caller 仍存在,EOF 触发取消。callback failure 也走有界清理;它不读取或改写 Todo/lease authority。Windows relay 是启动适配,不构成第二份生命周期策略。
  • run_codex_cli_host(codex_cli.py:761)保留既有 typed result-file 和 opaque-session recovery;改为有界 LF framing 与有限 failure-category 集合。不完整诊断不制造成功或武断分类,但有效 typed result 仍可用,timeout 已观察到的 session 仍保留。
  • _run_host(executor.py:646)经同一 transport 收集有限输出,只有 complete、zero-exit 且合法 JSON 才进入原有 validator/settlement。已有 admission 在执行前,journal retry 与 settlement 不被新 supervisor 接管。

对主干的风险

默认行为确实改变:managed subprocess 在正常返回、失败或 caller loss 后清理其 owned group;不能再用这条路径故意遗留长期服务。协议明确披露了这个变化。missing executable/timeout 的 diagnostic reason 也变为明确的 process outcome,成功 JSON、UTF8、非零退出和非法 JSON 的对照观察则保留。

最强反例是 leader 返回有效结果但 child 继续产生效果,普通结果测试或 mock Popen 会漏掉它。独立回放在 base 的上述三种场景均观察到返回后文件继续变化,head 三种均停止,正常 generic/Codex 结果仍有效;没有凭 PID 是否存在来假定清理成功。原生 suite 另覆盖 ignored TERM、继承或关闭 pipes、abort、owner death、consumer failure、输出超限、session/retry 等路径。

验证已完成:Python head 144/base 136 通过,TS 真实进程测试 9/9;control-plane typecheck、ruff 和配置内 mypy 20 sources 通过;base/head 的真实 Turn fake-host walkthrough 通过。重新 build wheel、从独立一次性环境安装并核对 site-packages provenance后,相同生产入口回放与 source head 观察完全一致,打包的 supervisor/bridge 可用。第一次构建在 base/head 都因缺少生成的 Chat bundle 被同一既有 guard 拒绝;按正常 build:chat 流程补齐后通过,未放宽 guard 或预算。

语义与 CI 对齐

复用现有 Turn vocabulary 与 authority admission,新增的是同版本随包交付的 transient process outcome,不是新的 persisted actor/lease 协议;默认 cleanup 增量已在协议披露且有 base/head 反例,既有 journal/session/replay 语义保留。

远端 CI 未查询、未等待,不以无因果关联的红 CI request changes。这些证据不涵盖 native Windows、逃逸进程组、supervisor 被 SIGKILL、remote effects 或 lease 到期/回收后的取消;Windows 仅 best-effort,协议也这样声明。没有 PostgreSQL store 重构或新 authority 写入,所以这个本地 OS-process slice 不需要用活跃 Goal/数据库做实验。增加 Node 启动成本是取舍,未据此宣称延迟改善。

非阻断建议:[P2] 更新 current-frontier 的 PR 状态

docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-27-host-supervision.md:6 开始的新记录仍说 #5140 open,后面的当前 inventory/count 也继续计入;对应中文与 RFC current-frontier 应同步核对。发布前读取生命周期元数据:#5140 已在 2026-09-26 22:13:02 UTC 合并,#5142 已在 19:29:55 UTC 合并。

建议保留注明修订的历史 snapshot,但刷新“当前未合入 PR 数”和区分已合入 recovery 与尚未完成 integration qualification,避免推进计划误算。该 currentness 修正不否定已独立验证的进程清理,不作为 runtime approval 的阻断;#5141 等剩余边界也不能因本 PR 而视为完成。

我的整体评价

APPROVE:这个 slice 改善长期推进中的 orphan-effect 风险,并让现有 Host 用户获得有界返回和清理,不额外增加人工配置或执行权限。代码规模与两个活跃 production caller 相称,删除重复 Python lifecycle 是已应用的有界 future-facing refactor;继续扩大到 lease cancellation/attached Host 应按现有 RFC 具体验收,不在这里添加未使用的框架。

Public-boundary 检查通过;本 review 使用合成 fixture 和真实本地进程,不公开原始轨迹或私人状态。仍保留上面的明确未验证项与非阻断文档建议。该 PR 是 runtime/control-plane 修改,审查通过不代替 maintainer merge,也不等同 merge readiness;本次没有合并。

English verdict: APPROVE

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…ndex

The new ledger/shared-goal-authority-state-provider-v0/2026-09-27-host-supervision
entry raised the authoritative-state-provider ledger count from 20 to 21 without
regenerating the index, so docs-governance-smoke reported a stale RFC status
index. Regenerate STATUS.md and STATUS.zh-CN.md with scripts/generate_rfc_status_index.py.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Merging origin/main (0943b03, #5054 todo-event retirement) added ledger
entries, so regenerate the derived RFC status index in the same head.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

PR #5144 复审 — exact head 0d9de02958d9f5e95a927e5f9feedbc3413fc126

动机

真实复现暴露了两条同源缺陷:通用命令 Host 的 leader 超时返回之后,它启动的子进程仍在继续改文件;内置 Codex CLI 在 leader 退出后也跳过了后代清理。租约写、迟到结果拒绝都只能约束"结果能不能用",不能终止已经在跑的进程。本 PR 让两个已交付的子进程入口(通用命令 executor 与内置 Codex CLI)共用同一个 TypeScript 进程监督者,覆盖超时、父进程消失、输出 drain 与后代清理;Python 保留命令传输、Codex 会话/结果解释与既有结算调用方。不新增能力、provider API、持久化格式或 CLI 参数,方向与 TS 迁移 RFC"把生命周期与 effect 决策放到 TS、Python 只做适配"一致。

改动思路

进程所有权集中到 loopx/control_plane/turn_driver/host_process.ts:POSIX 下以独立进程组启动(detached),并且在 leader 退出之后仍然对整组发信号——TERM 后 300 毫秒升级为 KILL,正是"leader 已退出但子进程还在写"这条复现路径;drain_timeout_ms 让被后代继承的管道不会无限阻塞结果返回;stdout 在流式消费阶段就按既有 12,000 字节结果预算裁剪(output_limit),而不是先收完再判断;abort(控制管道 EOF、SIGTERM/SIGINT、stdout 断开)取消执行。Windows 保留 Python 的命令启动兼容(含批处理入口),先 taskkill /T 再退化为直接 kill,并且只声明 best-effort。

父子之间只走一条私有、非持久传输:Python host_process_transport.py 把 argv/cwd/input/timeout_ms/drain_timeout_ms/stdout_limit_bytes 以单行 JSON 写入 host_process_bridge.ts,bridge 用严格的 decodeHostProcessRequest 解码后执行,再把 stdout/stderr 分片与最终 result 以 NDJSON 回写;控制管道在整个作业期间保持打开,EOF 即表示所有者离开并触发取消。Python 侧只剩解释工作:executor._run_host 与 codex_cli.run_codex_cli_host 改为调用该 transport,codex_cli 删掉了自建 subprocess、两个 reader 线程与 _terminate_process,失败类别由 list 改为 set(选择仍由 (优先级, 名称) 决定,结果确定)。

具体改动

新增 host_process.ts(进程监督者:decodeHostProcessRequest、signalGroup、runHostProcess、HOST_PROCESS_TERMINATE_GRACE_MS = 300)、host_process_bridge.ts(私有 NDJSON 传输与 8 MiB 请求上限)与 host_process_transport.py(HostOutputLines 的 LF 分帧与 1,048,576 字符上限、run_host_process 的 Popen + 结果解析)。executor.py 删除 subprocess.run 路径,改为受管流式执行并区分 output_limit/非 exited/输出不完整/非零退出,保留 HOST_RESULT_MAX_BYTES 与 _run_host_runner 的进程内预算;codex_cli.py 删除 _terminate_process 与线程 reader,改用回调观察 LF 记录,并在结果/超时两条路径上都由 TS 完成组清理。测试侧新增 TS 9 项(UTF-8 跨界、stdin EOF、非法请求、可执行缺失、流式预算、abort、输出消费者失败)与 Python 6 项(HostOutputLines 边界、真实往返与预算、回调失败仍等待清理、所有者消失取消、超时停止真实后代、Windows relay 的 argv/stdin),并在 tests/test_loopx_turn_codex_cli.py 加入参数化的后代回收用例(假 Codex 启动忽略 SIGTERM 并持续写文件的子进程,断言结果返回与超时两种情形下都停止)。文档侧新增 loopx-turn-v0.md 的 Managed Host Process Lifetime 段,并在两份 RFC 的交付前沿记录本切片;合并 main 时解析了 4 个 RFC 文件同位置的"当前交付边界"冲突(保留 main 的更新版计划,并把本切片作为独立交付段补入),另用生成器刷新了因本 PR 新增 ledger 条目而失真的 RFC 状态索引。

对主干的风险

这是默认生命周期变更:Host 进程组不得再用于启动需要长期存活的服务,文档已明确写出;逃逸出进程组的后代、远程副作用、attached App 会话、进程内 DSH 执行以及用 SIGKILL 杀掉监督者本身都在边界之外,且不声称 Windows 与 POSIX 等价。失败语义有可见变化:通用 Host 的超时/取消/输出不全/预算超限现在映射为新的 reason 字符串(如 host process timeout),我核对了仓库内没有消费者依赖旧路径的 TimeoutExpired 字符串。Codex 侧的失败类别选择保持确定(按优先级与名称取最小),集合化不改变结果;codex_cli 的 typed result 契约、超时会话恢复与独立结算仍然保留,进程观察本身不认证 Todo 完成、不退款、不回滚外部效果。代价是作者披露的每次作业启动成本(12 次本地调用中位数 25.73 ms → 132.05 ms),并明确不声称延迟改善。另有一条环境事实值得记录:本机 loopx canary premerge 默认会用 editable 安装解析到主仓库并执行主仓库的脚本,而主仓库残留了未跟踪的 loopx/capabilities/cross_runtime/__pycache__ 目录(被当作命名空间包),会让 bounded-context 与 semantic-drift 两个 canary 秒失败;改用它本工作树的代码(PYTHONPATH=<worktree>)后 premerge 全绿。这不是本 PR 或 main 的缺陷。

我的整体评价

这是一个边界清楚、证据扎实的运行时修复:它对准的是真实复现的"LoopX 已返回、子进程还在干活",把重复的 Python 生命周期规则删掉并收敛成一个 TS 监督者,同时把不属于该边界的部分(租约续约、撤权、attached 会话、Windows 等价性)明确排除,没有借机扩大自动化或默认切换。我在该 head 上复验了 TS 定向 9/9、全量 control-plane TS 3262 项(3232 passed / 30 skipped / 0 failed)、相关 Python 317 项、架构与 RFC 索引检查、typecheck:control-plane、定向 Ruff、git diff --check、17 文件 public boundary 扫描,以及用本工作树代码运行的 premerge(10 canaries + 8 risk smokes + boundary 全绿,0 manual holds)。建议合入。

English verdict: APPROVE - 0d9de02 gives the generic command executor and the built-in Codex CLI one TypeScript process supervisor that signals the owned group even after leader exit, bounds output drain and streaming budgets, and cancels on owner EOF; typecheck, the 3262-test control-plane suite, 317 related Python tests, architecture/RFC-index checks, the 17-file boundary scan and a worktree-scoped premerge all pass, with the supervision-vs-authority and Windows boundaries disclosed as explicit limits.

@huangruiteng
huangruiteng merged commit 3b366de into main Sep 27, 2026
7 of 8 checks passed
@huangruiteng
huangruiteng deleted the codex/managed-host-process-supervision branch September 27, 2026 05:33
@huangruiteng

Copy link
Copy Markdown
Collaborator Author

Merged as 3b366de0287a990ea522887516cf9ffcc201bd73 (second parent = reviewed head 0d9de02958d9f5e95a927e5f9feedbc3413fc126).

  • Exact-head review: fix(turn): supervise managed Host process lifetimes in TypeScript #5144 (review) (approval conclusion; author-owned PR).
  • Conflict repair: the branch was merged with origin/main twice as main advanced (7 commits, then 16 including refactor(state): retire legacy Todo events and isolate supervisor history #5054's legacy Todo-event retirement). Six RFC-document conflicts were "both sides edited the same delivery-frontier section"; each kept main's newer plan text and folded this slice in as a separate delivered paragraph, with the English and Chinese versions mirrored. No runtime file conflicted.
  • Derived-index repair: this PR's new ledger entry (and then main's new ledger entries) made docs/architecture/rfcs/STATUS*.md stale; regenerated with scripts/generate_rfc_status_index.py and committed as its own change, so docs-governance-smoke and test_rfc_status_index are green.
  • Validation at the merged head: npm run typecheck:control-plane; npm run test:control-plane (3262 tests, 3232 passed, 30 environment-gated skips, 0 failures); 317 related Python tests across host process, Codex CLI, executor, driver and the three architecture suites; RFC status index (13 passed); targeted Ruff; git diff --check; loopx check over 17 changed paths (clean); and loopx canary premerge --from-git-diff (10 catalog canaries, 8 risk smokes, public boundary, direct checks; 0 manual holds).
  • Environment note for future locally-run gates: invoking loopx from a worktree resolves the editable install to the primary checkout, so a default premerge runs the primary repo's scripts. This primary checkout carries an untracked loopx/capabilities/cross_runtime/__pycache__ directory that reads as a namespace package, which made bounded-context-namespace-smoke and semantic-vocabulary-drift-smoke fail fast. Running the gate with the worktree's own code passes; this is local drift, not a main or PR defect.
  • Admin bypass was required by the protect main ruleset's require_last_push_approval after the maintainer main-integration pushes.

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.

1 participant