清理立项策划运行时死代码
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Successful in 3m26s
Project CI / Native shell tests (pull_request) Successful in 14m4s

删除未使用的 planning storage 加锁 wrapper 和未接入授权层

删除旧 provider binding 与 approval fingerprint 入口

修正测试 fixture 直接调用 _locked 核心实现

修复非 Unix 分支不可达返回
This commit is contained in:
2026-08-24 08:23:04 +00:00
parent 1c6a95aa1a
commit 7057daeb82
6 changed files with 110 additions and 369 deletions
@@ -2102,7 +2102,7 @@ mod pending_recovery_tests {
updated_at_utc: "2026-08-14T00:00:00.000Z".to_string(),
};
session.session_fingerprint = plan_session_fingerprint(&session).expect("session fp");
write_plan_session_atomic(root, &session).expect("write planning session");
write_plan_session_atomic_locked(root, &session).expect("write planning session");
let action = AgentRuntimeToolAction {
tool: PLAN_SUBMIT_GDD_TOOL.to_string(),
@@ -2341,8 +2341,13 @@ mod orphaned_external_generation_recovery_tests {
)
.expect("build committed GDD");
let gdd_bytes = canonical_plan_gdd_bytes(&gdd).expect("canonical committed GDD");
durable_create_json_no_replace(root, ".agent/planning/gdd.v1.json", &gdd_bytes, "GDD")
.expect("persist committed GDD");
durable_create_json_no_replace_locked(
root,
".agent/planning/gdd.v1.json",
&gdd_bytes,
"GDD",
)
.expect("persist committed GDD");
assert_eq!(
read_plan_gdd_chain(root)
.expect("read committed GDD chain")
@@ -2384,7 +2389,7 @@ mod orphaned_external_generation_recovery_tests {
.error
.as_deref()
.is_some_and(|error| error.contains("恢复锚点同时缺失")));
assert!(read_plan_gdd_approval_pending(root)
assert!(read_plan_gdd_approval_pending_locked(root)
.expect("read planning approval pending")
.is_none());
let audit_count = read_agent_db_records_bounded(root, 1024 * 1024)
@@ -2403,7 +2408,7 @@ mod orphaned_external_generation_recovery_tests {
);
let _ = resume_game_creator_agent_background_tasks_at(root)
.expect("second recovery scan may surface the existing reconciliation state");
assert!(read_plan_gdd_approval_pending(root)
assert!(read_plan_gdd_approval_pending_locked(root)
.expect("reread planning approval pending")
.is_none());
let audit_count_after_second_scan = read_agent_db_records_bounded(root, 1024 * 1024)
@@ -619,7 +619,7 @@ mod tests {
updated_at_utc: current_plan_timestamp_utc(),
};
session.session_fingerprint = plan_session_fingerprint(&session).expect("session fp");
write_plan_session_atomic(&root, &session).expect("write usage session");
write_plan_session_atomic_locked(&root, &session).expect("write usage session");
UsageFixture {
_temporary: temporary,
root,
@@ -1498,32 +1498,6 @@ fn validate_approval_response_id(value: &str) -> Result<(), PlanningStorageError
validate_uuid_prefixed(value, "gdd-response-", "responseId")
}
pub(crate) fn plan_gdd_approval_decision_fingerprint(
input: &PlanGddApprovalDecisionInputV1,
normalized_comment: Option<&str>,
) -> Result<String, PlanningStorageError> {
typed_serde_fingerprint(
PLAN_GDD_APPROVAL_DECISION_FINGERPRINT_DOMAIN,
&PlanGddApprovalDecisionFingerprintValue {
project_id: &input.project_id,
gdd_id: &input.gdd_id,
version: input.version,
fingerprint: &input.fingerprint,
pending_action_id: &input.pending_action_id,
action_fingerprint: &input.action_fingerprint,
approval_request_id: &input.approval_request_id,
response_id: &input.response_id,
source: PLAN_GDD_APPROVAL_SOURCE,
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD,
run_profile_binding_fingerprint: "",
session_id: "",
run_id: "",
action: &input.action,
normalized_comment,
},
)
}
pub(crate) fn plan_gdd_approval_decision_fingerprint_for_identity(
input: &PlanGddApprovalDecisionInputV1,
source: &str,
@@ -3112,20 +3086,6 @@ fn is_recoverable_index_projection_error(error: &PlanningStorageError) -> bool {
)
}
/// Read the derived index and repair a missing, malformed, or stale primary
/// from the validated GDD lineage while holding the project lock. The index
/// is never used to invent authority: if no GDD fact exists, an absent index
/// is returned as `None`, while an index left behind without a GDD is a hard
/// reconciliation failure.
pub(crate) fn read_plan_gdd_index_with_recovery(
root: &Path,
rebuilt_at_utc: &str,
) -> Result<Option<PlanGddIndexV1>, PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.index.read")
.map_err(|error| io_error("读取 planning index 时取得项目锁失败", error))?;
read_plan_gdd_index_with_recovery_locked(root, rebuilt_at_utc)
}
pub(crate) fn read_plan_gdd_index_with_recovery_locked(
root: &Path,
rebuilt_at_utc: &str,
@@ -3309,18 +3269,6 @@ pub(crate) fn read_plan_gdd_chain_locked(
Ok(values)
}
/// Read the immutable approval receipts. Only exact `vN.json` entries are
/// facts; a malformed entry that claims the reserved prefix is rejected rather
/// than silently ignored. The caller is responsible for reconciling each
/// receipt against the corresponding GDD lineage.
pub(crate) fn read_plan_gdd_approvals(
root: &Path,
) -> Result<Vec<PlanGddApprovalV1>, PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.read-approvals")
.map_err(|error| io_error("读取 planning approval receipts 时取得项目锁失败", error))?;
read_plan_gdd_approvals_locked(root)
}
pub(crate) fn read_plan_gdd_approvals_locked(
root: &Path,
) -> Result<Vec<PlanGddApprovalV1>, PlanningStorageError> {
@@ -4042,29 +3990,15 @@ fn verify_planning_hardlink_publish_identity(
format!("{label} fallback 发布前 temp/target 文件身份不一致"),
));
}
Ok(())
}
#[cfg(not(unix))]
{
return Err(PlanningStorageError::new(
Err(PlanningStorageError::new(
"PLAN_UNSUPPORTED_PLATFORM",
format!("{label} fallback 无法证明 temp/target 文件身份"),
));
))
}
Ok(())
}
/// Publish an immutable planning JSON file without replacing an existing
/// target. A byte-identical target is an idempotent replay; every other
/// existing target is an identity conflict.
pub(crate) fn durable_create_json_no_replace(
root: &Path,
relative_path: &str,
bytes: &[u8],
label: &str,
) -> Result<PlanningCreateOutcome, PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.create")
.map_err(|error| io_error("取得 planning create 项目锁失败", error))?;
durable_create_json_no_replace_locked(root, relative_path, bytes, label)
}
pub(crate) fn durable_create_json_no_replace_locked(
@@ -4177,14 +4111,6 @@ pub(crate) fn durable_create_json_no_replace_locked(
Ok(outcome)
}
pub(crate) fn read_plan_gdd_approval_pending(
root: &Path,
) -> Result<Option<PlanGddApprovalPendingV1>, PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.approval-pending.read")
.map_err(|error| io_error("读取 GDD approval pending 时取得项目锁失败", error))?;
read_plan_gdd_approval_pending_locked(root)
}
pub(crate) fn read_plan_gdd_approval_pending_locked(
root: &Path,
) -> Result<Option<PlanGddApprovalPendingV1>, PlanningStorageError> {
@@ -4199,18 +4125,6 @@ pub(crate) fn read_plan_gdd_approval_pending_locked(
}
}
/// The pending file is a replaceable projection. It is written atomically,
/// but unlike a receipt it may be advanced from awaiting to observed or
/// rebuilt after a crash. Equal bytes are treated as an idempotent replay.
pub(crate) fn write_plan_gdd_approval_pending_atomic(
root: &Path,
value: &PlanGddApprovalPendingV1,
) -> Result<(), PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.approval-pending.write")
.map_err(|error| io_error("写入 GDD approval pending 时取得项目锁失败", error))?;
write_plan_gdd_approval_pending_atomic_locked(root, value)
}
pub(crate) fn write_plan_gdd_approval_pending_atomic_locked(
root: &Path,
value: &PlanGddApprovalPendingV1,
@@ -4266,24 +4180,9 @@ pub(crate) fn remove_plan_gdd_approval_pending_locked(
Ok(())
}
pub(crate) fn remove_plan_gdd_approval_pending(root: &Path) -> Result<(), PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.approval-pending.remove")
.map_err(|error| io_error("清理 GDD approval pending 时取得项目锁失败", error))?;
remove_plan_gdd_approval_pending_locked(root)
}
/// Rebuild and atomically replace the derived GDD index. The index is not an
/// immutable fact: a new GDD version must replace it, while a corrupt or
/// missing index can always be rebuilt from the authoritative GDD chain.
pub(crate) fn write_plan_gdd_index_atomic(
root: &Path,
value: &PlanGddIndexV1,
) -> Result<(), PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.index")
.map_err(|error| io_error("取得 planning index 项目锁失败", error))?;
write_plan_gdd_index_atomic_locked(root, value)
}
pub(crate) fn write_plan_gdd_index_atomic_locked(
root: &Path,
value: &PlanGddIndexV1,
@@ -4342,19 +4241,6 @@ pub(crate) fn write_plan_gdd_index_atomic_locked(
result
}
/// Atomically publish the human-readable Fast GDD projection. This is a
/// Runtime-owned projection writer, deliberately separate from the generic
/// `file.write` gate (which rejects this path for planning Agents). The
/// caller must hold the project write lock when using the `_locked` variant.
pub(crate) fn write_plan_fast_gdd_markdown_atomic(
root: &Path,
markdown: &str,
) -> Result<(), PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.fast-gdd")
.map_err(|error| io_error("取得 Fast GDD 投影项目锁失败", error))?;
write_plan_fast_gdd_markdown_atomic_locked(root, markdown)
}
pub(crate) fn write_plan_fast_gdd_markdown_atomic_locked(
root: &Path,
markdown: &str,
@@ -4516,19 +4402,6 @@ fn verify_replace_target_is_safe(target: &Path, label: &str) -> Result<(), Plann
}
}
/// Atomically advance `session.json`, retaining exactly one `.session.previous`
/// recovery copy. Callers should hold the project write lock; the public
/// wrapper acquires it for standalone/tests and the `_locked` variant is used
/// by Runtime code that already owns the lock.
pub(crate) fn write_plan_session_atomic(
root: &Path,
value: &PlanSessionV1,
) -> Result<(), PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.session")
.map_err(|error| io_error("取得 planning session 项目锁失败", error))?;
write_plan_session_atomic_locked(root, value)
}
pub(crate) fn write_plan_session_atomic_locked(
root: &Path,
value: &PlanSessionV1,
@@ -4834,50 +4707,6 @@ pub(crate) fn read_plan_session_read_only_locked(
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct PlanningRuntimeWriteIdentity<'a> {
pub(crate) agent_id: &'a str,
pub(crate) source: &'a str,
pub(crate) run_profile: &'a str,
pub(crate) parent_agent_id: Option<&'a str>,
}
pub(crate) fn validate_planning_runtime_write_identity(
identity: &PlanningRuntimeWriteIdentity<'_>,
) -> Result<(), PlanningStorageError> {
if identity.agent_id != "project-planning"
|| identity.source != "agent-delegate"
|| identity.run_profile != "standard"
|| identity.parent_agent_id != Some("project-supervisor")
{
return Err(PlanningStorageError::new(
"PLAN_WRITE_AUTHORIZATION_DENIED",
"只有 project-planning/agent-delegate/standard/Supervisor 子 Run 可以申请规划 Runtime 写入",
));
}
Ok(())
}
pub(crate) fn durable_create_json_no_replace_authorized(
root: &Path,
identity: &PlanningRuntimeWriteIdentity<'_>,
relative_path: &str,
bytes: &[u8],
label: &str,
) -> Result<PlanningCreateOutcome, PlanningStorageError> {
validate_planning_runtime_write_identity(identity)?;
durable_create_json_no_replace(root, relative_path, bytes, label)
}
pub(crate) fn write_plan_session_atomic_authorized(
root: &Path,
identity: &PlanningRuntimeWriteIdentity<'_>,
value: &PlanSessionV1,
) -> Result<(), PlanningStorageError> {
validate_planning_runtime_write_identity(identity)?;
write_plan_session_atomic(root, value)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -5196,24 +5025,24 @@ mod tests {
let path = ".agent/planning/gdd.v1.json";
let malformed_root = tempfile::tempdir().expect("malformed root");
assert_eq!(
durable_create_json_no_replace(malformed_root.path(), path, br"{}", "GDD",)
durable_create_json_no_replace_locked(malformed_root.path(), path, br"{}", "GDD",)
.unwrap_err()
.code(),
"PLAN_INVALID_JSON"
);
assert_eq!(
durable_create_json_no_replace(root, path, &bytes, "GDD").expect("first create"),
durable_create_json_no_replace_locked(root, path, &bytes, "GDD").expect("first create"),
PlanningCreateOutcome::Created
);
assert_eq!(
durable_create_json_no_replace(root, path, &bytes, "GDD").expect("replay"),
durable_create_json_no_replace_locked(root, path, &bytes, "GDD").expect("replay"),
PlanningCreateOutcome::Replayed
);
let mut changed = bytes.clone();
let changed_index = changed.len() - 2;
changed[changed_index] ^= 1;
assert_eq!(
durable_create_json_no_replace(root, path, &changed, "GDD")
durable_create_json_no_replace_locked(root, path, &changed, "GDD")
.unwrap_err()
.code(),
"PLAN_INVALID_JSON"
@@ -5224,27 +5053,6 @@ mod tests {
assert!(delete_local_project_file_at(root, "game/fast_gdd.md").is_err());
}
#[test]
fn planning_write_identity_is_fail_closed() {
let valid = PlanningRuntimeWriteIdentity {
agent_id: "project-planning",
source: "agent-delegate",
run_profile: "standard",
parent_agent_id: Some("project-supervisor"),
};
assert!(validate_planning_runtime_write_identity(&valid).is_ok());
let invalid_identity = PlanningRuntimeWriteIdentity {
agent_id: "project-supervisor",
..valid
};
assert_eq!(
validate_planning_runtime_write_identity(&invalid_identity)
.unwrap_err()
.code(),
"PLAN_WRITE_AUTHORIZATION_DENIED"
);
}
#[test]
fn submit_input_has_strict_canonical_parser_and_runtime_field_boundary() {
let value = golden_submit_input();
@@ -5273,8 +5081,13 @@ mod tests {
let root = directory.path();
let first = golden_gdd();
let first_bytes = canonical_plan_gdd_bytes(&first).expect("first bytes");
durable_create_json_no_replace(root, ".agent/planning/gdd.v1.json", &first_bytes, "GDD")
.expect("write first GDD");
durable_create_json_no_replace_locked(
root,
".agent/planning/gdd.v1.json",
&first_bytes,
"GDD",
)
.expect("write first GDD");
let mut second = first.clone();
second.version = 2;
second.submission_id = "action-abcdefabcdefabcdefabcdef".to_string();
@@ -5283,8 +5096,13 @@ mod tests {
second.action_fingerprint = "4".repeat(64);
second.fingerprint = plan_gdd_fingerprint(&second).expect("second fingerprint");
let second_bytes = canonical_plan_gdd_bytes(&second).expect("second bytes");
durable_create_json_no_replace(root, ".agent/planning/gdd.v2.json", &second_bytes, "GDD")
.expect("write second GDD");
durable_create_json_no_replace_locked(
root,
".agent/planning/gdd.v2.json",
&second_bytes,
"GDD",
)
.expect("write second GDD");
let chain = read_plan_gdd_chain(root).expect("read chain");
assert_eq!(chain, vec![first.clone(), second.clone()]);
let index = build_plan_gdd_index(&chain, "2026-08-10T00:00:00.000Z").expect("index");
@@ -5316,15 +5134,20 @@ mod tests {
);
let index_bytes = canonical_plan_index_bytes(&index).expect("index bytes");
assert_eq!(
durable_create_json_no_replace(root, PLAN_GDD_INDEX_PATH, &index_bytes, "GDD index")
.unwrap_err()
.code(),
durable_create_json_no_replace_locked(
root,
PLAN_GDD_INDEX_PATH,
&index_bytes,
"GDD index"
)
.unwrap_err()
.code(),
"PLAN_DEDICATED_WRITER_REQUIRED"
);
write_plan_gdd_index_atomic(root, &index).expect("write index");
write_plan_gdd_index_atomic_locked(root, &index).expect("write index");
let mut rebuilt = index.clone();
rebuilt.rebuilt_at_utc = "2026-08-11T00:00:00.000Z".to_string();
write_plan_gdd_index_atomic(root, &rebuilt).expect("replace index");
write_plan_gdd_index_atomic_locked(root, &rebuilt).expect("replace index");
assert_eq!(
parse_plan_index_bytes(
&fs::read(
@@ -5337,7 +5160,7 @@ mod tests {
);
fs::remove_file(root.join(PLAN_GDD_INDEX_PATH.replace('/', std::path::MAIN_SEPARATOR_STR)))
.expect("remove index for recovery");
let recovered = read_plan_gdd_index_with_recovery(root, "2026-08-12T00:00:00.000Z")
let recovered = read_plan_gdd_index_with_recovery_locked(root, "2026-08-12T00:00:00.000Z")
.expect("recover missing index")
.expect("recovered index");
assert_eq!(recovered.entries, index.entries);
@@ -5348,9 +5171,10 @@ mod tests {
br"{}",
)
.expect("corrupt index");
let recovered_corrupt = read_plan_gdd_index_with_recovery(root, "2026-08-13T00:00:00.000Z")
.expect("recover corrupt index")
.expect("recovered corrupt index");
let recovered_corrupt =
read_plan_gdd_index_with_recovery_locked(root, "2026-08-13T00:00:00.000Z")
.expect("recover corrupt index")
.expect("recovered corrupt index");
assert_eq!(recovered_corrupt.entries, index.entries);
assert_eq!(recovered_corrupt.rebuilt_at_utc, "2026-08-13T00:00:00.000Z");
assert!(build_plan_gdd_index(&[], "2026-08-10T00:00:00.000Z").is_err());
@@ -5363,12 +5187,17 @@ mod tests {
let first = golden_session();
let first_bytes = canonical_plan_session_bytes(&first).expect("session bytes");
assert_eq!(
durable_create_json_no_replace(root, PLAN_SESSION_PATH, &first_bytes, "plan session")
.unwrap_err()
.code(),
durable_create_json_no_replace_locked(
root,
PLAN_SESSION_PATH,
&first_bytes,
"plan session"
)
.unwrap_err()
.code(),
"PLAN_DEDICATED_WRITER_REQUIRED"
);
write_plan_session_atomic(root, &first).expect("write session v1");
write_plan_session_atomic_locked(root, &first).expect("write session v1");
let mut second = first.clone();
second.session_revision = 2;
second.previous_fingerprint = Some(first.session_fingerprint.clone());
@@ -5376,7 +5205,7 @@ mod tests {
second.active_run_id = None;
second.phase = "awaiting_user_input".to_string();
second.session_fingerprint = plan_session_fingerprint(&second).expect("v2 fingerprint");
write_plan_session_atomic(root, &second).expect("write session v2");
write_plan_session_atomic_locked(root, &second).expect("write session v2");
assert!(root.join(PLAN_SESSION_PREVIOUS_PATH).exists());
assert_eq!(
read_plan_session_with_recovery(root).expect("recover session"),
@@ -5388,7 +5217,7 @@ mod tests {
invalid_next.previous_fingerprint = Some(second.session_fingerprint.clone());
invalid_next.session_fingerprint = plan_session_fingerprint(&invalid_next).expect("bad fp");
assert_eq!(
write_plan_session_atomic(root, &invalid_next)
write_plan_session_atomic_locked(root, &invalid_next)
.unwrap_err()
.code(),
"PLAN_IDENTITY_CONFLICT"
@@ -5452,7 +5281,7 @@ mod tests {
forged_collecting.session_fingerprint =
plan_session_fingerprint(&forged_collecting).expect("recompute forged fingerprint");
assert_eq!(
write_plan_session_atomic(root, &forged_collecting)
write_plan_session_atomic_locked(root, &forged_collecting)
.expect_err("runtime boundary must reject an unrelated latest delegation")
.code(),
"PLAN_NEEDS_RECONCILIATION"
@@ -5554,7 +5383,7 @@ mod tests {
plan_session_fingerprint(&session).expect("recompute forged fingerprint");
validate_plan_session(&session).expect("standalone session shape remains valid");
assert_eq!(
write_plan_session_atomic(root, &session)
write_plan_session_atomic_locked(root, &session)
.expect_err("quality repair edge must clear old applied answers")
.code(),
"PLAN_IDENTITY_CONFLICT"
@@ -5566,7 +5395,7 @@ mod tests {
let directory = tempfile::tempdir().expect("temp root");
let root = directory.path();
let first = golden_session();
write_plan_session_atomic(root, &first).expect("write session");
write_plan_session_atomic_locked(root, &first).expect("write session");
let primary_path = root.join(PLAN_SESSION_PATH.replace('/', std::path::MAIN_SEPARATOR_STR));
let previous_path =
root.join(PLAN_SESSION_PREVIOUS_PATH.replace('/', std::path::MAIN_SEPARATOR_STR));
@@ -5597,7 +5426,7 @@ mod tests {
second.active_run_id = None;
second.phase = "awaiting_user_input".to_string();
second.session_fingerprint = plan_session_fingerprint(&second).expect("second fp");
write_plan_session_atomic(root, &second).expect("write successor");
write_plan_session_atomic_locked(root, &second).expect("write successor");
let mut forked_previous = first.clone();
forked_previous.updated_at_utc = "2026-08-11T00:00:00.000Z".to_string();
forked_previous.session_fingerprint =
@@ -5626,9 +5455,14 @@ mod tests {
ensure_planning_parent(&symlink_path).expect("planning dir");
symlink(&outside, &symlink_path).expect("symlink");
assert_eq!(
durable_create_json_no_replace(root, ".agent/planning/gdd.v1.json", &bytes, "GDD")
.unwrap_err()
.code(),
durable_create_json_no_replace_locked(
root,
".agent/planning/gdd.v1.json",
&bytes,
"GDD"
)
.unwrap_err()
.code(),
"PLAN_UNTRUSTED_PATH"
);
@@ -5640,7 +5474,7 @@ mod tests {
ensure_planning_parent(&hardlink_path).expect("hardlink planning dir");
fs::hard_link(&hardlink_outside, &hardlink_path).expect("hardlink");
assert_eq!(
durable_create_json_no_replace(
durable_create_json_no_replace_locked(
hardlink_root,
".agent/planning/gdd.v1.json",
&bytes,
@@ -466,111 +466,6 @@ pub(crate) fn validate_plan_provider_session_binding(
Ok(())
}
/// Capture the source session before a Provider action becomes durable. The
/// action sidecar stores this value, so execution can reject a stale response
/// instead of reading and trusting whatever session happens to be current.
pub(crate) fn capture_plan_provider_session_binding(
root: &std::path::Path,
runtime: &AgentRuntimeState,
) -> Result<PlanProviderSessionBindingV1, String> {
let session = read_plan_session_with_recovery(root)
.map_err(|error| error.to_string())?
.ok_or_else(|| "planning submit 建立前缺少 durable session".to_string())?;
if session.project_id != game_creator_agent_runtime_context_project_id(root)?
|| session.agent_id != runtime.agent_id
|| session.source != runtime.source
|| session.run_profile != runtime.run_profile
|| session.run_profile_binding_fingerprint != runtime.run_profile_binding_fingerprint
|| session.session_id != runtime.session_id
|| session.active_run_id.as_deref() != Some(runtime.run_id.as_str())
|| session.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| session.applied_steer_cursor != runtime.applied_steer_cursor
{
return Err("planning submit action 建立时 session identity 不匹配".to_string());
}
let delegation_id = runtime
.delegation_id
.clone()
.ok_or_else(|| "planning submit action 建立时缺少 delegationId".to_string())?;
if !session.latest_delegation_id.is_empty() && session.latest_delegation_id != delegation_id {
return Err("planning submit action 建立时 delegation identity 已漂移".to_string());
}
let goal_snapshot_fingerprint = agent_goal_snapshot_fingerprint_for_state_at(root, runtime)?;
let request_context_fingerprint = format!(
"sha256-serde-json-v2:{:x}",
Sha256::digest(
format!(
"plan-provider-context\n{}\n{}\n{}\n{}\n{}",
runtime.agent_id,
runtime.run_id,
runtime.loop_iteration,
session.session_revision,
session.session_fingerprint
)
.as_bytes(),
)
);
let request_kind = "tool-plan";
let request_slot = format!("loop-{}-plan-submit", runtime.loop_iteration);
let provider_request_id = plan_provider_request_id(&PlanProviderRequestIdentityValue {
project_id: &session.project_id,
gdd_id: &session.gdd_id,
agent_id: &runtime.agent_id,
task_id: &runtime.task_id,
session_id: &session.session_id,
run_id: &runtime.run_id,
root_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
root_run_id: &session.root_run_id,
delegation_id: &delegation_id,
source: &runtime.source,
run_profile: &runtime.run_profile,
run_profile_binding_fingerprint: &runtime.run_profile_binding_fingerprint,
goal_id: runtime.goal_id.as_deref(),
goal_revision: runtime.goal_revision,
goal_snapshot_fingerprint: &goal_snapshot_fingerprint,
session_revision: session.session_revision,
session_fingerprint: &session.session_fingerprint,
applied_steer_cursor: runtime.applied_steer_cursor,
request_kind,
request_slot: &request_slot,
web_search_enabled: false,
request_context_fingerprint: &request_context_fingerprint,
})
.map_err(|error| error.to_string())?;
let binding = PlanProviderSessionBindingV1 {
schema_version: PLAN_PROVIDER_SESSION_BINDING_SCHEMA_VERSION.to_string(),
project_id: session.project_id,
gdd_id: session.gdd_id,
agent_id: runtime.agent_id.clone(),
task_id: runtime.task_id.clone(),
provider_request_id,
session_id: session.session_id,
run_id: runtime.run_id.clone(),
root_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
root_run_id: session.root_run_id,
delegation_id,
goal_id: runtime.goal_id.clone(),
goal_revision: runtime.goal_revision,
goal_snapshot_fingerprint,
source: runtime.source.clone(),
run_profile: runtime.run_profile.clone(),
run_profile_binding_fingerprint: runtime.run_profile_binding_fingerprint.clone(),
session_revision: session.session_revision,
session_fingerprint: session.session_fingerprint,
applied_steer_cursor: runtime.applied_steer_cursor,
request_kind: request_kind.to_string(),
request_slot,
web_search_enabled: false,
request_context_fingerprint,
fingerprint: String::new(),
};
let mut binding = binding;
binding.fingerprint =
plan_provider_session_binding_fingerprint(&binding).map_err(|error| error.to_string())?;
validate_plan_provider_session_binding(&binding).map_err(|error| error.to_string())?;
Ok(binding)
}
/// Capture the same planning source/session identity that was used to build a
/// concrete Provider request. Unlike the batch-only helper above, this
/// variant takes the immutable request snapshot, so retry attempts and repair
@@ -2352,7 +2247,7 @@ mod tests {
session.session_fingerprint = plan_session_fingerprint(&session).expect("session fp");
context.source_session_revision = session.session_revision;
context.source_session_fingerprint = session.session_fingerprint.clone();
write_plan_session_atomic(&root, &session).expect("write session fixture");
write_plan_session_atomic_locked(&root, &session).expect("write session fixture");
(root, context, input)
}
@@ -2453,20 +2348,26 @@ mod tests {
)
.expect("build committed GDD");
let gdd_bytes = canonical_plan_gdd_bytes(&gdd).expect("canonical GDD");
durable_create_json_no_replace(&root, ".agent/planning/gdd.v1.json", &gdd_bytes, "GDD")
.expect("write immutable GDD");
durable_create_json_no_replace_locked(
&root,
".agent/planning/gdd.v1.json",
&gdd_bytes,
"GDD",
)
.expect("write immutable GDD");
let index =
build_plan_gdd_index(&[gdd.clone()], &context.created_at_utc).expect("build GDD index");
write_plan_gdd_index_atomic(&root, &index).expect("write GDD index");
write_plan_gdd_index_atomic_locked(&root, &index).expect("write GDD index");
let markdown = render_plan_fast_gdd_markdown(&gdd, "ready_for_approval")
.expect("render Fast GDD Markdown");
write_plan_fast_gdd_markdown_atomic(&root, &markdown).expect("write Fast GDD Markdown");
write_plan_fast_gdd_markdown_atomic_locked(&root, &markdown)
.expect("write Fast GDD Markdown");
let source_session = read_plan_session_with_recovery(&root)
.expect("read source session")
.expect("source session exists");
let successor = build_submit_session_successor(&source_session, &context, &gdd)
.expect("build submitted session successor");
write_plan_session_atomic(&root, &successor).expect("write submitted session");
write_plan_session_atomic_locked(&root, &successor).expect("write submitted session");
let root_runtime =
read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
@@ -3858,7 +3759,7 @@ mod tests {
.expect("GDD exists");
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
let pending = read_plan_gdd_approval_pending(&root)
let pending = read_plan_gdd_approval_pending_locked(&root)
.expect("read approval pending")
.expect("pending exists");
assert_eq!(pending.status, "awaiting_decision");
@@ -3975,7 +3876,7 @@ mod tests {
)
.expect("disabled acceptance recovery is a no-op");
assert_eq!(outcome, PlanGddAcceptanceGateOutcome::NotApplicable);
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read pending")
.is_none());
@@ -4170,13 +4071,13 @@ mod tests {
)
.expect("commit receipt");
let mut pending = read_plan_gdd_approval_pending(&root)
let mut pending = read_plan_gdd_approval_pending_locked(&root)
.expect("read observed pending")
.expect("observed pending remains during recovery");
pending.run_identity.run_id = "run-other-001".to_string();
pending.pending_fingerprint =
plan_gdd_approval_pending_fingerprint(&pending).expect("recompute pending fingerprint");
write_plan_gdd_approval_pending_atomic(&root, &pending)
write_plan_gdd_approval_pending_atomic_locked(&root, &pending)
.expect("write mismatched projection");
let blocker = plan_gdd_completion_blocker_at_locked(
@@ -4367,7 +4268,7 @@ mod tests {
assert_eq!(first.decision_ref.action, action);
assert_eq!(first.approved_gdd_ref.is_some(), action == "approve");
let approvals = read_plan_gdd_approvals(&root).expect("read receipts");
let approvals = read_plan_gdd_approvals_locked(&root).expect("read receipts");
let index = build_plan_gdd_index_with_approvals(
&read_plan_gdd_chain(&root).expect("read GDD chain"),
&approvals,
@@ -4679,7 +4580,7 @@ mod tests {
assert_eq!(waiting_detail["approvalPending"], "not-created");
assert_eq!(waiting_detail["nextRequiredAction"], "agent.delegate");
assert_eq!(waiting_detail["repairOfDelegationId"], gdd.delegation_id);
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read pending before gate")
.is_none());
@@ -4703,7 +4604,7 @@ mod tests {
)
.expect("parse passed detail");
assert_eq!(passed_detail["approvalPending"], "created");
let pending = read_plan_gdd_approval_pending(&root)
let pending = read_plan_gdd_approval_pending_locked(&root)
.expect("read created pending")
.expect("pending exists");
assert_eq!(pending.gdd_ref.gdd_id, gdd.gdd_id);
@@ -4803,7 +4704,7 @@ mod tests {
assert!(after_claim_detail.contains("planGddAcceptanceGate"));
assert!(after_claim_detail.contains("\"nextRequiredAction\":\"agent.delegate\""));
assert!(after_claim_detail.contains(&gdd.delegation_id));
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read pending")
.is_none());
cleanup_fixture(root);
@@ -4829,7 +4730,7 @@ mod tests {
let detail = claim.detail.as_deref().expect("claim gate detail");
assert!(detail.contains("\"nextRequiredAction\":\"file.read\""));
assert!(!detail.contains("\"repairOfDelegationId\":\""));
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read pending before evidence")
.is_none());
cleanup_fixture(root);
@@ -4870,7 +4771,7 @@ mod tests {
.expect("parse passed detail");
assert_eq!(passed_detail["nextRequiredAction"], "agent.run_status");
assert_eq!(passed_detail["delegationId"], gdd.delegation_id);
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read pending before claim")
.is_none());
@@ -4893,7 +4794,7 @@ mod tests {
.as_deref()
.expect("claim detail")
.contains("\"approvalPending\":\"created\""));
let pending = read_plan_gdd_approval_pending(&root)
let pending = read_plan_gdd_approval_pending_locked(&root)
.expect("read pending after claim")
.expect("pending exists after claim");
assert_eq!(pending.gdd_ref.gdd_id, gdd.gdd_id);
@@ -4928,7 +4829,7 @@ mod tests {
}],
)
.expect("record passed graph without running gate");
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read missing pending")
.is_none());
@@ -4949,7 +4850,7 @@ mod tests {
let detail = replay.detail.as_deref().expect("run status detail");
assert!(!detail.contains("readyDelegateReceipts"));
assert!(detail.contains("\"approvalPending\":\"created\""));
let pending = read_plan_gdd_approval_pending(&root)
let pending = read_plan_gdd_approval_pending_locked(&root)
.expect("read replayed pending")
.expect("pending created after zero-ready replay");
assert_eq!(pending.gdd_ref.gdd_id, gdd.gdd_id);
@@ -4985,7 +4886,7 @@ mod tests {
)
.await;
assert_eq!(observation.status, "ok");
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read pending after dispatched poll")
.is_none());
cleanup_fixture(root);
@@ -5047,7 +4948,7 @@ mod tests {
}),
);
assert_eq!(observation.status, "rejected", "case={case}");
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read pending after rejected evidence")
.is_none());
cleanup_fixture(root);
@@ -5110,7 +5011,7 @@ mod tests {
.detail
.as_deref()
.is_some_and(|detail| detail.contains("从 startLine=1 开始分页")));
assert!(read_plan_gdd_approval_pending(&root)
assert!(read_plan_gdd_approval_pending_locked(&root)
.expect("read pending after old hash")
.is_none());
cleanup_fixture(root);
@@ -5160,7 +5061,7 @@ mod tests {
}),
);
assert_eq!(partial.status, "rejected");
assert!(read_plan_gdd_approval_pending(&partial_root)
assert!(read_plan_gdd_approval_pending_locked(&partial_root)
.expect("read partial pending")
.is_none());
cleanup_fixture(partial_root);
@@ -5218,7 +5119,7 @@ mod tests {
.detail
.as_deref()
.is_some_and(|detail| detail.contains("\"approvalPending\":\"created\"")));
let pending = read_plan_gdd_approval_pending(&complete_root)
let pending = read_plan_gdd_approval_pending_locked(&complete_root)
.expect("read complete pending")
.expect("complete coverage creates pending");
assert_eq!(pending.gdd_ref.version, gdd.version);
@@ -5372,16 +5273,16 @@ mod tests {
&root_runtime.run_id,
)
.expect("create initial pending");
let original = read_plan_gdd_approval_pending(&root)
let original = read_plan_gdd_approval_pending_locked(&root)
.expect("read initial pending")
.expect("initial pending exists");
remove_plan_gdd_approval_pending(&root).expect("remove pending for recovery window");
remove_plan_gdd_approval_pending_locked(&root).expect("remove pending for recovery window");
assert!(
reconcile_plan_gdd_approval_projections_at(&root).expect("reconcile acceptance gate"),
"recovery should repair the missing pending projection"
);
let recovered = read_plan_gdd_approval_pending(&root)
let recovered = read_plan_gdd_approval_pending_locked(&root)
.expect("read recovered pending")
.expect("recovered pending exists");
assert_eq!(recovered, original);
@@ -5393,7 +5294,7 @@ mod tests {
"second recovery must not rewrite an exact pending projection"
);
assert_eq!(
read_plan_gdd_approval_pending(&root)
read_plan_gdd_approval_pending_locked(&root)
.expect("read replayed pending")
.expect("pending survives replay"),
original
@@ -5439,7 +5340,7 @@ mod tests {
&root_runtime.run_id,
)
.expect("create approval pending");
let mut forged_pending = read_plan_gdd_approval_pending(&root)
let mut forged_pending = read_plan_gdd_approval_pending_locked(&root)
.expect("read awaiting pending")
.expect("awaiting pending exists");
@@ -5462,7 +5363,7 @@ mod tests {
});
forged_pending.pending_fingerprint = plan_gdd_approval_pending_fingerprint(&forged_pending)
.expect("fingerprint forged pending");
write_plan_gdd_approval_pending_atomic(&root, &forged_pending)
write_plan_gdd_approval_pending_atomic_locked(&root, &forged_pending)
.expect("write forged pending");
let error = ensure_plan_gdd_approval_pending_after_acceptance_at(
@@ -4776,7 +4776,7 @@ fn planning_clarification_user_revision_after_answer_preserves_round_for_revise_
plan_session_fingerprint(&awaiting_approval).expect("approval-wait fingerprint");
validate_plan_session_successor(&collecting, &awaiting_approval)
.expect("project approval-wait successor");
write_plan_session_atomic(&fixture.root, &awaiting_approval)
write_plan_session_atomic_locked(&fixture.root, &awaiting_approval)
.expect("persist approval-wait session");
let mut decided = awaiting_approval.clone();
@@ -4805,7 +4805,8 @@ fn planning_clarification_user_revision_after_answer_preserves_round_for_revise_
plan_session_fingerprint(&decided).expect("decision fingerprint");
validate_plan_session_successor(&awaiting_approval, &decided)
.expect("project revise/reject successor");
write_plan_session_atomic(&fixture.root, &decided).expect("persist revise/reject session");
write_plan_session_atomic_locked(&fixture.root, &decided)
.expect("persist revise/reject session");
mark_static_delegate_delivery_user_revision_requested_at(
&fixture.root,