Skip to content

fix(turn): prepare managed Todo claims before builtin Codex delivery - #8648

Closed
gcl-coder wants to merge 8 commits into
loopx-project:mainfrom
gcl-coder:fix/turn-run-once-claim-preparation
Closed

gcl-coder wants to merge 8 commits into
loopx-project:mainfrom
gcl-coder:fix/turn-run-once-claim-preparation

Conversation

@gcl-coder

@gcl-coder gcl-coder commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem / 问题

Codex CLI turn run-once --execute needed a manual todo claim before the built-in host could run a hard-lease Todo. Adding an automatic claim ahead of the host exposed a series of ordering problems: the claim could run before the Turn receipt identity was checked, the shared launcher decoded host diagnostics strictly, a conflicting Turn bound between the identity check and the claim could still lease another Todo, and an owner-cadence interval_wait, invalid validator input or manual bypass was decided only after the receipt and claim had already written state.

Fix / 修复

  • builtin_execution_preparation_claim normalizes the Turn identity once, validates the Todo/Agent/Goal binding, and reserves the exact Turn receipt binding through ensure_turn_heartbeat_settlement_receipt before it runs the ordinary claim. A conflicting or padded identity is refused before any lease exists.
  • The shared launcher decodes only the receipt-bearing stdout strictly; stderr is diagnostic text and tolerates undecodable bytes.
  • Transport validation (validate_turn_host_inputs, including --validation-command-json) runs before the preparation boundary.
  • The owner cadence is now admitted atomically before the Turn receipt and the ordinary claim: managed_cadence_start reserves or resumes the interval slot inside builtin_execution_preparation_claim, the accepted reservation is carried into run_loopx_turn_once as admission, and the driver reuses it instead of reserving a second start. A refused admission returns interval_wait with no lease, receipt, journal or host attempt.
  • --manual-interval-bypass-reason reaches that same atomic admission, so an explicit manual start still records explicit_manual_interval_bypass and runs while an ordinary automatic start waits.
  • A reservation whose receipt writer is attached after the early admission delivers that writer once the Turn adopts it, so a run without an explicit --turn-instance-id still records the matching heartbeat receipt.

Validation / 验证

  • tests/test_loopx_turn_driver.py: 203 passed, including real File and SQLite fixtures for automatic claim, interval_wait with zero lease/receipt/host side effects, explicit manual bypass committing a second host start, malformed and wrong-shape validator JSON refused before the claim, the competing-guard race, and every managed-start interruption checkpoint.
  • tests/test_loopx_turn_executor.py and tests/control_plane/test_turn_host_decision_capture.py: pass with the driver change.
  • Ruff on the changed Python files and git diff --check: passed.

Not covered locally: Windows, an installed Host build, and managed CI.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
The builtin preparation adapter rebuilt the advertised claim from
whitespace-trimmed identities and accepted a decision Agent that only
matched the projected preparation. Rebuild the same argv from exact
decision strings, require the caller, decision, preparation, selected
Todo and identity contract to agree, and refuse anything else back to
its typed wait. Cover the PowerShell launcher route, the sqlite
provider, and the e2e claim-before-host path.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
The builtin preparation adapter rebuilt the advertised claim from
whitespace-trimmed identities and accepted a decision Agent that only
matched the projected preparation. Rebuild the same argv from exact
decision strings, require the caller, decision, preparation, selected
Todo and identity contract to agree, and refuse anything else back to
its typed wait. Cover the PowerShell launcher route, the sqlite
provider, and the e2e claim-before-host path.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>

@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 · max

本次评审对应提交 d39a090226a8f63d9b9c4a21d0bd73f36d8f60ab。

[P2] 轮次身份检查晚于认领。 当原 Turn 绑定任务 A 时,针对 B 的请求会被拒绝,但 B 已有活跃租约。

动机 / Motivation

通过内置 Codex CLI 跑硬租约任务、复用原 Turn 身份并依赖权威租约的调用者。主干缺少内置 Codex 认领路径。需要租约的任务会在宿主前被拒绝,调用者需要额外执行 todo claim。同一已绑定任务的真实 CLI 在 File 和 SQLite 上都从 claim_required 进入 committed,假 Codex 宿主只调用一次。使用隔离 File 和 SQLite、真实 CLI 和合成 Codex 宿主。线上模型和 Windows 安装入口本次未验证。原 Turn 回执绑定任务 A 时,以同一 Turn ID 运行任务 B。命令因身份不符退出 1,但两种 provider 都把 B 从没有租约改为 active version 1,宿主调用为零。

改动思路 / Approach

仍由已有 claim owner 处理竞争和权限。应在入口完成已有身份检查后再进入这条路径。保持改动在内置 CLI 适配器,修正准备和原回执验证顺序。 现有准备结果和原子认领可复用。让同一 Turn 的回执检查参与准备,随后再重新读原任务的 ready 状态。

具体改动 / Changes

关键代码讲解

基线 3a5899f00cab97294c20459ac8508d16dc317492 与提交 d39a090226a8f63d9b9c4a21d0bd73f36d8f60ab 的完整 diff 共四条路径。新增的 builtin_execution_preparation_claim 检查普通 argv 和当前任务,handle_turn_command 先调用它,再构造计划并检查原回执。真实 File/SQLite 的同任务分支各运行一次宿主并提交,修复了原问题。第二个对照先用真实 guard 把 Turn 绑定 A,再以相同 Turn ID 指定 B。实际命令以回执不符拒绝,B 却已取得 active version 1 租约。精确 merge base 没有这次写入。当前提交和与 main 的集成树都复现。共享 launcher 的最新改动也会严格解码 stderr,合成子进程的 stdout 回执正常时仍可触发解码失败。

规格 docs/reference/protocols/loopx-turn-v0.md。核对 ordinary_managed_claim、claim_owner_and_scope、receipt_binding_before_claim。 原问题

对主干的风险 / Risks

错误重试虽然停止了 Host,后续合法执行仍会遇到意外租约。把已有身份校验移到认领前,再使用原有原子入口,可同时覆盖原问题和拒绝的副作用。

语义与 CI 对齐

上一提交的 273 项 Python 通过。当前提交重跑准备和 quota 聚焦测试、本地预合并及 File/SQLite 实际 CLI。记录了合成宿主调用、租约和独立 launcher 诊断流对照。

我的整体评价 / Verdict

请求修改。保留已验证的同任务认领,先核原 Turn 与 Todo 的绑定,再执行准备。回归应检查拒绝后另一任务依旧没有租约。本次评审覆盖现有宿主与 CLI 行为。使用隔离 File 和 SQLite、真实 CLI 和合成 Codex 宿主。线上模型和 Windows 安装入口本次未验证。

English verdict: REQUEST_CHANGES. Request changes. The happy path claims and runs once, but a receipt-bound Turn targeting a different Todo creates that Todo's lease before rejecting the mismatch.

if native_execution_preparer is not None and args.turn_command == "run-once" and args.execute:
prepared_todo = native_execution_preparer(decision)
if execution_preparer is not None and args.turn_command == "run-once" and args.execute:
prepared_todo = execution_preparer(decision)

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.

用真实 quota should-run 把一个 Turn ID 绑定到任务 A,随后用同一 ID 运行任务 B。两种 provider 上,B 起初没有租约。命令报 Turn heartbeat receipt belongs to another settlement identity,退出 1,宿主调用为零,但 B 已有 active version 1 租约。PR 的 merge base 上没有这个写入,与当前 main 集成的树也复现。请在执行这里的认领前复用现有回执身份检查,并覆盖这一拒绝路径的租约读回。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已修复。认领前先由回执所有者校验并保留 Turn 身份绑定,冲突的 Turn 在普通认领之前被拒绝;本轮又把 cadence 准入提前到同一准备边界,拒绝路径不写租约、回执或 journal。

return subprocess.run(
list(command), cwd=str(cwd), capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=timeout,
encoding="utf-8", errors="strict", timeout=timeout,

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.

共享 launcher 把 stdout 与 stderr 同时按严格 UTF-8 解码。隔离子进程退出 0,stdout 返回有效认领回执,只有 stderr 含不可解码字节。前一提交能读到回执,本提交触发 UnicodeDecodeError。既有 native execution preparation 也使用该 runner。建议严格解码用于回执的 stdout,将 stderr 作为可容错诊断。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在后续提交修复。_default_runner 只对承载回执的 stdout 严格解码;stderr 作为诊断文本容错解码,因此诊断中的不可解码字节不再让有效回执失败。

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>

@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

复审提交:55164368afe46372b7ca2c6d9dd2b502ab489bde。
[P2] 并发写入同一 Turn 绑定后,冲突的待办仍会先拿到租约。

动机 / Motivation

用内置 Codex CLI 执行硬租约待办,或以同一个 Turn ID 重入的调用者。 本提交修复了单次执行和预先已有的冲突;如果另一请求在身份检查与子进程认领之间写入不同绑定,冲突依然会留下租约。 用真实 File 和 SQLite CLI 挂起 B 的认领子进程;此时 guard 把原 Turn 绑定到 A。放行后 B 的命令报身份冲突退出 1,B 却从无租约变为 active version 1;Host 未调用,扣额为零。 本次独立验证使用真实源码 CLI 与 canonical File/SQLite,Codex 为合成进程;未运行 Windows 原生、已安装 Host 或远端 CI。

B 的普通 claim 仍会提交租约;随后的重读正确拒绝冲突 Turn,已写入的 B 租约没有撤销。两种存储均复现,无 Host 和扣额。 依据 docs/reference/protocols/loopx-turn-v0.md(708c68e7040559b2a4368002439570b90bc080b2)。 原问题 #8587。 核验项:ordinary_managed_claim, claim_owner_and_scope, receipt_binding_before_claim。

改动思路 / Approach

复用现有 typed preparation 和普通认领 owner;读取本身不授予执行权,且必须在写入前处理绑定竞争。 仅内置 Codex run-once execute 新增自动认领。原 dry-run、无可认领动作、其他宿主和 app-server 走原入口。 读同任务的 preparation → 执行原子 ordinary claim → 复读同 Todo ready → 调用一次 Host。 多加一次独立读取仍不能消除交错,应在既有身份和认领 owner 保持顺序。

具体改动 / Changes

File/SQLite 都验证同任务从无租约到 ready 再调用一次宿主;在进入预检查前已有 A 回执时,两种存储均拒绝 B 且不创建其租约。 精确源码的认领与 quota 测试合计 92 项通过;真实交错对照另外执行。

对主干的风险 / Risks

新代码检查了已有回执,但普通 claim 不知道这次竞争的 Turn;不能用后面的身份错误抵消已经发生的写入。已有正确绑定场景通过,说明补丁的范围无需扩到其他 Host。 本次属于现有入口修复,分阶段 RFC 经验不改变验收范围。精确源码的 Ruff、签署、diff、双语门和主干合入模拟均检查;不以 CI 结论代替以上对照。

我的整体评价 / Verdict

请求修改。先完成这条现有入口的独立反例,再复审。 在产生认领副作用前,使用既有 Turn 身份 owner 原子保留或校验该 Turn 的绑定,让不同 Todo 的绑定竞争先被拒绝。随后仍用原普通 claim;补真实 File/SQLite 并发用例,验证败方没有租约。 未改变渲染界面;该行为通过源码命令和调用方回读。

English verdict: REQUEST_CHANGES. Head 5516436. The pre-existing binding case is fixed, but a real interleaving still leaves a lease on the losing Todo. Bind the Turn before the claim or fence the claim in the existing authority transaction.

)
try:
_run_json(
expected_claim,

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.

[P2] 用真实 File 和 SQLite CLI 挂起 B 的认领子进程;此时 guard 把原 Turn 绑定到 A。放行后 B 的命令报身份冲突退出 1,B 却从无租约变为 active version 1;Host 未调用,扣额为零。 在产生认领副作用前,使用既有 Turn 身份 owner 原子保留或校验该 Turn 的绑定,让不同 Todo 的绑定竞争先被拒绝。随后仍用原普通 claim;补真实 File/SQLite 并发用例,验证败方没有租约。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 55164368a 之后的提交中收敛:认领前先用回执所有者原子保留 Turn 绑定,normalize_turn_instance_id 先统一校验 ID,冲突时在普通认领之前被拒绝。当前 51e72fcf4a 保留该顺序,并把 cadence 准入也提前到同一准备边界内,因此竞争场景不会先产生租约。

Bind the Turn receipt before the ordinary claim so a competing guard cannot acquire a lease for a different Todo. Cover the check-to-claim interleaving on File and SQLite providers.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>

@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

复审提交:474307ba234ac52eafc6c992dbfce43e28825f04。

[P2] 先统一校验 Turn ID,再领取待办。 同一 Turn 已绑定 A 时,若以带前后空格的原 ID 启动 B,CLI 仍以身份冲突退出 1,却把 B 的租约从无写成 active version 1。

动机 / Motivation

通过内置 Codex CLI 执行硬租约待办,或用同一 Turn ID 恢复工作的调用者。 主干对缺租约的待办会在启动 Codex 前拒绝,调用者需手工领取。 这个补丁让内置 Codex CLI 执行既有普通领取,重新确认同一待办 ready,再启动一次宿主。 当前提交修复了无空白 ID 的既有冲突和并发交错,File 与 SQLite 的成功路径均通过。 同一 Turn 已绑定 A 时,若以带前后空格的原 ID 启动 B,CLI 仍以身份冲突退出 1,却把 B 的租约从无写成 active version 1。 失败调用留下的租约会改变下一次准入,并可能让后续合法工作等到租约到期。 本次验证真实源码 CLI、隔离的 File/SQLite 存储和合成 Codex 宿主;未验证原生 Windows、安装版或托管 CI。

改动思路 / Approach

当下应保留既有 TypeScript 准入、原子 Todo claim 和持久 Turn 回执;只需让准备与计划使用同一个 Turn ID 校验结果。 保持在既有内置 Codex CLI 准备、原回执与普通领取的交界。合法输入先由现有回执 owner 在锁内保留原绑定,随后普通 claim 检查权限与租约,重新读到同一 Todo ready 才启动宿主。上一轮指出的竞争窗口,在完全相同的 ID 上已由这次预留修复。独立重读一次仍会有交错窗口;本次剩下的失败不需要另建租约规则或扩大到别的宿主。

具体改动 / Changes

以 docs/reference/protocols/loopx-turn-v0.md(167b0a1f3a3307c284b021ea9a09c7c0b5e5a11f)的 Decide → Route → Prepare 顺序及 #8587 为准。ordinary_managed_claim 和 claim_owner_and_scope 已覆盖;receipt_binding_before_claim 在带空白的原 ID 和显式非法 ID 上未满足。5 个文件 +758/-15。两处生产文件负责准备入口、身份和共享启动器;三处测试覆盖 JSON 通道、拒绝路径和真实 provider 交错。

关键代码讲解

  • handle_turn_command 在内置 Codex 执行路径装配准备器,成功后重读同一待办;预览和其他宿主未接入自动领取。
  • builtin_execution_preparation_claim 核对准备状态、Agent、Todo 及允许的 argv,再用原回执 owner 预留 Turn。此处把两端有空白的显式 ID 当作缺失,跳过预留,但随后仍执行普通领取。
  • build_loopx_turn_transaction_plan 使用 normalize_turn_instance_id 去掉两端空白。它会把同一输入认成 A 的原 Turn,再正确拒绝 B;租约此前已写入。内部有空格的非法 ID 则在这一阶段报格式错误。
  • _default_runner 单独严格解码 stdout,stderr 作为诊断容错,修复此前有效回执被坏诊断字节掩盖的问题。真实 CLI 交错回归 和准备器用例复验通过。

对主干的风险 / Risks

既有 Turn 回执绑定 A;执行 B 时显式传入带前后空格的同一 --turn-instance-id,或传入内部带空格的非法 ID。 File 和 SQLite 上,带空格的冲突请求在主干退出且无租约;此 head 与主干组合均出现 B active/version 1,仍零 Host、零扣额。非法 ID 在 File 上同样先写租约再报格式错误。 使用完全相同、不带空格的 ID 时,候选会拒绝 B,B 仍无租约;这一对照把问题定位到准备与计划的身份校验不一致。退出码和宿主调用数单独看都无法发现已持久化的租约。在任何领取或回执写入前使用既有 normalize_turn_instance_id 校验一次,把同一结果交给回执读取、原子预留和计划;明确区分省略 ID 与显式非法 ID。

语义与 CI 对齐

保留现有 readiness 与原子领取;不要用一次失败回包抵消已经写入的租约。修复后,用真实 quota should-run 把 Turn 绑定 A,再用同一 ID 的带空格形式或非法 ID 执行 B,前后读 task-lease inspect,并核对 Host 和扣额计数。 再运行 uv run --extra test pytest -q tests/test_loopx_turn_driver.py -k 'run_once and (claim or reserv)',保留合法成功、原样冲突和并发竞争三条控制路径。精确 head 聚焦测试 96 项通过;当前主干组合的领取测试 9 项通过,quick premerge 通过。两种 provider 的新反例仍失败,未查询托管 CI。

我的整体评价 / Verdict

请求修改。自动领取解决了 #8587 的手工步骤,且已有竞争修复可保留。当前回归会让一次失败的恢复调用留下占用状态,后续合法运行仍可能受影响;请让身份在领取前按同一规则确定,再以 File 和 SQLite 读回拒绝时无租约变化。本次验证真实源码 CLI、隔离的 File/SQLite 存储和合成 Codex 宿主;未验证原生 Windows、安装版或托管 CI。

English verdict: REQUEST_CHANGES. The exact-ID race is repaired, but a padded or invalid Turn ID still causes a durable lease before refusal. Validate the same Turn identity before claim and confirm no lease mutation on File and SQLite.

):
return None
heartbeat = decision.get("heartbeat_receipt")
exact_turn_instance_id = _exact_identity(turn_instance_id)

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.

[P2] 先统一校验 Turn ID,再领取待办。 同一 Turn 已绑定 A 时,若以带前后空格的原 ID 启动 B,CLI 仍以身份冲突退出 1,却把 B 的租约从无写成 active version 1。 File 与 SQLite 都复现;主干及无空白 ID 对照都未创建 B 的租约,宿主调用和扣额均为零。在任何领取或回执写入前使用既有 normalize_turn_instance_id 校验一次,把同一结果交给回执读取、原子预留和计划;明确区分省略 ID 与显式非法 ID。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 d4348db14d51952a74247e3003768a2f969c7216 修复。

builtin_execution_preparation_claim 现在在任何回执读取或普通领取之前先用 normalize_turn_instance_id 解析一次 Turn 身份,并把同一结果用于回执预定、原子领取和后续计划(计划本身也按相同规则规范化)。带前后空格的原 ID 会被还原为原 Turn,先与已绑定的 Todo A 冲突并在领取前拒绝;内部含空格或其他非法字符的 ID在领取前直接以 turn_instance_id must be 1-128 public-safe letters, numbers, or ._:- 报错。两者都不会再先创建 B 的租约。合法 ID、原样冲突和竞争守卫三条控制路径保持不变。

新增回归在 File 与 SQLite 上分别覆盖 padded-valid 和 explicit-invalid 两组:先用 quota should-run 把 Turn 绑定到 A,再以相同 Turn 的带空格形式或非法形式启动 B;两次都退出 1、假 Codex 未启动、task-lease inspect 显示 B active=false/lease=null。tests/test_loopx_turn_driver.py 全量 195 项通过,Ruff 与 git diff --check 通过。本次仍未验证 Windows 与安装版本,托管 CI 尚未观察到全绿。

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>

@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 · max

本次评审对应提交 d4348db14d51952a74247e3003768a2f969c7216。

[P2] 先通过调度与参数检查,再写领取和 Turn 回执。 调度器返回 interval_wait 或 CLI 拒绝无效 validator JSON 时,仍先把待办从无租约改为 active,并追加其 quota_should_run 回执;没有启动 Host、写 journal 或扣额。

动机 / Motivation

通过内置 Codex CLI 推进硬租约任务、或配置了最小调度间隔的操作者。当前主干遇到缺租约的后续待办会要求先手工领取;同样的等待或无效参数请求不写该待办的租约或 Turn 回执。新版允许真实 File/SQLite 硬租约任务按现有授权自动领取、同 Todo ready 后启动一次。上一轮指出的带空格和非法 Turn ID 均已在领取前拒绝。本次验证真实源码 CLI、隔离 File/SQLite 和合成 Codex;Windows、安装版和托管 CI 未测。没有 UI 改动。调度器返回 interval_wait 或 CLI 拒绝无效 validator JSON 时,仍先把待办从无租约改为 active,并追加其 quota_should_run 回执;没有启动 Host、写 journal 或扣额。

改动思路 / Approach

保留现有普通领取和原回执锁,避免恢复到有竞争窗口的只读预检。复用已有 validator 参数和 cadence 准入,把准备写入排到它们允许之后。当前改动限于内置 Codex CLI 的自动准备与共享启动器,不改变原生 app-server 或其它宿主的业务范围。 对照 docs/reference/protocols/loopx-turn-v0.md(3a5899f00cab97294c20459ac8508d16dc317492)的 ordinary-managed-claim、original-turn-binding 和 route-before-prepare。前两项有独立正反例,第三项在等待和参数拒绝时仍不成立。#8587 的自动领取目标可保留。

具体改动 / Changes

关键代码讲解

在 d4348db,loopx/cli_commands/turn.py 的调用先运行内置准备器,loopx/control_plane/turn_driver/codex_execution_preparation.py核对现有 argv 与规范化 Turn ID,再预留回执和普通领取。之后loopx/cli_commands/turn_host_request.py才解析 validator 参数,loopx/cli_commands/turn_cadence.py才走最小间隔准入。启动器现在严格解码 stdout,stderr 只作诊断。上一个 head 474307b 的 ID 问题已修,真实 File 和 SQLite 均读回零租约变化;这里是独立的准入顺序问题。

对主干的风险 / Risks

调度器返回 interval_wait 或 CLI 拒绝无效 validator JSON 时,仍先把待办从无租约改为 active,并追加其 quota_should_run 回执;没有启动 Host、写 journal 或扣额。 File、SQLite 各自同输入比较:主干 167b0a1 不写第二个待办;最终 head 与主干组合都写出 active 租约和回执,journal、Host 与扣额计数不增加。若操作者看到 state_written=false 后只重试,可能忽略这个已发生的占用。

语义与 CI 对齐

先在测试 Goal 配置间隔、合法跑完第一个 Turn,再在未到期时启动缺租约的第二个 Todo。另用非法 JSON 触发拒绝,读回 lease 与 rollout。原 ID 反例已通过;146 项聚焦测试与 quick premerge 通过;现行真实反例仍失败。没有查询托管 CI。

我的整体评价 / Verdict

请求修改。把无副作用的参数校验与既有调度准入移到普通 claim 和 Turn 预留之前;调度等待或输入拒绝不得改写租约和回执。通过后仍先原子保留准确绑定再 claim,并读回同一 Todo ready。 在 File 和 SQLite 重跑两种拒绝路径与合法成功、竞争、原 ID 负例。本次评审覆盖现有宿主与 CLI 行为。本次验证真实源码 CLI、隔离 File/SQLite 和合成 Codex;Windows、安装版和托管 CI 未测。没有 UI 改动。

English verdict: REQUEST_CHANGES. The original Turn-ID regression is fixed, but cadence waits and invalid validator input still persist a lease and Turn receipt before refusal. Move existing preflight and cadence admission before preparation writes.

if native_execution_preparer is not None and args.turn_command == "run-once" and args.execute:
prepared_todo = native_execution_preparer(decision)
if execution_preparer is not None and args.turn_command == "run-once" and args.execute:
prepared_todo = execution_preparer(decision)

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.

[P2] 先通过调度与参数检查,再写准备状态。 File 和 SQLite 上,配置的最小间隔未到时命令返回 interval_wait,但这个调用已为第二个 Todo 写 active 租约和 Turn 回执。非法 --validation-command-json 同样先写再拒绝;两例都无 Host 或扣额,且回包称 state_written=false。请把现有输入校验和 cadence 准入排在 Turn 预留和普通 claim 之前,保留原子绑定以及同 Todo ready 复读。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 a1842895bbfde1523b8bdafb658ba3086ed3beed 修复。

自动准备前增加了两步无写入的前置检查:

  • validate_turn_host_inputs 在领取前校验 --validation-command-json 与 host/adapter 参数,非法 JSON 或类型错误直接退出,不再先写租约或回执。
  • managed_cadence_wait_projection 以 quota.automation_cadence.manage 的 只读 read 获取当前 owner 间隔;处于 waiting 时跳过回执预定和普通领取,由 execute_turn_run_once 用正常执行投影返回 interval_wait,effects.state_written=false,不写日志、不启动 Host。

通过检查后仍保留原有顺序:原子预定原 Turn 绑定 → 普通领取 → 复读同一 Todo 准备好。合法成功、原样冲突、padded/invalid Turn ID 和竞争守卫路径均未改变。

新增真实 File/SQLite 回归:

  • 配置 3 分钟间隔并完成第一个回合后,对第二个缺租约 Todo 执行 run-once:返回 interval_wait(退出码 1),假 Codex 调用数不变,rollout 日志字节完全不变,task-lease inspect 显示该 Todo active=false/lease=null。
  • --validation-command-json 为 not-json 或错误形状时,领取前退出 1,rollout 日志不变,Todo 无租约,假 Codex 未启动。

tests/test_loopx_turn_driver.py 全量 201 项通过,Ruff 与 git diff --check 通过。Windows、安装版本与托管 CI 本次仍未验证。

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>

@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

复审提交 a1842895bbfde1523b8bdafb658ba3086ed3beed。

[P2] 并发准入落败后仍写租约和轮次回执。 两种存储都在只读检查之后写入另一启动的原生 cadence 预留。落败命令 exit 1、interval_wait、state_written=false,仍从无租约写成 active,并新增该 Turn 的 quota_should_run 回执。没有 Host、journal 或扣额。 在轮次回执或领取之前,从现有 cadence owner 原子取得该次启动的准入,并把同一预留带入后续 driver。落败请求保留零副作用。 先读 cadence 的调用

[P2] 自动间隔拦截已有权限的人工启动。 相同的有效手工确认请求,主干在两种硬租约存储均获得 explicit_manual_interval_bypass、调用一次宿主并提交。当前头返回 interval_wait,宿主调用数保持原值。 保留现有手工意图与重放判断,把手工原因交给同一个 cadence 准入;有有效普通租约的显式启动仍可执行。 提前返回等待的分支

动机 / Motivation

通过内置 Codex CLI 推进硬租约待办的操作者,会在连续执行或显式确认任务时遇到这个问题。 旧版遇到待办还没领取,会在启动前要求额外调用。当前提交可自动领取,并在顺序执行中先拒绝未到期的自动启动和非法校验参数。 两种存储都复现了剩余问题。竞争启动先占到调度间隔后,落败的命令返回等待,但已经领取待办并追加轮次回执。已有租约的显式人工启动也被提前拦截。 验证使用真实源码 CLI、隔离 File 与 SQLite、可控的 Codex 进程。原生 Windows、安装版与托管 CI 未测。

改动思路 / Approach

范围限现有内置 Codex run-once 启动与领取、原 Turn 回执,以及共享启动器的协议解码。 由现有原子 cadence 准入先判定本次启动,再进入 Turn 预留和普通领取。 保留已通过反例验证的规范化 Turn ID、同一待办回执和普通领取。现有原子准入可以一次判断自动等待、人工原因和原请求重入。当前只读等待分支缺少后两类输入,真实原子准入的位置也留在领取之后。相邻 #8669 已公开提出让现有 cadence owner 在领取前接收准入。其完整行为需要另按精确提交独立验证。 相邻提案

具体改动 / Changes

关键代码讲解

  • ordinary-managed-claim。内置执行只接受准确的 typed claim,成功后复读同一待办。缺租约正例在 File 和 SQLite 都走过正常宿主与提交。普通领取与身份核对
  • receipt-binding-before-claim。准备器统一规范化 Turn ID,并先预留它的准确绑定。原非法校验参数、顺序 interval_wait、显式 Turn ID 格式和竞争绑定的负例已通过。 共享启动器保持 stdout 严格 UTF-8,stderr 可容忍无效诊断字节。原轮次身份 启动器解码
  • route-before-prepare。参数校验已前移。等待路径仍把只读结果当准入凭证。真正的 cadence 预留在 turn_run_once,晚于待办和回执写入。参数校验 前置读取 真实准入调用
  • manual-interval-admission。已有手工原因只传给后段准入。早期分支只看自动间隔,无法到达该调用。真实已有租约的候选待办仍被拦截。等待判断 原手工参数

依据 docs/reference/protocols/loopx-turn-v0.md,版本 167b0a1,沿 Decide、Route、Prepare 顺序核对。原问题要求减少每次额外领取,并保留原本的 ownership 与租约校验。Turn 生命周期 原问题 #8587

对主干的风险 / Risks

竞争时,调用者看到 state_written=false,但后续其他执行已能观察到租约,现有 Turn 回执也被追加。原本可以直接执行的人工确认则在候选里变成等待。精确源码 89 项聚焦测试、主干试合并 16 项原负例和成功用例通过。Ruff、DCO、diff、双语门与试合并通过。 独立对照只在真正的读取和原子预留之间安排另一启动,领取、回执、journal 和两种存储均走生产源码。请用这条交错与有效人工启动一起回归,并保留已通过的顺序拒绝、普通成功、身份冲突及同一轮次恢复用例。

我的整体评价 / Verdict

请求修改。普通自动领取和上一轮的顺序拒绝已覆盖。真实准入需要成为领取前的单一判断,等待返回也必须与租约和轮次回执一致。在轮次回执或领取之前,从现有 cadence owner 原子取得该次启动的准入,并把同一预留带入后续 driver。落败请求保留零副作用。 保留现有手工意图与重放判断,把手工原因交给同一个 cadence 准入;有有效普通租约的显式启动仍可执行。 验证使用真实源码 CLI、隔离 File 与 SQLite、可控的 Codex 进程。原生 Windows、安装版与托管 CI 未测。

English verdict: REQUEST_CHANGES. In both canonical File and SQLite, a competing start can win after the read-only check and leave the losing call with a lease and a Turn receipt despite an interval-wait response. The early check also blocks existing explicit manual starts. Reuse the atomic cadence admission before preparation and preserve manual intent.

Comment thread loopx/cli_commands/turn.py Outdated
# Transport and interval refusals land before the Turn receipt
# reservation and the ordinary claim can write a lease.
validate_turn_host_inputs(args)
preclaim_cadence_wait = managed_cadence_wait_projection(

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.

[P2] 等待前仍会写入领取。 两种存储都在只读检查之后写入另一启动的原生 cadence 预留。落败命令 exit 1、interval_wait、state_written=false,仍从无租约写成 active,并新增该 Turn 的 quota_should_run 回执。没有 Host、journal 或扣额。 在轮次回执或领取之前,从现有 cadence owner 原子取得该次启动的准入,并把同一预留带入后续 driver。落败请求保留零副作用。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 51e72fcf4a 修复。所有权 cadence 的准入现在发生在 Turn 回执与普通认领之前:managed_cadence_start 在 builtin_execution_preparation_claim 内先原子 admit(或 resume)该次启动,被拒绝时直接返回 interval_wait,不再写租约、回执或 journal;已接受的预留作为 admission 传入 run_loopx_turn_once,driver 复用它而不是再次预留。interval_wait 的 真实 File/SQLite 用例断言 host_invoked=false、rollout 日志不变、task-lease inspect 仍为 active=false。

Comment thread loopx/cli_commands/turn_cadence.py Outdated
expected_agent_id=agent_id,
expected_automation_id=automation_id,
)
eligibility = readback["eligibility"]

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.

[P2] 保留人工启动。 相同的有效手工确认请求,主干在两种硬租约存储均获得 explicit_manual_interval_bypass、调用一次宿主并提交。当前头返回 interval_wait,宿主调用数保持原值。 保留现有手工意图与重放判断,把手工原因交给同一个 cadence 准入;有有效普通租约的显式启动仍可执行。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

已在 51e72fcf4a 修复。--manual-interval-bypass-reason 现在直接传给同一个原子 admit,不再经过只读投影,因此手工显式启动仍得到 explicit_manual_interval_bypass 并实际调用一次 Host。新增真实 File/SQLite 用例:配置 3 分钟间隔、先跑完第一个 Turn,再以手工原因启动第二个 Turn,断言 status=committed、host_invoked=true、宿主调用次数为 2。

@mergify

mergify Bot commented Nov 13, 2026

Copy link
Copy Markdown

This pull request has merge conflicts with main and cannot be merged
until they are resolved. Please rebase or merge the base branch, @gcl-coder.

Choose the remote for the base repository, not an out-of-date fork.
For a fork clone, first inspect git remote -v; upstream must point
to https://github.com/loopx-project/loopx.git. If it is absent, add it
with git remote add upstream https://github.com/loopx-project/loopx.git.
Then run:

git fetch upstream
git rebase upstream/main
# Resolve each conflict, git add the resolved files, then git rebase --continue.
git push --force-with-lease origin HEAD

For a same-repository clone whose origin points to
https://github.com/loopx-project/loopx.git, use origin instead of
upstream for fetch/rebase. If you prefer merging the base, use
git merge <base-remote>/main and push normally.

Keep the DCO Signed-off-by trailer on every commit when you rebase.
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Nov 13, 2026
Reserve or resume the owner cadence slot inside the execution-preparation
boundary, ahead of the Turn receipt and the ordinary claim, and hand that
same admission to the driver so a losing request leaves no lease, receipt or
journal behind. The manual interval bypass reason now reaches the same
atomic admission instead of a read-only projection that could not see it.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
@gcl-coder

Copy link
Copy Markdown
Contributor Author

Superseded by main.

Latest main already carries an equivalent fix in b89150097c1c (fix(turn): admit managed Codex preparation before claiming work). It validates the transport arguments and admits the owner interval before any Turn receipt, lease or journal write, and it normalizes/mints the Turn identity before the ordinary claim.

I verified the behaviour this thread asked for against 31ff0569 rather than assuming equivalence:

  • The three zero-side-effect regressions from this branch (malformed and wrong-shape --validation-command-json, interval_wait, --manual-interval-bypass-reason) pass unmodified on main: 8 passed, both File and SQLite providers.
  • tests/test_loopx_turn_driver.py plus tests/control_plane/test_turn_run_once_claim_preparation.py on main: 237 passed.
  • The interval-wait refusal now leaves no active lease, no appended quota_should_run receipt and no journal, and reports state_written=false truthfully.

Closing this PR to avoid maintaining a duplicate patch. The manual-interval-bypass zero-side-effect case is the one scenario I did not find covered by a main-side test; I am happy to send it as a small test-only follow-up if that coverage is wanted.

Not verified here: Windows, and I did not wait for hosted CI on this branch.

@gcl-coder gcl-coder closed this Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase Mergify: the pull request has merge conflicts with its base branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants