澄清转述那一轮的 context bundle 不再被判身份不匹配
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Successful in 4m2s
Project CI / Native shell tests (pull_request) Successful in 18m9s

走过一轮澄清的立项策划必挂:策划子 Agent 完成、父 run 醒来认领委派回执时,整轮报
「恢复 Agent Runtime context bundle 失败:身份与当前任务不匹配」,用户答案已落盘却
无法继续。

bundle.task 存的是写出这份 bundle 的那一轮的任务,跟 run 的持久 task 是两个独立参数
(build_game_creator_agent_runtime_context_bundle 的 task 形参)。用户答完澄清后整条
run 改跑在转述任务上(run_recovered_game_creator_context_on_fresh_task 传的就是
pending.task),那一轮写出的 bundle 因此带着转述文本,而 task record 与 current_task
始终是用户原始请求。实测项目里两侧身份字段全等,只有 task 一项分叉。

validate_agent_runtime_pending_context 早就为 pending 入口豁免过同一条文本相等断言,
并留了注释说明转述是唯一一处 task 不等于本 run 任务的场景;bundle 这个入口漏了。这里
补上同一个豁免,只放开文本相等一条——project/agent/task_id/session/run/source/profile/
绑定指纹与 verification_gate 三项照旧全等,转述文本也只能由 Runtime 在本 run 内生成,
不放开任何跨 run 或跨身份的重放面。

新增用例同时断言两侧:转述 bundle 必须能读回,其它文本分叉照旧 fail closed。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 07:32:25 +00:00
parent 2a44169ddb
commit 65c3d172b2
2 changed files with 70 additions and 1 deletions
@@ -486,6 +486,21 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_context_bundle_with_supe
bundle.run_profile_binding_fingerprint = runtime.run_profile_binding_fingerprint.clone();
bundle = sanitize_game_creator_agent_runtime_context_bundle(root, &bundle);
}
// `bundle.task` 存的是**写出这份 bundle 的那一轮**的任务,和 run 的持久 task 是两个
// 独立参数(build_game_creator_agent_runtime_context_bundle 的 `task` 形参)。澄清转述
// 是唯一一处二者必然不同的场景:用户答完后整条 run 改跑在转述任务上
// run_recovered_game_creator_context_on_fresh_task 传的就是 pending.task),那一轮
// 写出的 bundle 因此带着转述文本,而 task record 与 current_task 始终是用户原始请求。
//
// validate_agent_runtime_pending_context 已经为 pending 入口豁免过同一条文本相等断言,
// 但 bundle 这个入口漏了:D11 实测走过一轮澄清后,策划子 Agent 完成、父 run 醒来认领
// 委派回执时必定在这里判失败,用户答案已落盘却整轮报「身份与当前任务不匹配」。
//
// 豁免同样只放开文本相等这一条。project/agent/task_id/session/run/source/profile/绑定
// 指纹和 verification_gate 三项在上下都照旧全等,转述文本本身也只能由 Runtime 在本 run
// 内生成,所以不放开任何跨 run 或跨身份的重放面。
let bundle_task_is_clarification_relay =
agent_runtime_task_is_delegate_clarification_relay(&bundle.task);
if bundle.project_id != game_creator_agent_runtime_context_project_id(root)?
|| bundle.agent_id != redact_agent_runtime_project_paths(root, &runtime.agent_id, 160)
|| bundle.task_id != redact_agent_runtime_project_paths(root, &runtime.task_id, 160)
@@ -494,7 +509,8 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_context_bundle_with_supe
|| bundle.source != redact_agent_runtime_project_paths(root, &runtime.source, 120)
|| bundle.run_profile != runtime.run_profile
|| bundle.run_profile_binding_fingerprint != runtime.run_profile_binding_fingerprint
|| validate_agent_runtime_context_task_parameter(root, runtime, &bundle.task).is_err()
|| (!bundle_task_is_clarification_relay
&& validate_agent_runtime_context_task_parameter(root, runtime, &bundle.task).is_err())
|| bundle.verification_gate.project_id != bundle.project_id
|| bundle.verification_gate.agent_id != bundle.agent_id
|| bundle.verification_gate.run_id != bundle.run_id
@@ -1971,6 +1971,59 @@ fn legacy_context_and_pending_records_fail_closed() {
fs::remove_dir_all(root).ok();
}
/// 澄清转述那一轮写出的 bundle 必须能被父 run 唤醒时读回来。
///
/// 用户答完澄清后整条 run 改跑在转述任务上,那一轮的 bundle 因此带着转述文本,而 run 的
/// 持久 task 始终是用户原始请求。validate_agent_runtime_pending_context 早就为 pending 入口
/// 豁免过同一条文本相等断言,bundle 入口漏了——D11 走过一轮澄清后,策划子 Agent 完成、父
/// run 认领委派回执时必定在这里失败。豁免只放开转述这一种 task,别的文本照旧 fail closed。
#[test]
fn agent_runtime_context_bundle_accepts_only_the_clarification_relay_task_divergence() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "澄清转述上下文项目").expect("project init");
let task = "贪吃蛇";
let state = start_game_creator_agent_runtime_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
task,
"plan-clarification-relay-run",
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
"等待澄清回答",
vec!["回答澄清后续跑".to_string()],
)
.expect("start clarification runtime state");
let context_tracker = AgentRuntimeContextWindowTracker::default();
let relay_task = format!(
"{AGENT_RUNTIME_DELEGATE_CLARIFICATION_TASK_PREFIX}delegation-96db734b07f794a576ac5c99questionCount=1 · questionsSha256=ef5b1201。请回答以下问题。"
);
let relay_bundle = build_game_creator_agent_runtime_context_bundle(
&root,
&state,
&relay_task,
&AgentRuntimeToolPlan::default(),
&[],
0,
&context_tracker,
)
.expect("build clarification relay context bundle");
write_game_creator_agent_runtime_context_bundle(&root, &relay_bundle)
.expect("write clarification relay bundle");
let restored = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect("clarification relay bundle must restore")
.expect("clarification relay bundle present");
assert_eq!(restored.task, relay_task);
let mut unrelated_task = relay_bundle;
unrelated_task.task = "另一个不相干的任务".to_string();
write_game_creator_agent_runtime_context_bundle(&root, &unrelated_task)
.expect("write unrelated-task bundle");
let error = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("非转述任务的文本分叉必须照旧 fail closed");
assert!(error.contains("身份与当前任务不匹配"), "{error}");
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_context_bundle_rejects_task_source_and_loop_identity_mismatches() {
let root = unique_project_path();