Skip to content

fix(turn): block ambiguous dispatch and revoked recovery - #7456

Open
hhyykk wants to merge 9 commits into
mainfrom
codex/interruption-recovery
Open

hhyykk wants to merge 9 commits into
mainfrom
codex/interruption-recovery

Conversation

@hhyykk

@hhyykk hhyykk commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

问题与结果

同一 Turn 在派发意图落盘后中断,如果 Host 结果尚未保存,不能把未知执行结果当作未执行并自动重派。已有 typed result 的恢复也必须满足当前 Goal 生命周期,避免退役后继续写回或扣费。

本 PR 保存明确的初始 attempt=0,并在未知派发结果时返回 host_outcome_unknown。恢复沿用原 journal、不可变上下文、provider 回执和结算身份。

本次修订

  • 合入 main 15471f8e832d7fb5893539fa053170ea233de5a7,解决 CLI 冲突,保留主干的 prepare_turn_host_request 提取及本 PR 的恢复准入捕获。
  • 统一复用现有 launch_lifetime,去掉 executor 对 source instance / legacy registration 的重复分支。相同 AST 指标从 92 条语句 / 56 个决策点降到 90 / 55;90 / 60 上限和例外清单均未改,完整 maintainability ratchet 通过。
  • 退役 Goal 不支持执行恢复,包括终态回执重放。 不添加退役兼容分支、迁移或替代身份。拒绝时保留原 journal 和已提交扣费;只读 inspect-journal 继续可用。
  • 有效 Goal 保留原回执恢复,不能重复调用 Host、写回或扣费。File / SQLite 新增退役后终态拒绝用例,并加强有效 Goal 重放时本次 effects 全为 false 的断言。
  • 同步协议的中英文说明,明确旧 evidence.json 的版本边界。运行代码、测试和文档分别提交,新增提交均带 DCO sign-off。

本轮验证

环境:Python 3.14.8、Node 24.21.0、Hypothesis 6.142.4。测试仅使用一次性隔离 fixture 和确定性注入 Host。

  • 重构前:原 16 项真实 CLI 中断/正常对照通过;复现维护预算 92 > 90 的失败。
  • 重构后:198 项 Python 检查通过,包含 18 项 File / SQLite 真实 CLI SIGKILL / 重启 / 正常对照、source lifetime、原结算回执、独立下游记录、Host 监督及租约恢复检查。
  • 139 项 TypeScript 检查通过;npm run typecheck:control-plane 通过。
  • 五项去除保护的 mutation probes 均被断言检出;语法或导入错误不计为检出。
  • 相关 Python ruff、差异检查通过。
  • 标准 premerge(固定 main 15471f8e832d7fb5893539fa053170ea233de5a7):19 项 smoke、6 项直接检查全部通过,包含 public-boundary 扫描,零失败。

Python 选择可重现为:

.venv/bin/python -m pytest -q tests/test_stage2c_dependency_lock.py tests/test_loopx_turn_executor.py tests/control_plane/test_turn_host_decision_capture.py tests/control_plane/test_turn_interruption_recovery.py tests/control_plane/test_turn_recovery_state_machine.py tests/control_plane/test_host_process.py tests/control_plane/test_first_party_host_runtime.py tests/control_plane/test_effect_program_fault_replay_matrix.py tests/control_plane/test_leased_host_process.py tests/control_plane/test_in_flight_turn_replay_cli.py

验证边界

额外的两文件 mypy 调用会递归检查依赖,基线 c3c647d1409e2abe997304f9fcf7bbe2cb247763 与本次修订均报告 617 个文件中的 5417 项错误。忽略源行号后完整 error / note 诊断集合无增减;这项检查没有通过,不能声称全仓 Python 类型检查通过。

evidence.json 保留原 head 335eae25f2544456c9bb644ea5db311210f747e0 的历史源码哈希、16 项进程记录和有限模型结果,不作为新 head 的验收。既有六项 native 连续性失败未在本次重跑或宣布修复;本次也不新增真实模型、Windows、断电持久化或任意外部工具 exactly-once 资格。

设计、历史失败和当前支持范围见恢复实现说明。本次未合并 PR。

Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com>
Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com>
Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com>
Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com>
Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com>
@mergify

mergify Bot commented Oct 27, 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, @hhyykk.

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 Oct 27, 2026

@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; runtime_reported; gpt-6.1-sol; OpenAI; reasoning_effort=max

REQUEST_CHANGES — 当前 head 335eae25f2544456c9bb644ea5db311210f747e0 有两个可复现问题:退休后的已结算回执恢复被误挡,以及新引入的维护预算失败。

动机 / Motivation

使用 turn run-once 恢复原任务的操作者。Host 已执行但结果未落盘时,旧版可能再次启动 Host;本版改为明确停止并要求核对原执行。未知派发不再自动重发,已保存结果仍可沿原身份结算;但已结算回执在 Goal 退休后被新增检查误挡。本次不交付双会话、完整统一契约、任意外部效果的回滚或所有宿主连续性。

这项修复对应真实可执行的崩溃窗口,具有独立价值。已保存结果、未知派发与已经完成的回执必须分开:前两者需要不同恢复动作,第三者应返回既有事实。退休后的已结算回执读回和当前函数预算尚未通过;完整 native 连续性另属原工作。

改动思路 / Approach

恢复判定、生命周期和结算继续复用现有 owner;新增状态仅把已有计数明确写为零。当前错误在 guard 的执行顺序,修复不需要新框架。本 PR 应完成原 Turn 的未知派发保护、撤权后待结算拒绝和合法历史读回,并通过已有维护预算;完整 C1 和双会话保持原范围。

execute_turn_run_once 把原有生命周期对象传给共享 driver;recoveryDecision 仍是恢复判定的 TypeScript owner,Python 负责检查顺序和实际 IO。原有 capture、journal、prepared effect 和结算身份均继续使用,未增加 scheduler、权限引擎或第二个 Goal。计数由真实执行器自动写入,零表示尚未记录派发意图;缺失证据保持未知,不能由摘要或进程退出推断未执行。

原 journal 当前应有动作 本次观察
intent 已落盘、无 typed result 停止,核对原执行与外部效果 File/SQLite 均正确拒绝重发
已保存 typed result 校验并只结算原未完成步骤 原矩阵通过,一笔原身份扣额
committed 且只丢响应 返回原回执,不产生新效果 Goal 退休后错误拒绝

这是从已读源码和实际反例整理的流程比较;不表示完整双会话或任意外部系统资格。仓库经验中“先检查真实后继和使用结果”的建议适用于本轮:我沿真实 CLI、下游 SQLite 和 CSV 核对了恢复效果;没有继承经验案例结论,也没有把上下文送达当作收益证明。

具体改动 / Changes

完整 base 218aa087b66bb99038b1ce7d782110741a5e42ae 到 head 有 22 个文件、+2236/-4;五个产品文件仅 +31/-2,其余主要是中断矩阵、独立业务 oracle、状态机、有限模型、测试依赖及两份文档。逐文件读过新增验证材料,作者的 JSON 是可追溯声明,未替代本次执行。Hypothesis/锁覆盖和 Pygments/Setuptools 历史修正保留其原版本边界。

验收依据为 docs/architecture/rfcs/controller-executor-unified-contract-v0.md,固定 spec_revision=9d2d022d14732b3b2dac309013fe9538b1d9ba5d,并参照原 stable Turn 协议。下面沿原条文映射,标签是本次审查引用,不扩大整个 RFC 的当前交付义务:

criterion_id 当前处置与证据
Record intent before invoking the executor implemented:派发前保存既有计数;intent/tool 矩阵与移除 guard 的 mutation 有实证。
Controller crashes after dispatch implemented:未知结果不自动重发;同六旧缺陷在基线失败、当前矩阵通过。
Result/settlement response is lost not_met:已完整结算后退休的真实同 key 恢复两后端失败;最小修复是保留无新增效果的历史回执返回。
Cancellation, changed requirements, expired lease or late result implemented within this slice:cached-result 退休拒绝、原 source lifetime/身份测试通过;不宣称任意外部工具持续 fencing。
Old persisted captures and receipts implemented for original identity/formats:原 capture/回执不改写;旧缺计数保守拒绝。已退休 terminal 例外仍由上行 not_met 保留。

关键代码讲解

  1. loopx/cli_commands/turn_run_once.py:64::execute_turn_run_once 在实际 execute 时创建并传递 legacy admission,使 cached-result 恢复也能使用已有生命周期 owner;preview 没有新增执行。
  2. loopx/control_plane/turn_driver/executor.py:1435::run_loopx_turn_once 初始化原计数为零,并在已有 journal 分支加入 lifetime 检查。问题是检查发生于识别 return_existing 之前,错误覆盖已完成的历史返回。
  3. loopx/control_plane/turn_driver/turn_journal.ts:417::recoveryDecision 对未完成前两阶段且计数非零或缺失的记录返回 host_outcome_unknown。已保存结果继续 validation,terminal 判定仍明确返回 terminal_result_retained。
  4. loopx/control_plane/turn_driver/turn_journal_effect_readback.ts:16::recordedTurnEffects 将缺失计数视为未知 null,只让明确零证明没有派发;这避免诊断把不确定效果显示成 false。

对主干的风险 / Risks

[P2] 保留退休 Goal 的已结算回执恢复。 在 executor.py:1559-1564,新增 launch_lifetime 位于恢复判定和 terminal 返回之前。复现步骤是:使用现有真实 CLI fixture,在 committed journal 落盘后、最终响应返回前 SIGKILL;随后将该 legacy Goal 退休,再用原 --resume-turn-key --execute 恢复。不可变基线 File/SQLite 均返回 committed,四项本次 effects 全为 false、仍只有一笔原扣额;当前 head 两例均 exit 1 / goal_not_registered。与此同时错误回复中的 journal owner 仍判定 action=return_existing、reason=terminal_result_retained。这不是新写入授权不足,而是既有完成结果无法正常返回。请区分无效果历史响应与需要新结算/hook 效果的恢复,保持后者的当前权限检查,不要清空 journal 或换身份规避。添加两后端退休 terminal 回归,并核对重复恢复零新增 Host/写回/扣额。

[P2] 修复本次引入的 required 维护检查失败。 标准 premerge 实际执行 19 项选中检查,有一项 control-plane-maintainability-ratchet-smoke.py 失败;不可变基线同命令通过。相同 AST 度量显示该函数由 89 增至 92 条语句,现行预算 90;decision points 为 55→56,预算 60。这里不是要求不相关重构或按 diff 行数否定价值,而是当前新增分支越过已有门。建议结合上条在现有恢复 owner 内做有界整理并证明语义不变,或按已有预算决策流程提供证据与审查;不能仅调大数字或把失败记为通过。

语义与 CI 对齐

PASS:本 head 的 176 项 Python(含 16 真实 CLI 正反路径)、138 项 TypeScript、control-plane typecheck,以及五项保护移除检测。FAIL:新增退休 terminal 反例两例;标准预检中的维护 ratchet。其余 18 项选中检查和六项 direct 检查通过,公开边界扫描也通过。独立基线六项 intent/tool/revoked 反例全部失败,证明修复确实改善原缺陷;这不抵消新反例。CQR cqr_115e96d7795dc6067b82 为 FAIL,已在首个公开结论前完整读取实际规范回执。

本轮未读取或等待 GitHub CI。作者保留的六项早期 native 连续性失败,不因新局部矩阵通过变成成功;本轮未新跑付费模型、Windows SIGKILL、断电持久性或外部工具持续 fencing。没有展示层变化,视觉截图不适用。未知派发是强制 refusal,相关文字只是解释恢复方式,不是可忽略的 guidance。

我的整体评价 / Verdict

本次交付是有价值的 bounded increment,现有 typed owner 和原 provider readback 的复用合理,生产改动成本相称。但 long_horizon 与 user_experience 均存在上述 terminal 恢复 regression;完整 base/head 比较为 unintended_drift,不能因为正常矩阵全绿就批准。该边界没有新增可选能力或默认关闭声明;当前 actor/authority 命名范围合适,既有 vocabulary 的复用不授予新执行权。

最高价值的整理是在现有 owner 区分 terminal readback 与 continuation preflight,同时解决已触发的函数预算;无须引入新框架或为了本 PR 完成整个 C1。视觉判断不适用的理由是实际 diff 仅涉及 CLI/runtime/测试文档;未验证范围仍为完整 native/Windows/断电/外部 fencing 和实际模型收益。请修复这两个当前问题后,按新完整 head 重验并复审;合并冲突、最终集成和维护者合并权限仍独立。本轮没有修改作者分支或执行合并。

English verdict: REQUEST_CHANGES at 335eae2: committed receipt replay is incorrectly blocked after legacy Goal retirement, and the new executor growth fails the native maintainability ratchet; 176 Python and 138 TypeScript checks pass, with two additional real CLI regressions reproduced.

Comment thread loopx/control_plane/turn_driver/executor.py Outdated
…7456-fix-20261028

Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>

# Conflicts:
#	loopx/cli_commands/turn_run_once.py
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
@mergify mergify Bot removed the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Oct 27, 2026

@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; runtime_reported; gpt-6.1-sol; OpenAI; reasoning_effort=max

APPROVE — 当前完整 head fa97552ebf09aeb9dbff5c4b40b4a768b2d94e81 的原 Turn 有界恢复修复通过本次独立核验;合并与实际安装采用另有门禁。

动机 / Motivation

使用原 Turn 恢复一次被中断工作的维护者与 Agent。Host 已向下游提交导出,但 typed result 尚未保存时进程被终止;旧版恢复会再次启动 Host,可能重复提交;当前版本保留不确定状态并停止自动重发。已保存结果能沿原身份完成剩余结算,活跃 Goal 的已完成回执可无新增效果重放;退役后的执行恢复明确拒绝,历史状态仍能通过只读入口检查。本切片不交付完整 controller/executor 分离、所有宿主连续性、任意外部工具恰好一次或真实模型收益。完整 C1、六项历史 native 连续性失败、正式 Host/安装采用和完整费用仍留在原验收范围;不由本 PR 的局部通过结清。

这里的 Host 是实际执行工具的宿主进程;“派发结果未知”意味着外部操作可能已发生,不能用进程退出推断尚未执行。重复启动会重复导出或其他不可撤回操作。新增保护有实际基线反例,原要求、产物与一次结算的正向接续也一并验证。

改动思路 / Approach

恢复不变量归原 TS journal,当前权限归原 Goal lifetime,IO 与规范回执仍在原 owner;将两类生命周期复用到一个 launch_lifetime 调用是有界整理。本 PR 的交付是原 Turn 未知派发拒绝、撤权执行拒绝与合法原身份接续;完整 C1、实际 native 连续性和任意外部 fencing 不在此范围。

CLI 把原 lifecycle admission 交给共享 driver,driver 在执行性恢复前复用当前生命周期检查;TypeScript 仍决定原 journal 的下一阶段。计数是执行器保存的派发事实,初态零不可从缺字段推导。原 provider 处理已提交但未落 checkpoint 的结算,通过查询原 effect reference 解决,不另造 store、scheduler 或幂等协议。

下表依据本 head 源码与本次实际进程实验,代码链接见下方;这是解释性对照,不是完整外部工具恰好一次证明。

原记录 当前动作 实测边界
明确 attempt 0 可沿原身份继续首次派发 原有 managed/recovery 测试通过
intent 已存、无 typed result 停止自动重发,保留未知 intent/tool 两后端 SIGKILL 通过
typed result 已存 校验并只结算未完成步骤 下游一次、原扣额一次
terminal 且 Goal 活跃 返回原结果,无新效果 响应丢失、重复恢复通过
Goal 已退役 execute 拒绝,inspect 只读 原 journal/扣额不改,合法恢复后可沿原 key 继续

具体改动 / Changes

完整差异以 15471f8e832d7fb5893539fa053170ea233de5a7 为 merge base,22 文件 +2297/-5;五个产品文件 +31/-3,其余主要为中断/状态机/oracle/model、明确的历史证据和测试依赖。生产部分没有因为验证材料较多就引入通用框架。已读完新增 TLA、配置、README 和历史 evidence.json;它们的旧 source hash 与六项 native 失败保留原版本,本轮未把静态材料当成当前执行证明。

接受依据为 docs/architecture/rfcs/controller-executor-unified-contract-v0.md,固定 spec_revision=15471f8e832d7fb5893539fa053170ea233de5a7,并参照同版本 stable Turn 协议中诊断不等于执行权限的条款。intent-before-dispatch implemented:保存意图后未知不得重发;original-result-settlement implemented:原 result/receipt 与 effect 身份保留、只处理未完成步骤;current-authority implemented:执行恢复检查当前原 Goal,退役后的只读诊断仍可达;retained-original-context implemented within this slice:本次 18 个真实 CLI 案例保留原 capture 字节,当前 codec/capture 校验通过。all-host-controller-continuity 为 out_of_scope,完整 C1/native/模型成果仍由原工作验收,不靠本 PR 提前宣告完成。

关键代码讲解

  1. execute_turn_run_once 仅在 execute 且原对象缺失时由现有 factory 创建 admission,然后将它交给唯一的生产 driver 调用;保留 main 的 prepare_turn_host_request 整理。
  2. run_loopx_turn_once 在已有 journal 分支使用一次 launch_lifetime,同时覆盖原 source instance 与 legacy registration;新 journal 明确保存计数零。已存结果不重新预留启动区间。
  3. recoveryDecision 在未完成前两阶段、计数非严格数值零时返回 host_outcome_unknown。正常 saved result/terminal 和显式 failed retry 仍有各自原判定,不从 retry flag 推断安全。
  4. recordedTurnEffects 对缺派发事实保持 null,只允许明确零说明未派发;这是观测,不是新增执行授权。

对主干的风险 / Risks

上一轮的两个问题已逐项重新检查。维护性方面,本 head 复用现有 launch_lifetime,避免重复 source/legacy 分支;原失败记录仍保留,未加预算豁免。退役 terminal 方面,当前版本明确选择拒绝所有执行式恢复,双语协议与兼容/回退说明已经披露。我对照旧接受协议重新区分了 replay_legal 诊断与 execute 权限:terminal 返回还可能进入 post-settlement recovery,并非无条件纯读。故不再要求另加退役执行兼容分支;历史事实通过原只读 inspector 检查,原 receipt/debit 保留。

这个边界有独立实际验证,不只改文案:在 File/SQLite 的 result 或 terminal checkpoint 后退役,execute 拒绝;四次 inspect-journal 均 effects=[],原 journal 与完整 runtime 文件快照(排除锁文件)不变。随后仅在隔离合成环境恢复原合法注册,原 --resume-turn-key 连续恢复两次,均不重启 Host,下游操作一条、原扣额一笔,第二次无新增效果。真实环境仍须合法 owner 决定生命周期,不能清 journal、换 key 或自动复活 Goal 来绕过拒绝。

语义与 CI 对齐

PASS:当前 119+59=178 项 Python,包含 18 项真实 CLI 中断/正常控制、Hypothesis 状态机及 capture/Host/authority/effect/依赖锁检查;51+88=139 项 TypeScript、control-plane typecheck、全部变更 Python Ruff、diff check 通过。另同一个八例 harness 在不可变 base 为两正常通过、六个 intent/tool/revoked 失败,在当前 head 八项全过;五个保护移除 mutation 均由业务断言抓到,语法/导入失败没有充数。Hypothesis 的目录忽略 warning 原样保留。

CQR cqr_641f8367365a7473c60f 为 PASS,首个公开结论前已物理完整读取。当前原生 standard premerge 的 6 项 direct 与 19 项选中检查全部通过,零失败、零人工 hold;原函数维护 ratchet 通过,预算和例外未改。gate 的验证通过不授予合并权。未查询、轮询或等待 GitHub CI。新测试依赖仅属于 test extra,原 hash lock 只补 Hypothesis/sortedcontainers;历史 Pygments/Setuptools 和原 UI/native 失败不被当前局部绿测覆盖。

旧未完成 journal 缺 attempt 计数现在保守拒绝,旧 binary 降级前须隔离或核对未知 intent;原 capture/receipt 从未被改写来制造连续性。这里新增的是既有词汇下的诊断 reason,不是第二 Actor 生命周期或新的可选开关;unknown refusal 和当前权限是机器义务,恢复说明是解释,不是可忽略 guidance。所有真实 fixture 均在一次性私有环境,没有碰活动 Goal 或用户原文件。

我的整体评价 / Verdict

本次是相称的 justified_increment:long_horizon 在有界原 Turn 安全恢复上 improved,基线反例被当前真实入口消除,已存结果和合法接续的后继也验证了。user_experience 为 accepted_tradeoff:正常原 key 恢复无需重新输入任务或额外确认;未知外部效果必须核对,退役执行必须停止,一次只读检查能读取保留状态。接受依据是独立旧协议的当前权限/未知效果条款及已有 inspect/execute 分工;四项合法恢复实证限定了代价与恢复路径,没有借作者意图单独正当化新门禁。

恢复不变量归原 TS journal,当前权限归原 Goal lifetime,IO 与规范回执仍在原 owner;将两类生命周期复用到一个 launch_lifetime 调用是有界整理。本 PR 的交付是原 Turn 未知派发拒绝、撤权执行拒绝与合法原身份接续;完整 C1、实际 native 连续性和任意外部 fencing 不在此范围。

已检查 CLI、共享 executor、journal 与 inspector;本 diff 没有渲染页面或布局改动,视觉评审不适用。视觉未验证范围为 none for rendered presentation;实际安装 Host/模型采用仍单列。保留六项较早版本 native 连续性失败,本轮未重新资格化;Windows SIGKILL、断电、任意外部工具 fencing、TLA 实跑、真实模型和完整费用也未宣称通过。原始要求在合成任务中有独立 CSV/downstream oracle,不能外推为真实模型理解或人员独立的作者实验。

有界整理已经应用,当前没有新增阻塞 finding。批准仅针对本次完整源码切片;历史 review、最终 target 集成、维护者合并与安装权限保持独立,未执行 dismissal 或 merge。

English verdict: APPROVE at fa97552: the bounded original-Turn recovery fix passes independent real File/SQLite crash, readonly-retirement and lawful same-key continuation checks, plus178Python/139TypeScript tests and five sensitive mutations. Full native continuity, live adoption and complete-cost benefit remain unqualified.

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