修复:封闭 game-chat 美术重试写入边界
在 successor durable 写入前类型化拒绝动态美术 child 通用 retry 将遗留 retry-source 美术 run 收敛为只读并阻断全部 mutation 补齐终态零副作用、工具矩阵及 DAG/非美术 retry 非回归测试 同步技术方案、决策记录与踩坑说明
This commit is contained in:
@@ -772,6 +772,23 @@ pub(crate) fn resolve_game_creator_agent_runtime_retry_configuration_at(
|
||||
Ok((run_profile, source))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(in crate::agent) enum AgentRuntimeRetryErrorKind {
|
||||
GameChatDynamicArtRetryUnsupported,
|
||||
}
|
||||
|
||||
impl AgentRuntimeRetryErrorKind {
|
||||
pub(in crate::agent) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::GameChatDynamicArtRetryUnsupported => "game-chat-dynamic-art-retry-unsupported",
|
||||
}
|
||||
}
|
||||
|
||||
fn wire_error(self, message: &str) -> String {
|
||||
format!("kind={} {message}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn retry_game_creator_agent_runtime_task_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -787,6 +804,13 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
|
||||
let task =
|
||||
read_latest_game_creator_agent_runtime_task_by_run_id(root, &agent_id, &target_run_id)?
|
||||
.ok_or_else(|| format!("未找到 Agent Runtime 任务:{target_run_id}"))?;
|
||||
if game_chat_dynamic_art_child_structural_identity_at(root, &task)? {
|
||||
return Err(
|
||||
AgentRuntimeRetryErrorKind::GameChatDynamicArtRetryUnsupported.wire_error(
|
||||
"game-chat 动态美术 child 不支持通用 Agent Runtime retry;请回到当前 code-prototype,先完成 asset.list 审计,再按真实缺口建立新的 durable 美术委派",
|
||||
),
|
||||
);
|
||||
}
|
||||
if task.goal_id.is_some() {
|
||||
return Err("持久 Goal 任务不能使用普通 retry;请清理后创建新 Goal".to_string());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,161 @@
|
||||
use super::*;
|
||||
|
||||
fn game_chat_delegated_art_agent_is_assets_only_at(
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum GameChatDelegatedArtAgentMutationScope {
|
||||
Unrestricted,
|
||||
AssetsOnly,
|
||||
RetryLineageBlocked,
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at(
|
||||
root: &Path,
|
||||
task: &AgentRuntimeTaskRecord,
|
||||
) -> Result<bool, String> {
|
||||
if !matches!(task.agent_id.as_str(), "art-director" | "art-asset-plan")
|
||||
|| task.parent_agent_id.as_deref() != Some("code-prototype")
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(parent_run_id) = task
|
||||
.parent_run_id
|
||||
.as_deref()
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(child_binding) =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, &task.agent_id, &task.run_id)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(parent) = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
"code-prototype",
|
||||
parent_run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(parent_binding) =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, "code-prototype", parent_run_id)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(root_binding) = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&child_binding.root_agent_id,
|
||||
&child_binding.root_run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(root_task) = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
&root_binding.agent_id,
|
||||
&root_binding.run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
Ok(child_binding.agent_id == task.agent_id
|
||||
&& child_binding.run_id == task.run_id
|
||||
&& child_binding.source == task.source
|
||||
&& child_binding.profile == task.run_profile
|
||||
&& child_binding.binding_fingerprint == task.run_profile_binding_fingerprint
|
||||
&& child_binding.parent_agent_id == task.parent_agent_id
|
||||
&& child_binding.parent_run_id == task.parent_run_id
|
||||
&& child_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& child_binding.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& parent.agent_id == "code-prototype"
|
||||
&& parent.run_id == parent_run_id
|
||||
&& parent.source == "agent-ready-task-scheduler"
|
||||
&& parent.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& parent.parent_agent_id.as_deref() == Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
&& parent.parent_run_id.as_deref() == Some(child_binding.root_run_id.as_str())
|
||||
&& parent.delegation_id.is_none()
|
||||
&& parent_binding.agent_id == parent.agent_id
|
||||
&& parent_binding.run_id == parent.run_id
|
||||
&& parent_binding.source == parent.source
|
||||
&& parent_binding.profile == parent.run_profile
|
||||
&& parent_binding.binding_fingerprint == parent.run_profile_binding_fingerprint
|
||||
&& parent_binding.parent_agent_id == parent.parent_agent_id
|
||||
&& parent_binding.parent_run_id == parent.parent_run_id
|
||||
&& parent_binding.project_id == child_binding.project_id
|
||||
&& parent_binding.root_agent_id == child_binding.root_agent_id
|
||||
&& parent_binding.root_run_id == child_binding.root_run_id
|
||||
&& child_binding.parent_binding_fingerprint.as_deref()
|
||||
== Some(parent_binding.binding_fingerprint.as_str())
|
||||
&& root_binding.project_id == child_binding.project_id
|
||||
&& root_binding.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& root_binding.run_id == child_binding.root_run_id
|
||||
&& root_binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
&& root_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& root_binding.root_agent_id == root_binding.agent_id
|
||||
&& root_binding.root_run_id == root_binding.run_id
|
||||
&& root_binding.parent_agent_id.is_none()
|
||||
&& root_binding.parent_run_id.is_none()
|
||||
&& parent_binding.parent_binding_fingerprint.as_deref()
|
||||
== Some(root_binding.binding_fingerprint.as_str())
|
||||
&& root_task.agent_id == root_binding.agent_id
|
||||
&& root_task.run_id == root_binding.run_id
|
||||
&& root_task.source == root_binding.source
|
||||
&& root_task.run_profile == root_binding.profile
|
||||
&& root_task.run_profile_binding_fingerprint == root_binding.binding_fingerprint
|
||||
&& root_task.parent_agent_id.is_none()
|
||||
&& root_task.parent_run_id.is_none()
|
||||
&& root_task.delegation_id.is_none())
|
||||
}
|
||||
|
||||
fn game_chat_delegated_art_agent_mutation_scope_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
) -> Result<GameChatDelegatedArtAgentMutationScope, String> {
|
||||
let canonical_output = match agent_id {
|
||||
"art-director" => AGENT_RUNTIME_ART_SPEC_PATH,
|
||||
"art-asset-plan" => AGENT_RUNTIME_ART_SPRITESHEET_PATH,
|
||||
_ => return Ok(false),
|
||||
_ => return Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted),
|
||||
};
|
||||
if let Some(task) =
|
||||
read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)?
|
||||
{
|
||||
if task.source == "agent-delegate-retry"
|
||||
&& game_chat_dynamic_art_child_structural_identity_at(root, &task)?
|
||||
{
|
||||
return Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked);
|
||||
}
|
||||
}
|
||||
// `assets/**` is a broad write scope. Grant it only after the same
|
||||
// canonical-output authorization used by Canvas replacement has verified
|
||||
// the live root, main parent, child binding, durable delivery and audited
|
||||
// generate-missing route.
|
||||
game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at(
|
||||
root,
|
||||
agent_id,
|
||||
run_id,
|
||||
canonical_output,
|
||||
Ok(
|
||||
if game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at(
|
||||
root,
|
||||
agent_id,
|
||||
run_id,
|
||||
canonical_output,
|
||||
)? {
|
||||
GameChatDelegatedArtAgentMutationScope::AssetsOnly
|
||||
} else {
|
||||
GameChatDelegatedArtAgentMutationScope::Unrestricted
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn game_chat_dynamic_art_retry_mutation_block(tool: &str) -> AgentRuntimeToolObservation {
|
||||
AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "game-chat 动态美术 child 不支持通用 retry 写入".to_string(),
|
||||
detail: Some(
|
||||
"必须回到当前 code-prototype 先完成 asset.list 审计,再按真实缺口建立新的 durable 美术委派"
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn game_chat_art_child_tool_is_read_only(tool: &str) -> bool {
|
||||
matches!(
|
||||
tool,
|
||||
@@ -48,17 +182,26 @@ pub(in crate::agent) fn game_chat_delegated_art_agent_project_path_mutation_bloc
|
||||
tool: &str,
|
||||
path: &str,
|
||||
) -> Option<AgentRuntimeToolObservation> {
|
||||
match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id) {
|
||||
Ok(false) => None,
|
||||
Ok(true) if path == "assets" || path.starts_with("assets/") => None,
|
||||
Ok(true) => Some(AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "game-chat 临时美术 Agent 只能修改 assets/**".to_string(),
|
||||
detail: Some(format!(
|
||||
"path={path} · allowed=assets/** · parentAgentId=code-prototype"
|
||||
)),
|
||||
}),
|
||||
match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) {
|
||||
Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted) => None,
|
||||
Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked) => {
|
||||
Some(game_chat_dynamic_art_retry_mutation_block(tool))
|
||||
}
|
||||
Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly)
|
||||
if path == "assets" || path.starts_with("assets/") =>
|
||||
{
|
||||
None
|
||||
}
|
||||
Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly) => {
|
||||
Some(AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "game-chat 临时美术 Agent 只能修改 assets/**".to_string(),
|
||||
detail: Some(format!(
|
||||
"path={path} · allowed=assets/** · parentAgentId=code-prototype"
|
||||
)),
|
||||
})
|
||||
}
|
||||
Err(error) => Some(AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
@@ -100,21 +243,28 @@ pub(in crate::agent) fn game_chat_delegated_art_agent_input_mutation_block(
|
||||
});
|
||||
}
|
||||
}
|
||||
let assets_only = match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id)
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "无法校验 game-chat 临时美术 Agent 的写入边界".to_string(),
|
||||
detail: Some(sanitize_agent_runtime_text(&error, 240)),
|
||||
});
|
||||
}
|
||||
};
|
||||
if !assets_only || game_chat_art_child_tool_is_read_only(tool) {
|
||||
let mutation_scope =
|
||||
match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "无法校验 game-chat 临时美术 Agent 的写入边界".to_string(),
|
||||
detail: Some(sanitize_agent_runtime_text(&error, 240)),
|
||||
});
|
||||
}
|
||||
};
|
||||
if game_chat_art_child_tool_is_read_only(tool) {
|
||||
return None;
|
||||
}
|
||||
match mutation_scope {
|
||||
GameChatDelegatedArtAgentMutationScope::Unrestricted => return None,
|
||||
GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked => {
|
||||
return Some(game_chat_dynamic_art_retry_mutation_block(tool));
|
||||
}
|
||||
GameChatDelegatedArtAgentMutationScope::AssetsOnly => {}
|
||||
}
|
||||
let mut block_path = |path: &str| {
|
||||
game_chat_delegated_art_agent_project_path_mutation_block(
|
||||
root, agent_id, run_id, tool, path,
|
||||
@@ -169,9 +319,16 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block(
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id) {
|
||||
Ok(true) if path == "assets" || path.starts_with("assets/") => return None,
|
||||
Ok(true) => {
|
||||
match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) {
|
||||
Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked) => {
|
||||
return Some(game_chat_dynamic_art_retry_mutation_block(tool));
|
||||
}
|
||||
Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly)
|
||||
if path == "assets" || path.starts_with("assets/") =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly) => {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
@@ -181,7 +338,7 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block(
|
||||
)),
|
||||
});
|
||||
}
|
||||
Ok(false) => {}
|
||||
Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted) => {}
|
||||
Err(error) => {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-08-11 game-chat 动态美术 child 通用 retry 失败关闭
|
||||
|
||||
- 裁决:game-chat 动态美术 child(`art-director` / `art-asset-plan`,parent 为单主 `code-prototype`、root 绑定 `project-supervisor-game-chat` source)不允许走通用 Agent Runtime retry。retry 入口识别该类 child 后,在创建 successor run、delegationId、task journal 或其它 durable 副作用前直接返回类型化错误,引导回到 game-chat,由当前 `code-prototype` 先完成 `asset.list` 审计,再按真实缺口建立新的 durable 美术委派;新委派自然产生新的 delegationId 与 durable delivery,沿用现行严格 lineage 判定,不新增 delivery 换绑、续发或可变机制。
|
||||
- 结构性依据:delivery 的 delegationId 由父动作 ID 派生,且 targetRunId 绑定原 child run;通用 retry 铸造的 `retry:{旧run}:{新run}:{纳秒}` 身份在结构上不可能匹配任何现有 delivery。即使只圈住 retry 的写边界,其产出也没有合法消费者。让 retry 继承 lineage 必须引入可变 delivery、换绑或放宽 exact binding,与不可变事实和失败关闭方向冲突,明确不采纳。
|
||||
- 纵深防御:执行策略层把 `source=agent-delegate-retry` 的 game-chat 美术 run 视为“声称委派 lineage 但无法证明”,只允许现有诊断性只读白名单,全部可变工具 fail closed;不得因严格授权返回 false 而回落为普通美术 Agent 权限。首次 `agent-delegate` 与遗留/伪造 `agent-delegate-retry` 共用同一动态美术 lineage 分类入口,但严格 Canvas、route、delivery 授权 predicate 继续只接受现行可证明的首次委派,不为 retry 放宽。
|
||||
- 错误 wire:现有 Tauri retry 命令继续保持 `Result<AgentRuntimeResult, String>`,M0B-1 不修改命令 DTO。Rust 内部使用稳定的类型化 retry error kind,并在 String wire 中固定保留 `kind=game-chat-dynamic-art-retry-unsupported` 与公开恢复建议;测试同时断言 error kind、可见文本和零 successor durable 副作用。
|
||||
- 范围限定:仅限上述 game-chat 动态美术 child。完整 16 任务 DAG 的 children 走文件交接、不依赖 delegationId delivery,其通用 retry 语义不变;非美术委派与顶层任务 retry 亦不受影响。前端隐藏或置灰该类 child 的 retry 控件归 M0B-2 source-aware 投影收敛,在此之前由后端类型化错误兜底。
|
||||
- 实现边界:入口守卫落在 `runtime_driver/lifecycle_control.rs` 的 `retry_game_creator_agent_runtime_task_at`,必须使用不要求 child 仍为 running 的结构身份分类;`resolve_game_creator_agent_runtime_retry_configuration_at` 保持不变。纵深防御落在 `runtime_tools/file_ops.rs` 的动态美术分类入口;严格 lineage/Canvas 授权 predicate 本身不放宽。除这两处与对应测试外,不扩展 M0B-1 生产改动面。
|
||||
- 验证方式:终态 failed/cancelled 动态美术 child 的 retry 返回类型化错误且不产生新 run;遗留或伪造的 `agent-delegate-retry` 美术 run 对 `file.write`、`project.patchset`、`canvas.asset_generate` 及其它全部可变工具阻断;完整 DAG child、非美术委派和顶层 retry 非回归;只有当前 `code-prototype` 重新审计并创建新委派、且新 delivery 全链一致时,新的首次 `agent-delegate` 才恢复 `assets/**` 授权。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。
|
||||
|
||||
## 2026-08-11 固定 owner 产物验证与可玩验收分离
|
||||
|
||||
- 背景:真实新项目初始化后没有 `package.json`,默认 `game/index.html` 只是无 `<canvas>` 的占位页。`design-foundation`、`balance-seed`、`art-asset-plan`、`audio-asset-plan` 位于 `code-prototype` 上游,只负责策划、数值、美术清单和音频清单;若要求它们执行 `project.verify` 或 `game.static_smoke`,前者没有可执行合同,后者只能检查尚未生成的占位游戏并必然失败。曾在测试中预先写入 `fake_llm_game_draft()` 会把占位入口替换成可玩页面,从而掩盖这条真实新项目死锁。
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
- 关联:相关文件、文档、提交或 Issue
|
||||
```
|
||||
|
||||
## 严格 delegated 授权失败不能把 retry 回落为普通美术权限
|
||||
|
||||
- 现象:game-chat 动态美术 child 以通用 retry 创建 `source=agent-delegate-retry` 的新 run 后,严格 Canvas/delivery 授权只接受首次 `agent-delegate`,返回 false;调用方却把 false 解释为“不是受限动态美术”,使 retry 回落到普通 autonomous 美术权限并可能写入 `game/**`、`.agent/**`,调用 preview 或继续委派。
|
||||
- 原因:代码混用了“是否声称动态美术 lineage”和“是否已证明当前首次委派授权”两个事实。retry 使用新 run 和新 delegationId,但没有与之绑定的 durable delivery,结构上无法满足现行 exact lineage;授权失败应表示不可信候选,而不是普通 Agent。
|
||||
- 处理:game-chat 动态美术 child 在通用 retry 入队前以终态可用的结构身份分类并返回 `kind=game-chat-dynamic-art-retry-unsupported`,引导当前 `code-prototype` 重新 `asset.list` 后建立新委派。对遗留/伪造的 retry source,美术分类入口只允许诊断性只读工具,全部 mutation fail closed;严格首次委派 predicate、delivery 和 Canvas 凭证不换绑、不续发、不放宽。完整 DAG、非美术委派和顶层 retry 不受影响。
|
||||
- 验证:覆盖 failed/cancelled child 重试零 successor run、遗留 retry 的 file/patchset/Canvas/command/preview/再委派拒绝、新首次委派 delivery 全链一致后恢复 `assets/**`,并对完整 DAG 与非美术 retry 做非回归。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs`、`docs/project-memory/shared-memory/decision-log.md`。
|
||||
|
||||
## 不能用可玩游戏 smoke 验证 code-prototype 上游的固定文档产物
|
||||
|
||||
- 现象:真实新项目的 `design-foundation`、`balance-seed`、`art-asset-plan` 或 `audio-asset-plan` 已写完自己的固定文件,却始终无法收束;`project.verify` 因项目没有 `package.json` 不可用,`game.static_smoke` 又报告缺少活动 `<canvas>`。测试若先调用 `fake_llm_game_draft()`,同一路径却会“通过”。
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
- ready-task 启动活性:`background_task.queued`、`autonomous_ready_task.scheduled`、Runner heartbeat 或执行锁已移交都不等于 child 已启动。实际持有执行权的 Runner 必须在释放项目写锁后同步写入 child 的 running task、`turn.started` 与 started journal,再把已启动 state 和 per-Agent 执行锁交给已确认开始轮询的独立 execution worker;同步启动或 worker 接管失败时,要在仍持有执行锁期间依次把 child 和 manifest Graph 节点明确落为 failed,再释放锁并让 parent 收到调度错误。`autonomous_ready_task.scheduled` 只作诊断审计,其写入失败不能阻断 durable child 启动;external client 只 wake Runner,不在客户端抢占执行。Supervisor 进度卡通过 durable `startedAt`(旧 Run 从完整 task journal 恢复,最新 task-record fallback 保持 0)显示真实持续时间,并以父 Run 与当前关联专业 Agent 的最大事件时间计算运行态活跃度:运行超过 5 分钟无新事件时显示“运行中 · 疑似停滞”和静默时长;等待用户、等待确认、Provider retry、视觉资产、进程会话、pausing 与 paused 不误报。父 Run terminal 后,持续时间冻结在父 Run 自身最后活动,不随 child 晚到收口事件增长。消息时间统一校验为 JavaScript 可表示的 Date;越界值显示“时间未知”且不写无效 `datetime`。实时回复只显示 response stream 自己的 `updatedAt`,缺失时同样显示“时间未知”,不能借用其它 Runtime 活动时间或随前端时钟漂移。该提示只提供可观测性,不改变 Runtime/manifest 正式状态。
|
||||
- ready-task manifest 漂移:父 Supervisor 必须分别判断“能否调度新节点”和“是否存在必须等待的工作”。派生视觉需要父规划修复时不再调度新 child,但当前最新且活跃的根 Run 下,只要存在确定性 runId、scheduler source、正确父绑定且 durable journal 为 queued/running 的 ready child,父 Run 就保持 `waiting-for-manifest-tasks`,不能因旧 hydration 快照把 manifest running 覆盖成 pending 而提前 fixed-graph-stalled。game-chat child 可在相同严格身份下容忍 pending 漂移;正式产物、Canvas、revision、`game.static_smoke` 与 `preview.validate` 门禁不放宽。GUI/CLI、旧父 Run、终态、确认/用户输入/reconciliation、伪造绑定或非确定性 runId 全部失败关闭;更新根 Run 后旧 child 不得继续维持新 DAG 或投影完成。
|
||||
- Supervisor 持久决策与单主条件美术:game-chat 的关键词、用户是否报告“美术未接入”、占位状态和当前资产探测只形成 `advisoryOnly=true` 的补充上下文,不得直接重置 Graph、预完成美术节点、选择复用/生成分支或继承历史试玩类型。当前根 Run 没有持久化 Supervisor 决策时,scheduler 不启动任何 child;Supervisor Provider 只通过 auto-safe 的 `agent.route_manifest` 提交 `game-chat-workflow-decision.v2`:`intentSummary` 是 Supervisor 自行理解并持久化的用户意图,`strategy=audit-existing-first` 只是固定安全执行策略,两者不得混用。此动作不能审计、生成、委派或替代后续判断,也不能把整体视觉重做解释成整套美术的强制重生成;成功后 Runtime 只启动唯一 `code-prototype` 主 Agent。升级恢复时严格校验 v1 sidecar 的旧 fingerprint,并从完成合同绑定的有效任务恢复 `intentSummary`;旧 `code-director` coverage/route 只作为迁移输入,不作为当前完成证据,必须由同一根 Run 的 `code-prototype` 重新 `asset.list` 后原位替换为单主合同。确定性 `code-prototype` Run 仅兼容已知 canonical task 文本版本,其余 task/binding/root 身份继续失败关闭;升级前已运行的 fixed-graph 美术 child 不再具备任何 mutation 或生图权限。主 Agent 必须以当前正式资产、Canvas 登记、私有图集合同、四张语义切片和 art manifest 判断真实缺口;完整覆盖时直接接入,不得生成或扣费。只有可证实缺失 `art-spec` 或核心 spritesheet 时,主 Agent 才可对相应 `art-director` 或 `art-asset-plan` 建立一条 durable 委派;每次最多一个活跃美术 child,child 仅可写 `assets/**`,不得修改 `game/**` 或接入/验收游戏。若两个槽位都缺失,必须先完成 `art-director`,由同一主 Run 认领其 `EvidenceReady` delivery 后,才能委派依赖规范图的 `art-asset-plan`;失败或未就绪 delivery 不得消耗不可重试的图集委派槽位。主 Agent 认领必要回执后继续同一 Run 完成素材接入、原玩法语义校验、`game.static_smoke` 与桌面/移动 `preview.validate`。绝对硬截止对嵌套美术 child 继续核验 `root -> code-prototype -> agent-delegate` 完整身份并保留未知外部生成的 reconciliation 证据。Runtime 只负责校验根/父子身份、当前 revision、路径、Canvas 登记、缺口/路由 fingerprint、写入范围及完成证据;纯“继续”仍走既有正式 continuation 合同,普通美术措辞不得借用更老项目的具体试玩场景。不得以增加 loop 预算、伪造 revision、机械改写 manifest 或重放历史图片 action 代替 Supervisor 决策和程序侧审计。
|
||||
- 动态美术 child 通用 retry 失败关闭:game-chat 的 `art-director` / `art-asset-plan` delivery 精确绑定原父动作派生 delegationId 与 targetRunId,不允许通用 Agent Runtime retry 换绑或继承。retry 入口以不要求 child 仍为 running 的结构身份识别当前 `root -> code-prototype -> art child` 后,在创建新 run 前返回 `kind=game-chat-dynamic-art-retry-unsupported`,引导当前主 Agent 重新 `asset.list` 并按真实缺口创建新的 durable 委派。遗留或伪造的 `source=agent-delegate-retry` 美术 run 只允许诊断性只读工具,`file.write`、patchset、Canvas、command、preview、memory/manifest 和再次委派等 mutation 全部失败关闭,不能因严格授权不成立而回落普通美术权限。完整 16 任务 DAG、非美术委派、顶层 retry 与现有 Tauri String error wire 不变;前端隐藏/置灰控件留给 source-aware 单主投影阶段。
|
||||
- ready-task 对账取消续跑:未知工具结果仍停在 `needs-reconciliation` 且禁止自动重放;人工核对后显式取消原 child,保留 cancel tombstone,旧 child 和旧父 Run 按真实终态收口。若随后创建同 Session、同 Supervisor source、同有效任务语义的 continuation,新完成合同只对同时具有历史 `failed / needs-reconciliation`、最终 `cancelled` 和 durable tombstone 的 ready-task,把当前 manifest 对应 failed 节点恢复为 pending,并由 scheduler 创建全新 child Run。manifest 的读取、failed 筛选、每任务一次的 child journal 索引、证据重验和写回必须位于同一项目写锁域;较新的无 child 根 Run 只有在 durable journal 精确表明为旧 failed Graph 在进入调度前即失败时才能跨过,scheduler 自身失败必须阻断借用更老 tombstone。普通失败、无 tombstone、不同 source/Session/任务语义或证据冲突均保持失败关闭;不得复活旧 pending action、补造 observation 或把取消任务标成 completed。
|
||||
- 完成门静态分析预算:Canvas 视觉门必须先做只会提前拒绝的词法预检。经典或模块脚本同时不含大小写精确的 `import` 与 `export` 字节序列时,不运行模块依赖语义分析;纯 `export ... from` / `export * from` 仍须进入正式模块图分析。当前脚本不含目标文件名或任一已绑定 DOM 图片元素 ID 时,先低成本解码 `\\xNN`、`\\uNNNN`、`\\u{...}`、简单转义和续行;解码后仍无候选才不运行完整 Canvas alias / 函数可达性分析,解码不确定则保守进入 Oxc。存在任一候选时仍执行原 parser、semantic binding、解码后的 computed 属性/StringLiteral 路径、可达 `drawImage`、可见 Canvas、路径大小写和动态 namespace 写入门禁;HTML 中存在某个绑定元素不得使所有无关 JavaScript 单元进入重分析,禁止把词法命中当作通过条件。
|
||||
- Provider 故障展示:Provider retry 的“是否可重试”继续使用 `upstream-5xx` 等稳定类别判断,但 durable retry record 保留安全的精确 `upstream-<HTTP status>` 身份。等待态必须从真实 record 显示 HTTP 状态、`nextAttempt/maxRetries` 与当前持久退避剩余秒数,例如“Provider 上游返回 HTTP 503,准备自动重试 1/3;预计 8 秒后重试”;不得以动画或前端自增计时伪造 attempt。重试耗尽的 Runtime 私有错误只保存 `kind/httpStatus/fingerprint/chars/retryAttempt/maxRetries/retryState`,前端和持久 conversation 仅在字段顺序、范围、状态一致且无尾随正文时派生“上游服务返回 HTTP 503;自动重试已耗尽(3/3)”;其它错误使用固定安全摘要。Provider 响应正文、URL/query、凭据、本地绝对路径、fingerprint、字符数和 `[redacted ...]` 占位符均不得进入用户可见消息。
|
||||
|
||||
@@ -1348,6 +1348,8 @@ Canvas 是条件外部能力,不是 M0-3 四个固定 owner 的通用前置:
|
||||
|
||||
正式任务 M0-4 的 PR 工作包 `M0B-1` 还必须让 `agent-delegate` 与 `agent-delegate-retry` 共用严格动态美术 lineage predicate;当前 retry source 不能绕过 `assets/**`。该修复不属于本文档 PR 的功能实现,但在 `M0B-2` 收敛 game-chat 前端投影前必须完成。
|
||||
|
||||
2026-08-11 裁决进一步冻结:game-chat 动态美术 child 不允许使用通用 Agent Runtime retry。原 child 的 delivery 同时精确绑定父动作派生 delegationId 与 targetRunId;通用 retry 产生的新 run/new delegationId 没有合法 delivery,禁止通过换绑、续发、复制凭证或放宽 strict predicate 赋权。`retry_game_creator_agent_runtime_task_at` 必须在任何 successor durable 副作用前,以不要求 child 仍为 running 的结构身份识别该类终态 child,并返回稳定类型 `kind=game-chat-dynamic-art-retry-unsupported`,引导当前 `code-prototype` 重新 `asset.list` 后创建新的首次委派。遗留/伪造的 `agent-delegate-retry` 美术 run 只保留诊断性只读白名单,全部 mutation fail closed;现有 Tauri String error wire、其它 delegated retry、完整 DAG 和顶层 retry 不变。
|
||||
|
||||
## 20. rollout、停用与回滚
|
||||
|
||||
- M1 以 AppData/Runtime capability 开关启用,不把 feature flag 写进用户项目。
|
||||
@@ -1446,7 +1448,7 @@ M0A-1 是 M1 详细设计与技术 spike 的输入,不是功能交付。M1 可
|
||||
|
||||
### 23.3 M0-4(PR 工作包 `M0B-1` / `M0B-2`)
|
||||
|
||||
`M0B-1` 先封闭 game-chat 动态美术 delegate/retry 的 `assets/**` 安全边界,`M0B-2` 再以 source-aware lineage 修复单主进度、最终回复、可玩 revision 与归档投影。M0-4 不阻塞 M1 策划闭环开工,但阻塞 M3 game-chat 接入和“M0 全部完成”。M0-1~M0-4 全部合入并通过各自门禁后,才能标记“M0 全部完成”。
|
||||
`M0B-1` 先封闭 game-chat 动态美术 delegate/retry 的安全边界:首次 `agent-delegate` 继续按严格 delivery/route/Canvas lineage 只写 `assets/**`;该类 child 的通用 retry 在入队前返回 `kind=game-chat-dynamic-art-retry-unsupported`,遗留或伪造的 `agent-delegate-retry` 只读且全部 mutation 失败关闭,恢复只能由当前 `code-prototype` 重新审计并创建新的首次委派。`M0B-2` 再以 source-aware lineage 修复单主进度、最终回复、可玩 revision 与归档投影。M0-4 不阻塞 M1 策划闭环开工,但阻塞 M3 game-chat 接入和“M0 全部完成”。M0-1~M0-4 全部合入并通过各自门禁后,才能标记“M0 全部完成”。
|
||||
|
||||
## 24. 最终不变量摘要
|
||||
|
||||
|
||||
Reference in New Issue
Block a user