保留交付执行底层错误

传播交付 worker 产物读取和执行器 OS 原因

通过 Direct 执行定向测试
This commit is contained in:
kdletters
2026-10-05 19:14:47 +08:00
parent 9d450eca7c
commit 88507e08b0
4 changed files with 26 additions and 22 deletions
@@ -58,11 +58,12 @@ pub(super) async fn update_plan(
}
*explanation = truncate_agent_runtime_text(explanation, 1200);
}
let value = serde_json::to_value(plan).map_err(|_| "direct-plan-invalid")?;
let value =
serde_json::to_value(plan).map_err(|error| format!("direct-plan-invalid:{error}"))?;
let session = Arc::clone(session);
tokio::task::spawn_blocking(move || session.update_plan(value))
.await
.map_err(|_| "direct-plan-worker-exited")?
.map_err(|error| format!("direct-plan-worker-exited:{error}"))?
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -404,7 +405,7 @@ pub(super) async fn requires_new_web_contract(
let kind = creation_type.map(str::to_string);
tokio::task::spawn_blocking(move || initial_requirement_at(&root, kind.as_deref(), cli))
.await
.map_err(|_| "delivery-initial-check-exited")?
.map_err(|error| format!("delivery-initial-check-exited:{error}"))?
}
fn mark_initial_delivered(root: &Path, ledger: &ExecutionLedger) -> Result<(), String> {
@@ -451,7 +452,7 @@ pub(super) async fn register_contract(
let value = serde_json::to_value(contract).map_err(|_| "delivery-contract-invalid")?;
let frozen = session.freeze_contract(value)?;
Ok(json!({"status":"frozen","contract":frozen,"next":"只推进已登记范围;使用托管构建/验证和固定场景提供真实证据"}))
}).await.map_err(|_| "delivery-contract-worker-exited")?
}).await.map_err(|error| format!("delivery-contract-worker-exited:{error}"))?
}
fn artifact_hash(root: &Path, relative: &str) -> Result<String, String> {
@@ -467,7 +468,7 @@ fn artifact_hash(root: &Path, relative: &str) -> Result<String, String> {
loop {
let count = file
.read(&mut buffer)
.map_err(|_| "delivery-artifact-read")?;
.map_err(|error| format!("delivery-artifact-read:{error}"))?;
if count == 0 {
break;
}
@@ -650,7 +651,7 @@ pub(super) async fn status(root: &Path, session: &Arc<ExecutionSession>) -> Resu
let session = Arc::clone(session);
tokio::task::spawn_blocking(move || { let ledger = session.snapshot()?; let assessment = assess(&root,&ledger)?;
Ok(json!({"phase":ledger.phase,"contract":ledger.contract,"plan":ledger.plan,"assessment":assessment,"writeRecoveryRequired":ledger.last_failed_write_revision.is_some(),"usedPasses":ledger.used_passes,"maxRuns":ledger.max_runs,"usedExecutionMs":ledger.used_execution_ms,"maxExecutionMs":ledger.max_execution_ms,"inFlight":ledger.active.len(),"report":ledger.terminal_report}))
}).await.map_err(|_| "delivery-status-worker-exited")?
}).await.map_err(|error| format!("delivery-status-worker-exited:{error}"))?
}
pub(super) async fn try_seal(root: &Path, session: &Arc<ExecutionSession>) -> Result<bool, String> {
@@ -667,7 +668,7 @@ pub(super) async fn try_seal(root: &Path, session: &Arc<ExecutionSession>) -> Re
session.begin_sealing(ledger.revision)
})
.await
.map_err(|_| "delivery-seal-worker-exited")?
.map_err(|error| format!("delivery-seal-worker-exited:{error}"))?
}
pub(super) async fn finish_sealing(
@@ -693,7 +694,7 @@ pub(super) async fn finish_sealing(
session.complete(report.clone())?;
if mark_initial_delivered(&root,&ledger).is_err() { app_log!("首次交付标记未写入,后续保持更严格的新项目验收要求"); }
Ok(Some(report))
}).await.map_err(|_| "delivery-finalize-worker-exited")?
}).await.map_err(|error| format!("delivery-finalize-worker-exited:{error}"))?
}
/// 回合末的宿主复核:返回要交付的答复,或者一个"还没完,按这份证据继续修"的要求。
@@ -722,9 +723,10 @@ pub(super) async fn review_reply(
let review_session = Arc::clone(session);
let review = tokio::task::spawn_blocking(move || review_session.record_delivery_review())
.await
.map_err(|_| "delivery-review-worker-exited")??;
.map_err(|error| format!("delivery-review-worker-exited:{error}"))??;
let assessment = status(root, session).await?;
let detail = serde_json::to_string(&assessment).map_err(|_| "delivery-review-invalid")?;
let detail = serde_json::to_string(&assessment)
.map_err(|error| format!("delivery-review-invalid:{error}"))?;
if review >= ledger.max_runs {
let missing = assessment
.pointer("/assessment/checks")
@@ -751,7 +753,7 @@ pub(super) async fn review_reply(
let saved = report.clone();
tokio::task::spawn_blocking(move || session.interrupt(saved))
.await
.map_err(|_| "delivery-review-worker-exited")??;
.map_err(|error| format!("delivery-review-worker-exited:{error}"))??;
return Ok(Some(report));
}
Err(TurnError::ReviewRequired {
@@ -128,11 +128,11 @@ struct CodexExecutorIdentity {
fn executor_digest(path: &Path) -> Result<String, String> {
use std::io::Read;
let mut file =
File::open(path).map_err(|_| "direct-execution-executor: 无法读取已选择的执行器")?;
let mut file = File::open(path)
.map_err(|error| format!("direct-execution-executor: 无法读取已选择的执行器:{error}"))?;
let before = file
.metadata()
.map_err(|_| "direct-execution-executor: 执行器身份不可用")?;
.map_err(|error| format!("direct-execution-executor: 执行器身份不可用:{error}"))?;
if !before.is_file() || before.len() > 512 * 1024 * 1024 {
return Err("direct-execution-executor: 执行器类型或大小无效".into());
}
@@ -142,7 +142,7 @@ fn executor_digest(path: &Path) -> Result<String, String> {
loop {
let read = file
.read(&mut buffer)
.map_err(|_| "direct-execution-executor: 执行器读取失败")?;
.map_err(|error| format!("direct-execution-executor: 执行器读取失败:{error}"))?;
if read == 0 {
break;
}
@@ -154,7 +154,7 @@ fn executor_digest(path: &Path) -> Result<String, String> {
}
let after = file
.metadata()
.map_err(|_| "direct-execution-executor: 执行器身份不可用")?;
.map_err(|error| format!("direct-execution-executor: 执行器身份不可用:{error}"))?;
if bytes != before.len()
|| after.len() != before.len()
|| before.modified().ok() != after.modified().ok()
@@ -446,7 +446,7 @@ pub(super) async fn begin(
)
})
.await
.map_err(|_| "direct-execution-start: 宿主状态初始化退出")??;
.map_err(|error| format!("direct-execution-start: 宿主状态初始化退出:{error}"))??;
let root = session.root.clone();
{
let mut registry = sessions()
@@ -515,7 +515,7 @@ pub(super) fn open_with_analytics_at(
config.validate()?;
let root = root
.canonicalize()
.map_err(|_| "direct-execution-project: 项目不可用")?;
.map_err(|error| format!("direct-execution-project: 项目不可用:{error}"))?;
if turn.is_empty() || turn.len() > 512 || request_hash.len() != 64 {
return Err("direct-execution-identity: 回合身份无效".into());
}
@@ -523,17 +523,17 @@ pub(super) fn open_with_analytics_at(
return Err("direct-execution-host: 权威状态不得写入模型项目目录".into());
}
crate::ensure_game_creator_private_directory_tree(host, "宿主执行状态目录")
.map_err(|_| "direct-execution-host: 无法准备私有执行状态目录")?;
.map_err(|error| format!("direct-execution-host: 无法准备私有执行状态目录:{error}"))?;
let host = host
.canonicalize()
.map_err(|_| "direct-execution-host: 状态目录不可用")?;
.map_err(|error| format!("direct-execution-host: 状态目录不可用:{error}"))?;
if host.starts_with(&root) {
return Err("direct-execution-host: 权威状态不得写入模型项目目录".into());
}
let project_key = hash(root.to_string_lossy().as_bytes());
let directory = host.join(&project_key);
crate::ensure_game_creator_private_directory_tree(&directory, "宿主项目执行状态")
.map_err(|_| "direct-execution-host: 无法准备私有项目执行状态")?;
.map_err(|error| format!("direct-execution-host: 无法准备私有项目执行状态:{error}"))?;
let key = hash(turn.as_bytes());
let lock_path = directory.join(format!("{key}.lock"));
if lock_path.exists() {
@@ -546,7 +546,7 @@ pub(super) fn open_with_analytics_at(
.create(true)
.truncate(false)
.open(&lock_path)
.map_err(|_| "direct-execution-owner: 无法打开执行归属锁")?;
.map_err(|error| format!("direct-execution-owner: 无法打开执行归属锁:{error}"))?;
owner
.try_lock()
.map_err(|_| "direct-execution-active: 同一回合仍由其他执行器持有")?;
@@ -119,6 +119,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
- WebView 普通错误出口遇到 Tauri 的结构化对象时,优先展示 `message/detail/reason/code`,再有界序列化对象;不得把对象转成 `[object Object]` 后回落“未提供具体错误正文”。
- Direct 工具桥图片读取和真实试玩验证证据的 OS 读取/哈希错误保留底层原因;路径越界、私有路径和安全拒绝仍使用稳定安全码。
- `agc_read_project_context` 的安全拒绝继续只给稳定码,但受控文件打开/读取/复核的 OS 错误要附在错误码后,不能把 `file-read-failed` 当成完整原因。
- Direct 交付复核/执行账本的 worker join、产物读取、执行器摘要和宿主目录/锁 I/O 错误也保留底层原因;稳定 delivery/direct-execution 分类码只负责分流。
- 浏览器启动/DevTools 握手与 Codex model-catalog 子进程失败保留 stderr、退出状态、解析错误和阶段;稳定机器码仍用于分类,但不能单独成为用户可见正文。
- Node/npm 环境探测和 Web scaffold 构建持续排空并保存有界 stdout/stderr 尾部,失败返回退出状态、超时阶段和脱敏正文;环境预检回执同时下发 `code` 与 `diagnostic`。
- HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。
@@ -22,6 +22,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
- WebView 结构化对象错误保留 `message/detail/reason/code` 或有界 JSON 正文,避免 `[object Object]` 触发普通错误兜底。
- Direct 工具桥图片与真实试玩证据读取/哈希错误保留 OS 正文,安全边界拒绝继续使用稳定码。
- `agc_read_project_context` 保留 `file-read-failed` 等稳定码,同时在能取得时附带文件打开/读取/复核的 OS 正文。
- Direct 交付复核和执行账本的 worker、产物读取、执行器摘要及宿主目录/锁失败保留底层原因,分类码不再单独承载正文。
- Transport / Stream / IPC / host process / memory exhaustion 的回归测试与错误事件证据。
## 不做