继续收敛 app-server 进程错误
保留执行器核验、OAuth、回合身份与图片解码正文 保留进程树归属、Job、等待和终止原因 补充进程树与 base64 回归验证
This commit is contained in:
@@ -1780,8 +1780,8 @@ async fn stage_codex_app_server_image(
|
||||
let (extension, encoded) = image_data_url_parts(image_url)?;
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.map_err(|_| {
|
||||
platform_llm::LlmError::InvalidRequest("多模态图片 base64 内容无效".to_string())
|
||||
.map_err(|error| {
|
||||
platform_llm::LlmError::InvalidRequest(format!("多模态图片 base64 内容无效:{error}"))
|
||||
})?;
|
||||
if bytes.is_empty() || bytes.len() > GAME_CREATOR_CODEX_APP_SERVER_IMAGE_MAX_BYTES {
|
||||
return Err(platform_llm::LlmError::InvalidRequest(
|
||||
@@ -2527,7 +2527,9 @@ impl CodexAppServerConnection {
|
||||
Ok::<_, String>((executable, version))
|
||||
})
|
||||
.await
|
||||
.map_err(|_| platform_llm::LlmError::InvalidConfig("Codex 执行器身份核验中断".into()))?
|
||||
.map_err(|error| {
|
||||
platform_llm::LlmError::InvalidConfig(format!("Codex 执行器身份核验中断:{error}"))
|
||||
})?
|
||||
.map_err(platform_llm::LlmError::InvalidConfig)?;
|
||||
if workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
||||
execution::validate_approval_version(&codex_cli_version)
|
||||
@@ -3066,8 +3068,10 @@ impl CodexAppServerConnection {
|
||||
let private_home = isolated_codex_home.clone();
|
||||
tokio::task::spawn_blocking(move || handoff.remember(&private_home))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
platform_llm::LlmError::InvalidConfig("私有 OAuth 轮换状态保存中断".into())
|
||||
.map_err(|error| {
|
||||
platform_llm::LlmError::InvalidConfig(format!(
|
||||
"私有 OAuth 轮换状态保存中断:{error}"
|
||||
))
|
||||
})?
|
||||
.map_err(platform_llm::LlmError::InvalidConfig)?;
|
||||
}
|
||||
@@ -5501,9 +5505,9 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
.map(|state| state.client_turn_id)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
.map_err(|error| {
|
||||
TurnError::HostStateUnavailable(HostStateUnavailable {
|
||||
detail: "宿主 CLI 回合身份读取中断".to_string(),
|
||||
detail: format!("宿主 CLI 回合身份读取中断:{error}"),
|
||||
})
|
||||
})?
|
||||
.map_err(|detail| TurnError::HostStateUnavailable(HostStateUnavailable { detail }))?;
|
||||
@@ -6650,6 +6654,18 @@ mod tests {
|
||||
assert!(std::path::Path::new(staged_path).is_file());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_invalid_base64_preserves_decoder_detail() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let error =
|
||||
stage_codex_app_server_image(temp.path(), "data:image/png;base64,not-base64", 0)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains("多模态图片 base64 内容无效"), "{error}");
|
||||
assert!(error.contains("Invalid"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_multimodal_validation_rejects_system_images() {
|
||||
let request = LlmRunRequest::new(vec![LlmMessage::multimodal(
|
||||
|
||||
@@ -46,15 +46,15 @@ impl OwnedProcessTree {
|
||||
#[cfg(unix)]
|
||||
let start_identity =
|
||||
crate::process_identity::external_agent_runner_process_start_identity(pid)
|
||||
.map_err(|_| "app-server-process-identity-unknown")?
|
||||
.map_err(|error| format!("app-server-process-identity-unknown:{error}"))?
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or("app-server-process-identity-unknown")?;
|
||||
.ok_or_else(|| "app-server-process-identity-unknown:启动身份为空".to_string())?;
|
||||
#[cfg(windows)]
|
||||
let job = crate::process_session::WindowsProcessJob::assign_tokio_named(
|
||||
child,
|
||||
&format!("Local\\AGCCodex-{}", uuid::Uuid::new_v4()),
|
||||
)
|
||||
.map_err(|_| "app-server-process-job-unavailable")?;
|
||||
.map_err(|error| format!("app-server-process-job-unavailable:{error}"))?;
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
return Err("app-server-process-control-unsupported".into());
|
||||
#[cfg(any(windows, unix))]
|
||||
@@ -91,16 +91,26 @@ impl OwnedProcessTree {
|
||||
return Err("app-server-process-owner-mismatch".into());
|
||||
}
|
||||
let deadline = tokio::time::Instant::now() + STOP_TIMEOUT;
|
||||
if self.terminate_owned_tree().is_err() {
|
||||
if let Err(error) = self.terminate_owned_tree() {
|
||||
// 只清理仍持有的直属 Child 句柄;不能据此报告子树已回收。
|
||||
let _ = child.start_kill();
|
||||
let _ = tokio::time::timeout_at(deadline, child.wait()).await;
|
||||
return Err("app-server-process-tree-termination-uncertain".into());
|
||||
let kill_error = child
|
||||
.start_kill()
|
||||
.err()
|
||||
.map(|error| format!(";直属进程终止失败:{error}"))
|
||||
.unwrap_or_default();
|
||||
let wait_error = match tokio::time::timeout_at(deadline, child.wait()).await {
|
||||
Ok(Ok(_)) => String::new(),
|
||||
Ok(Err(error)) => format!(";等待直属进程退出失败:{error}"),
|
||||
Err(_) => ";等待直属进程退出超时".to_string(),
|
||||
};
|
||||
return Err(format!(
|
||||
"app-server-process-tree-termination-uncertain:{error}{kill_error}{wait_error}"
|
||||
));
|
||||
}
|
||||
tokio::time::timeout_at(deadline, child.wait())
|
||||
.await
|
||||
.map_err(|_| "app-server-process-exit-timeout")?
|
||||
.map_err(|_| "app-server-process-exit-unconfirmed")?;
|
||||
.map_err(|_| "app-server-process-exit-timeout:等待直属进程退出超过 5 秒".to_string())?
|
||||
.map_err(|error| format!("app-server-process-exit-unconfirmed:{error}"))?;
|
||||
loop {
|
||||
if self.observed_tree_empty()? {
|
||||
break;
|
||||
@@ -127,7 +137,7 @@ impl OwnedProcessTree {
|
||||
{
|
||||
self.job
|
||||
.is_empty()
|
||||
.map_err(|_| "app-server-process-tree-state-unknown".into())
|
||||
.map_err(|error| format!("app-server-process-tree-state-unknown:{error}"))
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -141,7 +151,10 @@ impl OwnedProcessTree {
|
||||
if std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err("app-server-process-tree-state-unknown".into())
|
||||
Err(format!(
|
||||
"app-server-process-tree-state-unknown:{}",
|
||||
std::io::Error::last_os_error()
|
||||
))
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
@@ -155,7 +168,7 @@ impl OwnedProcessTree {
|
||||
{
|
||||
self.job
|
||||
.terminate()
|
||||
.map_err(|_| "app-server-process-tree-termination-uncertain".into())
|
||||
.map_err(|error| format!("app-server-process-tree-termination-uncertain:{error}"))
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -165,7 +178,7 @@ impl OwnedProcessTree {
|
||||
// leader 已消失或 PID 被复用时不盲杀 PGID;保留不确定状态。
|
||||
let current =
|
||||
crate::process_identity::external_agent_runner_process_start_identity(self.pid)
|
||||
.map_err(|_| "app-server-process-identity-unknown")?;
|
||||
.map_err(|error| format!("app-server-process-identity-unknown:{error}"))?;
|
||||
if current.as_deref() != Some(self.start_identity.as_str()) {
|
||||
return Err("app-server-process-owner-mismatch".into());
|
||||
}
|
||||
@@ -173,7 +186,10 @@ impl OwnedProcessTree {
|
||||
if result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("app-server-process-tree-termination-uncertain".into())
|
||||
Err(format!(
|
||||
"app-server-process-tree-termination-uncertain:{}",
|
||||
std::io::Error::last_os_error()
|
||||
))
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
@@ -186,7 +202,9 @@ impl OwnedProcessTree {
|
||||
impl Drop for OwnedProcessTree {
|
||||
fn drop(&mut self) {
|
||||
// Drop 只做应急回收,永远不伪造完成证明;正式终态必须 await shutdown。
|
||||
let _ = self.terminate_owned_tree();
|
||||
if let Err(error) = self.terminate_owned_tree() {
|
||||
app_log!("agent.codex_app_server.process_tree.drop_cleanup_failed error={error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,9 @@ struct ContextIdentity {
|
||||
fn context_identity(root: &Path) -> Result<ContextIdentity, String> {
|
||||
let manifest = read_existing_manifest_for_project(root)?;
|
||||
let revision = read_game_creator_agent_runtime_project_revision(root)?.revision;
|
||||
let canonical = root.canonicalize().map_err(|_| "项目上下文目录无法解析")?;
|
||||
let canonical = root
|
||||
.canonicalize()
|
||||
.map_err(|error| format!("项目上下文目录无法解析:{error}"))?;
|
||||
let active = list_active_turns()?.into_iter().find(|turn| {
|
||||
Path::new(&turn.project_path).canonicalize().ok().as_ref() == Some(&canonical)
|
||||
});
|
||||
|
||||
@@ -91,3 +91,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
|
||||
- Direct 交付终态读取原先把 `session.snapshot()` 锁/状态错误折叠成 `None`,上层继续显示“尚未确认交付完成”;现沿 `code-generation` 失败出口保留终态读取正文,并在已有回合失败时同时保留原始失败与终态读取失败。
|
||||
- 对话模型选择器读取 native 配置原先把 IPC/文件异常 `.catch(() => null)` 后只显示“读取客户端配置失败”;现通过统一可见错误脱敏出口保留具体正文,并保留配置不可读时不猜测模型路由的安全行为。
|
||||
- 策划工作区事件订阅失败仍有一处直接静默结束,导致文件刷新失联却没有提示;现把订阅 IPC 正文写入工作区错误状态,继续保留手动刷新与已有清单读取错误出口。
|
||||
- 新一轮原生扫出 app-server 进程与上下文漏损:Codex 执行器核验/OAuth 保存/回合身份的 JoinError、图片 base64 解码、Direct 上下文 canonicalize,以及进程树归属、Job、等待、终止和 Drop 清理错误原先只剩固定码;现保留 JoinError、解码器、OS/Job、等待和终止阶段正文,并为进程树 Drop 清理失败写入诊断日志。
|
||||
|
||||
Reference in New Issue
Block a user