修复多 Agent 并行写入导致的自主构建中断

将执行前 project revision 漂移改为 blocked observation 并在同一 run 自动重新规划
为 file.write 和 file.patch 增加项目锁内 pending 与 revision 二次校验
补充 stale action 回归、真实外部模型与确定性可玩塔防验收记录
This commit is contained in:
AIGameCreator App
2026-07-22 19:49:12 +08:00
parent 9371cc537a
commit 64ca2f99b2
11 changed files with 317 additions and 90 deletions
@@ -250,8 +250,22 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
false,
|| observe_agent_runtime_file(root, &action.input),
),
"file.write" => observe_agent_runtime_file_write(root, agent_id, run_id, &action.input),
"file.patch" => observe_agent_runtime_file_patch(root, agent_id, run_id, &action.input),
"file.write" => observe_agent_runtime_file_write(
root,
agent_id,
run_id,
action,
&action_fingerprint,
pending_action,
),
"file.patch" => observe_agent_runtime_file_patch(
root,
agent_id,
run_id,
action,
&action_fingerprint,
pending_action,
),
"file.delete" => {
observe_agent_runtime_file_delete(root, agent_id, run_id, pending_action, &action.input)
}
@@ -564,6 +578,15 @@ pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_loc
}
}
if validate_revision_gate {
match pending_project_revision_drift_observation(root, pending, true) {
Ok(Some(observation)) => return Err(observation),
Ok(None) => {}
Err(error) => {
return Err(agent_runtime_pending_reconciliation_observation(
tool, root, &error,
));
}
}
if let Err(error) =
validate_agent_runtime_pending_verification_gate_before(root, pending)
{
@@ -347,11 +347,9 @@ pub(crate) fn agent_runtime_tool_requires_pending_revision_gate(tool: &str) -> b
)
}
pub(crate) fn validate_agent_runtime_pending_verification_gate_before(
root: &Path,
fn agent_runtime_pending_expected_project_revision(
pending: &AgentRuntimePendingToolAction,
) -> Result<(), String> {
validate_agent_runtime_project_revision(root, &pending.project_revision_before)?;
) -> Result<u64, String> {
let prior_action_count = usize::try_from(pending.action_index).unwrap_or(usize::MAX);
let prior_revision_advances = pending
.observations
@@ -360,11 +358,44 @@ pub(crate) fn validate_agent_runtime_pending_verification_gate_before(
.take(prior_action_count)
.filter(|observation| agent_runtime_observation_advances_project_revision(observation))
.count();
let expected_revision = pending
pending
.project_revision_before
.revision
.checked_add(u64::try_from(prior_revision_advances).unwrap_or(u64::MAX))
.ok_or_else(|| "Agent Runtime 待执行动作的预期项目 revision 已达到上限".to_string())?;
.ok_or_else(|| "Agent Runtime 待执行动作的预期项目 revision 已达到上限".to_string())
}
pub(in crate::agent) fn pending_project_revision_drift_observation(
root: &Path,
pending: &AgentRuntimePendingToolAction,
validate_revision_gate: bool,
) -> Result<Option<AgentRuntimeToolObservation>, String> {
if !validate_revision_gate {
return Ok(None);
}
validate_agent_runtime_project_revision(root, &pending.project_revision_before)?;
let expected_revision = agent_runtime_pending_expected_project_revision(pending)?;
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
if current_revision.revision == expected_revision {
return Ok(None);
}
Ok(Some(AgentRuntimeToolObservation {
tool: pending.action.tool.clone(),
status: "blocked".to_string(),
summary: "并行项目变更使旧动作过期,旧动作未执行".to_string(),
detail: Some(format!(
"projectRevisionDrift=true · expectedRevision={expected_revision} · currentRevision={} · replanRequired=true",
current_revision.revision
)),
}))
}
pub(crate) fn validate_agent_runtime_pending_verification_gate_before(
root: &Path,
pending: &AgentRuntimePendingToolAction,
) -> Result<(), String> {
validate_agent_runtime_project_revision(root, &pending.project_revision_before)?;
let expected_revision = agent_runtime_pending_expected_project_revision(pending)?;
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
if current_revision.revision != expected_revision {
return Err(format!(
@@ -167,6 +167,26 @@ pub(crate) fn pending_repository_context_drift_observation(
}))
}
pub(in crate::agent) fn pending_action_pre_execution_drift_observation(
root: &Path,
pending: &AgentRuntimePendingToolAction,
) -> Result<Option<AgentRuntimeToolObservation>, String> {
if let Some(observation) = pending_repository_context_drift_observation(root, pending)? {
return Ok(Some(observation));
}
let validate_revision_gate =
agent_runtime_tool_requires_pending_revision_gate(&pending.action.tool);
if let Some(observation) =
pending_project_revision_drift_observation(root, pending, validate_revision_gate)?
{
return Ok(Some(observation));
}
if validate_revision_gate {
validate_agent_runtime_pending_verification_gate_before(root, pending)?;
}
Ok(None)
}
pub(crate) fn reject_game_creator_agent_runtime_task_at(
root: &Path,
agent_id: &str,
@@ -1837,8 +1837,8 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
);
return AgentBackgroundTaskOutcome::Finished;
}
let repository_context_drift =
match pending_repository_context_drift_observation(&root, &pending_action) {
let pre_execution_drift =
match pending_action_pre_execution_drift_observation(&root, &pending_action) {
Ok(observation) => observation,
Err(error) => {
let _ = mark_game_creator_agent_runtime_needs_reconciliation_at(
@@ -1850,7 +1850,7 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
return AgentBackgroundTaskOutcome::NeedsReconciliation;
}
};
if let Some(observation) = repository_context_drift {
if let Some(observation) = pre_execution_drift {
pending_action.status =
AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED.to_string();
pending_action.observation = Some(observation.clone());
@@ -1876,22 +1876,6 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
durable_action = Some(pending_action);
observation
} else {
if agent_runtime_tool_requires_pending_revision_gate(
&pending_action.action.tool,
) {
if let Err(error) = validate_agent_runtime_pending_verification_gate_before(
&root,
&pending_action,
) {
let _ = mark_game_creator_agent_runtime_needs_reconciliation_at(
&root,
&mut runtime,
&pending_action,
&error,
);
return AgentBackgroundTaskOutcome::NeedsReconciliation;
}
}
let action_is_current =
match mark_game_creator_agent_runtime_auto_action_executing_if_current(
&root,
@@ -136,8 +136,8 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
let auto_execution = pending.is_auto();
let observation = match pending.status.as_str() {
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED => {
let repository_context_drift =
match pending_repository_context_drift_observation(&root, &pending) {
let pre_execution_drift =
match pending_action_pre_execution_drift_observation(&root, &pending) {
Ok(observation) => observation,
Err(error) => {
let _ = mark_game_creator_agent_runtime_needs_reconciliation_at(
@@ -149,7 +149,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
return;
}
};
if let Some(observation) = repository_context_drift {
if let Some(observation) = pre_execution_drift {
pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED.to_string();
pending.observation = Some(observation.clone());
pending.updated_at = unix_timestamp();
@@ -173,19 +173,6 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
}
observation
} else {
if agent_runtime_tool_requires_pending_revision_gate(&pending.action.tool) {
if let Err(error) =
validate_agent_runtime_pending_verification_gate_before(&root, &pending)
{
let _ = mark_game_creator_agent_runtime_needs_reconciliation_at(
&root,
&mut runtime,
&pending,
&error,
);
return;
}
}
pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING.to_string();
pending.updated_at = unix_timestamp();
if let Err(error) =
@@ -100,8 +100,11 @@ pub(in crate::agent) fn observe_agent_runtime_file_write(
root: &Path,
agent_id: &str,
run_id: &str,
input: &serde_json::Value,
action: &AgentRuntimeToolAction,
action_fingerprint: &str,
pending_action: Option<&AgentRuntimePendingToolAction>,
) -> AgentRuntimeToolObservation {
let input = &action.input;
let path = input
.get("path")
.and_then(|value| value.as_str())
@@ -168,6 +171,30 @@ pub(in crate::agent) fn observe_agent_runtime_file_write(
};
}
};
if let Some(pending) = pending_action {
if pending.agent_id != agent_id
|| pending.run_id != run_id
|| pending.action != *action
|| pending.action_fingerprint != action_fingerprint
{
return agent_runtime_pending_reconciliation_observation(
"file.write",
root,
"等待项目锁后 file.write pending action 身份已变化",
);
}
match pending_action_pre_execution_drift_observation(root, pending) {
Ok(Some(observation)) => return observation,
Ok(None) => {}
Err(error) => {
return agent_runtime_pending_reconciliation_observation(
"file.write",
root,
&error,
);
}
}
}
if let Err(error) =
prepare_agent_runtime_project_mutation_locked(root, agent_id, run_id, "file.write")
{
@@ -319,8 +346,11 @@ pub(in crate::agent) fn observe_agent_runtime_file_patch(
root: &Path,
agent_id: &str,
run_id: &str,
input: &serde_json::Value,
action: &AgentRuntimeToolAction,
action_fingerprint: &str,
pending_action: Option<&AgentRuntimePendingToolAction>,
) -> AgentRuntimeToolObservation {
let input = &action.input;
let path = agent_runtime_tool_input_text(input, &["path"]);
if path.is_empty() {
return AgentRuntimeToolObservation {
@@ -404,6 +434,30 @@ pub(in crate::agent) fn observe_agent_runtime_file_patch(
};
}
};
if let Some(pending) = pending_action {
if pending.agent_id != agent_id
|| pending.run_id != run_id
|| pending.action != *action
|| pending.action_fingerprint != action_fingerprint
{
return agent_runtime_pending_reconciliation_observation(
"file.patch",
root,
"等待项目锁后 file.patch pending action 身份已变化",
);
}
match pending_action_pre_execution_drift_observation(root, pending) {
Ok(Some(observation)) => return observation,
Ok(None) => {}
Err(error) => {
return agent_runtime_pending_reconciliation_observation(
"file.patch",
root,
&error,
);
}
}
}
if let Err(error) =
prepare_agent_runtime_project_mutation_locked(root, agent_id, run_id, "file.patch")
{
@@ -671,6 +671,122 @@ fn project_snapshot_rereads_durable_pending_after_project_lock_wait() {
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn agent_runtime_file_write_and_patch_replan_after_locked_revision_drift() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "并行写入 revision 围栏").expect("project init");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow direct file mutations in test");
fs::write(root.join("game/write-target.txt"), "write-original\n")
.expect("write file.write target");
fs::write(root.join("game/patch-target.txt"), "patch-original\n")
.expect("write file.patch target");
let cases = [
(
"code-prototype",
"code-write-locked-drift-run",
AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: Some("写入完整游戏文件".to_string()),
input: serde_json::json!({
"path": "game/write-target.txt",
"content": "stale-write\n"
}),
},
"game/write-target.txt",
"write-original\n",
),
(
"design-director",
"design-patch-locked-drift-run",
AgentRuntimeToolAction {
tool: "file.patch".to_string(),
reason: Some("局部修改游戏文件".to_string()),
input: serde_json::json!({
"path": "game/patch-target.txt",
"oldText": "patch-original",
"newText": "stale-patch",
"expectedReplacements": 1
}),
},
"game/patch-target.txt",
"patch-original\n",
),
];
for (index, (agent_id, run_id, action, target, expected_content)) in
cases.into_iter().enumerate()
{
let mut state = start_game_creator_agent_runtime_task_at(
&root,
agent_id,
"执行等待项目锁的并行文件修改",
run_id,
"agent-background-task",
"准备执行文件修改",
vec!["修改目标文件".to_string()],
)
.expect("start runtime state");
state.loop_iteration = 1;
let mut pending = pending_tool_action_for_test(
&root,
&state,
action.clone(),
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING,
None,
);
pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("write pending file action");
state.status = "running".to_string();
state.phase = "action".to_string();
state.pending_tool_action = Some(pending.summary());
append_game_creator_agent_runtime_task(&root, &state).expect("append running task");
write_game_creator_agent_runtime_state(&root, &state).expect("write running state");
assert_eq!(
advance_project_revision_for_test(
&root,
"art-director",
&format!("art-file-drift-{index}"),
"file.write",
),
u64::try_from(index + 1).expect("expected revision"),
);
let observation = execute_game_creator_agent_runtime_tool_action_with_pending_action(
&root,
agent_id,
run_id,
&state.current_task,
&action,
Some(&pending.action_id),
Some(&pending),
)
.await;
assert_eq!(observation.status, "blocked", "{observation:?}");
assert!(observation.summary.contains("旧动作未执行"));
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("projectRevisionDrift=true")));
assert_eq!(
fs::read_to_string(root.join(target)).expect("read preserved target"),
expected_content,
);
}
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn agent_runtime_file_delete_revalidates_revision_after_acquiring_project_lock() {
let root = unique_project_path();
@@ -791,7 +907,7 @@ async fn agent_runtime_file_delete_reports_reconciliation_when_audit_fails_after
}
#[tokio::test]
async fn background_agent_runtime_file_delete_confirmation_rejects_stale_project_revision() {
async fn background_agent_runtime_file_delete_confirmation_replans_after_stale_project_revision() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let target = root.join("game/stale-confirm-delete-target.txt");
@@ -813,8 +929,15 @@ async fn background_agent_runtime_file_delete_confirmation_rejects_stale_project
"response": ""
})
.to_string();
let final_response = "旧删除动作未执行,已按最新项目 revision 完成重新规划。";
let (sender, receiver) = mpsc::channel();
let base_url = spawn_mock_llm_server_responses_with_capture(vec![delete_plan], Some(sender));
let base_url = spawn_mock_llm_server_responses_with_capture(
vec![
delete_plan,
final_tool_plan_response(final_response.to_string()),
],
Some(sender),
);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
@@ -885,38 +1008,29 @@ async fn background_agent_runtime_file_delete_confirmation_rejects_stale_project
assert_eq!(approved.state.run_id, "design-stale-delete-confirm-run");
assert_eq!(approved.state.status, "running");
let mut reconciled = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read stale delete runtime")
.state;
for _ in 0..250 {
if reconciled.phase == "needs-reconciliation" {
break;
}
std::thread::sleep(Duration::from_millis(20));
reconciled = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read stale delete reconciliation")
.state;
}
assert_eq!(reconciled.run_id, "design-stale-delete-confirm-run");
assert_eq!(reconciled.status, "failed");
assert_eq!(reconciled.phase, "needs-reconciliation");
assert!(reconciled
.error
.as_deref()
.is_some_and(|error| error.contains("项目 revision 已变化")));
let replanning_request = receiver
.recv_timeout(Duration::from_secs(10))
.expect("same-run replanning request after stale delete approval");
assert!(replanning_request.contains("design-stale-delete-confirm-run"));
assert!(replanning_request.contains("projectRevisionDrift=true"));
assert!(replanning_request.contains("旧动作未执行"));
let completed = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(completed.run_id, "design-stale-delete-confirm-run");
assert_eq!(completed.status, "idle");
assert_eq!(completed.phase, "completed");
assert_eq!(completed.last_response.as_deref(), Some(final_response));
assert!(completed.error.is_none());
assert!(completed.recent_tool_calls.iter().any(|call| {
call.tool == "file.delete"
&& call.status == "blocked"
&& call.summary.contains("旧动作未执行")
}));
assert_eq!(
fs::read_to_string(&target).expect("read target after stale approval"),
"revision 漂移后必须保留\n"
);
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
let persisted: AgentRuntimePendingToolAction = serde_json::from_str(
&fs::read_to_string(&pending_path).expect("stale delete ledger must remain"),
)
.expect("parse reconciled stale delete ledger");
assert_eq!(
persisted.status,
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED
);
assert!(!pending_path.exists());
let records = read_agent_db_records_for_test(&root);
assert!(records.iter().any(|record| {
record["recordType"] == "agent.runtime.tool_confirmation.approved"
@@ -930,13 +1044,10 @@ async fn background_agent_runtime_file_delete_confirmation_rejects_stale_project
record["recordType"] == "agent.runtime.file.delete"
&& record["path"] == "game/stale-confirm-delete-target.txt"
}));
cancel_game_creator_agent_runtime_task_at(
&root,
"design-director",
"design-stale-delete-confirm-run",
)
.expect("cancel reconciled stale delete task after manual check");
assert!(!records.iter().any(|record| {
record["recordType"] == "agent.runtime.tool_action.needs_reconciliation"
&& record["actionId"] == pending_summary.action_id
}));
fs::remove_dir_all(root).ok();
}
@@ -53,14 +53,13 @@ fn agent_runtime_run_status_rechecks_revision_inside_project_lock() {
Some(&pending),
),
);
assert_eq!(
observation.status,
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
);
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("revision 已变化")));
assert_eq!(observation.status, "blocked", "{observation:?}");
assert!(observation.summary.contains("旧动作未执行"));
assert!(observation.detail.as_deref().is_some_and(|detail| detail
.contains("projectRevisionDrift=true")
&& detail.contains("expectedRevision=0")
&& detail.contains("currentRevision=1")
&& detail.contains("replanRequired=true")));
fs::remove_dir_all(root).ok();
}
@@ -5261,3 +5261,10 @@
- 集成边界:集成修复只补齐 `tool_plan_handoff` 下沉测试不再继承父模块作用域后缺失的 `response_fingerprint``validate_ledger``AsRawFd` import,并对兼容重导出添加局部 `#[allow(unused_imports)]`;未删除兼容出口,编译警告总数仍为 `18`
- 验收:客户端 crate 的 `cargo fmt --check``cargo check``cargo check --tests` 通过;`tool_plan_handoff``44/44``swarm_cli``35/35``browser``21 passed / 3 real Chrome ignored`。Linux 串行全量为 `1146 passed / 5 ignored / 0 failed`。确定性真实 Runner + Chrome E2E 为 **PASS**Provider lifecycle `17/17`,项目 revision `0 -> 2`,固定试玩 `37/37`,终局残留与泄漏均为 `0`
- 残余验证缺口:Windows cross check 在进入项目代码前即因宿主缺少 `x86_64-w64-mingw32-gcc` 而停止;本轮不能据此宣称 Windows 交叉编译已通过,需在补齐宿主交叉链接器后复验。
## 2026-07-22 Agent Runtime 并行 revision 漂移自动重规划
- 背景:真实无人干预塔防 E2E 中,`quality-review` 已在其独立产物路径写入验证脚本并把项目 revision 从 `0` 推进到 `1`;并行的 `code-prototype` 随后准备写 `game/index.html`。该动作尚未执行,却因 planning 时保存的全局 revision 为 `0` 被标记为 `needs-reconciliation`CLI 立即返回,项目没有生成。
- 决策:pending action 在执行前发现 project revision 漂移时,必须持久化为 `blocked` observation,明确 `projectRevisionDrift=true / replanRequired=true / 旧动作未执行`,清理旧 pending,并让同一 Agent、同一 run 基于最新项目状态继续 planning。只有副作用可能已经发生、持久身份损坏或审计无法证明结果时才进入 `needs-reconciliation`
- 锁内边界:`file.write``file.patch` 在取得项目写锁后再次核对 pending 身份、仓库上下文、revision 和 verification gate,避免预检后与另一 Agent 的项目修改交错。revision 漂移只拒绝旧动作,不忽略并发变化,也不直接执行可能覆盖他人结果的旧写入。
- 验收:三个定向回归、全部 `revision` 过滤测试 `32/32``cargo check --tests` 和 Linux 串行全量 `1147 passed / 5 ignored / 0 failed` 通过。确定性正式 E2E 继续以 `17/17` Provider lifecycle、revision `0 -> 2` 和 Chrome `37/37` 通过。新的独立真实 external-provider E2E 只写入一次塔防需求并立即 EOF,人工 approve / answer / steer 均为 `0`;父 turn `settled`,项目 revision `0 -> 8`static smoke 和真实 Chrome `lane-defense-v1 37/37` 通过,唯一 Supervisor assistant 写入,pending、confirmation、user-input、reconciliation、sidecar、重复与敏感信息泄漏均为 `0`
@@ -3535,3 +3535,11 @@
- 原因:Rust 子模块不会继承父模块的私有 `use` 作用域;兼容重导出的价值是维持旧调用面,不能用当前 facade 是否直接消费来判断;多个 Agent 即使写入范围互不重叠,全 crate 编译仍会读取全部模块,因而无法避开正在落盘的半成品。
- 处理:测试下沉时显式补齐自身依赖的 import,不把生产可见性为测试统一放宽。已确认属于旧调用面的重导出必须保留,只在精确重导出位置添加局部 `#[allow(unused_imports)]`,不得按 warning 机械删除或全局 suppress。并行阶段禁止启动全 crate 编译、全量测试和真实 E2E;各 Agent 只执行自己边界内的检查,待所有写入方完成后由主线程在稳定共享树统一验收。
- 验证:稳定树统一运行 `cargo fmt --check``cargo check``cargo check --tests`、三个定向测试组、Linux 串行全量和确定性真实 Runner + Chrome E2E;同时核对原测试名、`turn.report` 字段/顺序、raw JavaScript 哈希和兼容重导出。Windows cross check 若因宿主缺少交叉链接器而未进入项目代码,必须明确记录为残余验证缺口,不能写成项目代码已通过。
## 未执行的并行 stale 动作不能进入 needs-reconciliation
- 现象:两个专业 Agent 在不同文件上并行工作,一个 Agent 先推进全局 project revision;另一个 Agent 的 pending 写动作尚未执行,却因 revision 与 planning 快照不同进入 `failed / needs-reconciliation`。Swarm CLI 随即提前结束,父 Supervisor 仍在等待回执,项目 revision 甚至可能尚未包含核心游戏文件。
- 原因:旧实现把“执行前发现计划过期”和“执行后无法证明副作用结果”合并成同一种 reconciliation。全局 revision 会被任何合法项目修改推进,因此它能证明旧计划已过期,却不能证明尚未开始的动作产生了未知副作用。
- 处理:在 pending 标记为 executing 之前检查 revision 漂移;漂移时写入可恢复的 blocked observation,明确旧动作未执行并要求同 run 重新规划。文件写入和 patch 在项目锁内再做一次同样检查,防止预检后的竞态。只有动作可能已落盘、账本身份冲突、审计失败或持久记录损坏时继续失败关闭到 reconciliation。
- 验证:必须覆盖确认后 stale 动作和锁内 stale 动作两条路径,证明目标文件未改变、旧 pending 被收束、下一次 Provider planning 使用同一 run、最终状态可完成,并用单输入真实 external-provider E2E 验证并行专业 Agent 最终生成可试玩项目。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs`
@@ -697,3 +697,6 @@ game-project/
- 同轮 `swarm_cli.rs``4420` 行降到 `68` 行并拆为 `9` 个子模块,最大生产模块 `observer.rs``843` 行、测试模块为 `1529` 行;原 `35` 个测试名与 `turn.report` 字段/顺序不变。`browser.rs``4036` 行降到 `24` 行并拆为 `11` 个子模块,最大生产模块 `capture.rs``733` 行、`playtest/mod.rs``612` 行,测试模块为 `1169` 行;内嵌 raw JavaScript 的搬迁前后哈希一致。
- 第三轮集成修复仅补 `tool_plan_handoff` 下沉测试缺失的 `response_fingerprint / validate_ledger / AsRawFd` import,并为兼容重导出添加局部 `#[allow(unused_imports)]`;兼容出口未删除,警告总数仍为 `18`。测试子模块不继承父模块 `use`,后续拆分必须显式补 import;并行写入期禁止全 crate 编译,统一门禁只能在四个 Agent 全部完成后的稳定共享树运行。
- 第三轮稳定树已通过 `cargo fmt --check``cargo check``cargo check --tests`,以及 `tool_plan_handoff 44/44``swarm_cli 35/35``browser 21 passed / 3 real Chrome ignored`Linux 串行全量为 `1146 passed / 5 ignored / 0 failed`。确定性真实 Runner + Chrome E2E 为 **PASS**Provider lifecycle `17/17`、revision `0 -> 2`、固定试玩 `37/37`,残留与泄漏均为 `0`。Windows cross check 因宿主缺少 `x86_64-w64-mingw32-gcc`,在进入项目代码前停止,仍是明确的残余验证缺口。
- 2026-07-22 的 V1.45 修复专业 Agent 并行修改导致的 stale pending 中断。执行前发现 project revision 漂移不再进入 `needs-reconciliation`,而是以 `blocked + projectRevisionDrift + replanRequired` observation 回到同一 run 继续 planning`file.write / file.patch` 还会在项目写锁内复核 pending 身份、仓库上下文、revision 与 verification gate。该语义不放宽并发写安全:旧动作始终不执行,副作用未知、账本冲突或持久化损坏仍失败关闭。
- V1.45 的独立真实 external-provider 验收只向 `--swarm-chat --init --autonomous-game-build` 写入一次“植物大战僵尸式塔防”需求后立即 EOF。最终 `status=PASS / turn.report=settled`approve / answer / steer 为 `0`;两条初始 delivery 与一条 repair delivery 均被父 run 认领,项目 revision `0 -> 8``game/index.html``7814` bytesstatic smoke、desktop / mobile 浏览器和 `lane-defense-v1 37/37` 全通过。唯一 Supervisor assistant 已提交,pending、confirmation、user-input、provider retry/handoff、tool-plan handoff、finalization、reconciliation、重复和全部隐私泄漏计数均为 `0`
- V1.45 稳定树同时通过三个新增/更新定向回归、`revision 32/32``cargo check --tests`、Linux 串行全量 `1147 passed / 5 ignored / 0 failed``cargo fmt --check`、encoding 与 `git diff --check`。确定性正式 E2E 继续为 **PASS**Provider lifecycle `17/17`、revision `0 -> 2`、Chrome `37/37`,终局残留与泄漏均为 `0`