重构 Direct 交付合同与租约机制 #608
Reference in New Issue
Block a user
Delete Branch "contract-lease-refactor"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
当前 Direct 把交付合同作为操作准入条件,并在任意付费/执行失败后进入全局 Draining;一条无关的挂起命令可因此阻断后续代码编辑。本 PR 用于推进 #518、#529 的合同与租约机制重构。
当前仅提交草稿里程碑,运行时代码尚未修改,问题尚未修复。
工作范围:
docs/project-memory/plans/【里程碑】Direct合同与租约机制重构-2026-10-03.md。验证:
npm run check:doc-index、npm run check:encoding、git diff --cached --check通过。本次未运行运行时测试。关联 #518、#529;当前不关闭问题。
本次讨论关于租约机制的结论,作为后续实现方向:
合同方面:已识别 artifact/command 强制验收与 Agent 自身读取文件、判断命令结果的重叠。可选合同曾作为方向讨论,但是否保留新游戏等产品场景的强制交付检查、触发时机和工具形态尚未最终确定,需继续细化,不能把当前草稿视为已完成方案。
以上是已确认方向和明确的未决项;当前 PR 只有范围文档,尚未修改运行时实现。
This PR shall close #518 #529
合同机制的讨论结论更新:
目标是让合同跟随用户真实的游戏制作/交付请求,而不是由项目初始状态强加任务。此评论记录已确认方向;当前 PR 仍为 WIP,运行时尚未实现这些变更。
Review by Grok
Reviewed
contract-lease-refactor(063905ee) againstorigin/master.This branch replaces Direct execution leases and repair batches with per-operation admission, and it narrows delivery contracts to visual and gameplay checks that run only after a normal model response. In-process admission, migration, and evidence tests match that model. The production handoff back to
Workingstill depends on a full process-tree proof that Unix never produces, so no-contract completion and post-response sealing do not run on Linux.Issue counts
Issues
1. bug —
execution.rsprocess exit prooffinish_model_attemptrecordsProcessTreeExitProof::confirmed(), which requiresfull_tree_verified. That flag is Windows-only (process_tree.rsbuildsscope: "process-group-only"on Unix). On Linux a successful group shutdown still recordsproven == false.record_process_exit_proofthen forcesInterruptedand a “缺少完整子进程退出证明” report beforefinish_attemptruns.finish_attemptreturns immediately unless the phase is stillClosing, so it never restoresWorking.review_replyreturns that terminal report and does not seal or callfinish_without_contract. The sameconfirmed()value inshutdown_and_reportmoves a ready contract toSealing, then the false proof flips the phase toInterruptedbeforefinish_sealing. Mid-operationtry_sealwas removed, so this is now the only completion path. Unit tests passrecord_process_exit_proof(true)directly and do not assert phase or the returned report.On Unix, set
executor_stoppedfromowned_scope_retired()and leave the phase inClosingsofinish_attemptcan return toWorking. Reserveconfirmed()for a missing or failed group shutdown. Assert on Linux that a no-contract attempt with a finished operation reachesCompletedwithterminal_report == None, and that a ready contract reachesCompletedwith the host acceptance report.2. bug —
process_tree.rs:195linux_process_group_has_live_memberis meant to ignore zombies sokill(-pgid, 0)does not keep a reparented zombie group alive untilSTOP_TIMEOUT.entry.ok()?aborts the whole scan toNoneon the firstread_direrror, and the caller falls back tokill(-pgid, 0), which still treats zombies as a live group. An unreadable/proc/<pid>/statis skipped. If every live member is skipped, the function returnsSome(false).observed_tree_emptythen returns true without consultingkill.terminate_owned_treereturns immediately without signaling the group, and shutdown can report the owned scope retired while a live child remains.On a bad directory entry or an unreadable
statfor a numeric pid, do not returnNoneor treat the group as empty. Skip only entries that are clearly not a live member. If any numeric pid in the target group cannot be classified, returnNoneso the caller keeps thekill(-pgid, 0)result.3. suggestion —
direct_delivery.rs:522finish_without_contractcallstick_lockedand, if the wall clock or execution budget has just expired, commitsExhaustedplus the budget report, then returnsErr("direct-execution-close: 回合尚不能结束")because the phase is no longerWorking.review_replyuses?on that error. The GUI/CLI match arms treatreview_replyerrors as turn failures and do not readterminal_report. The user sees an unclassified close error instead of the budget report that was just persisted.If
finish_without_contractor the post-try_sealpath observes a terminal phase, returnOk(Some(terminal_report))instead of propagating the close error.@@ -1204,6 +1193,13 @@ impl ExecutionAdapter {let session = Arc::clone(&self.session);let _ =tokio::task::spawn_blocking(move || session.record_process_exit_proof(proven)).await;[bug] Review by Grok.
finish_model_attemptpassesProcessTreeExitProof::confirmed()intorecord_process_exit_proof.confirmed()requiresfull_tree_verified, which is Windows-only. On Linux a successful group shutdown still recordsproven == false, so the phase becomesInterruptedbeforefinish_attemptcan return toWorking. No-contract completion and post-responsefinish_sealingtherefore do not run. The sameconfirmed()value inshutdown_and_reportflips a ready contract out ofSealing.Suggestion: On Unix, set
executor_stoppedfromowned_scope_retired()and leave the phase inClosing. Reserveconfirmed()for a missing or failed group shutdown. Assert the Linux phase and returned report.@@ -186,0 +192,4 @@#[cfg(all(unix, target_os = "linux"))]fn linux_process_group_has_live_member(pgid: i32) -> Option<bool> {for entry in std::fs::read_dir("/proc").ok()? {let entry = entry.ok()?;[bug] Review by Grok.
entry.ok()?aborts the whole/procscan on the first directory error. The caller then falls back tokill(-pgid, 0), which still treats zombies as a live group. Separately, an unreadable/proc/<pid>/statis skipped; if every live member is skipped, this returnsSome(false)and shutdown can report the owned scope retired while a live child remains.Suggestion: Skip only entries that are clearly not live members. If any numeric pid in the group cannot be classified, return
Noneso the caller keeps thekill(-pgid, 0)result.@@ -710,2 +520,3 @@let ledger = session.snapshot()?;if ledger.contract.is_none() && !ledger.requires_contract {if ledger.contract.is_none() {session.finish_without_contract()?;[suggestion] Review by Grok.
finish_without_contractcan commit anExhaustedbudget report and then returnErr("direct-execution-close: 回合尚不能结束")because the phase is no longerWorking.review_replypropagates that error, and the GUI/CLI turn-failure arms do not readterminal_report.Suggestion: If this path observes a terminal phase, return
Ok(Some(terminal_report)).Follow-up by Grok
The three findings from the previous review are fixed on
691e51e85(修复 Direct 回合收尾与进程退出判断).finish_model_attemptandshutdown_and_reportrecordowned_scope_retired()instead ofconfirmed(). A retired Unix process group setsexecutor_stoppedand leaves the phase inClosingorSealing, so a no-contract turn can return toWorkingand a ready contract canfinish_sealing.no_contract_operation_completes_with_original_response_after_group_shutdownandready_contract_allows_operation_then_complete_response_before_host_shutdownpassed./procgroup scan: a directory error or an unreadablestatno longer makes the group look empty, and the scan no longer stops at the first bad entry. A vanished pid or a zombie is not a live member. If a numeric pid in the group cannot be classified, the scan returnsNoneand the caller keepskill(-pgid, 0).incomplete_process_scan_never_proves_empty,process_scan_distinguishes_live_zombie_foreign_and_disappeared_pids, andreal_zombie_is_not_a_live_group_member_before_reapingpassed.review_replyreturns a persisted terminal report afterfinish_without_contractand after sealing. AnExhaustedreport created during close is shown instead ofdirect-execution-close: 回合尚不能结束. A close with no terminal report still returns that error.no_contract_review_returns_budget_report_created_during_closeandno_contract_review_preserves_close_error_without_terminal_reportpassed.