15c227cebf
完善Codex CLI、App Server、ConPTY与进程会话在Windows下的发现、启动、恢复和退出行为 修复Agent Runtime、Provider重试、项目写锁及工具交接账本的并发与跨测试串线问题 补齐配置目录、路径脱敏、原子写入、浏览器探测和本地Provider smoke的跨平台兼容 增强Goal Contract、自动策略、资源生成及运行态恢复的契约和回归测试 更新AI游戏创作智能体App技术文档中的Windows稳定性说明 验证AGC开发态、Release打包、打包后GUI运行及完整agc:check门禁
371 lines
14 KiB
Rust
371 lines
14 KiB
Rust
use super::*;
|
|
|
|
pub(super) fn live_process_session(
|
|
process_id: &str,
|
|
) -> Result<Option<Arc<LiveProcessSession>>, String> {
|
|
validate_process_id(process_id)?;
|
|
Ok(process_session_registry()
|
|
.lock()
|
|
.map_err(|_| "process session registry 锁已损坏".to_string())?
|
|
.sessions
|
|
.get(process_id)
|
|
.cloned())
|
|
}
|
|
|
|
pub(super) fn poll_result_from_output(
|
|
process_id: &str,
|
|
output: &str,
|
|
state: &ProcessOutputState,
|
|
sandbox_backend: &str,
|
|
sandbox_mode: &str,
|
|
network_access: &str,
|
|
sandbox_profile_version: &str,
|
|
sandbox_establishment: &str,
|
|
target_exec: &str,
|
|
launch_failure_kind: Option<&str>,
|
|
cursor: Option<&str>,
|
|
max_chars: usize,
|
|
) -> Result<ProcessSessionPollResult, String> {
|
|
let offset = parse_process_session_cursor(process_id, cursor, output)?;
|
|
let end = output[offset..]
|
|
.char_indices()
|
|
.nth(max_chars)
|
|
.map(|(index, _)| offset + index)
|
|
.unwrap_or(output.len());
|
|
let next_cursor = process_session_cursor(process_id, end);
|
|
Ok(ProcessSessionPollResult {
|
|
process_id: process_id.to_string(),
|
|
status: state.status.clone(),
|
|
output: output[offset..end].to_string(),
|
|
cursor: process_session_cursor(process_id, offset),
|
|
next_cursor,
|
|
has_more: end < output.len(),
|
|
stdin_open: state.stdin_open,
|
|
exit_code: state.exit_code,
|
|
signal: state.signal.clone(),
|
|
output_bytes: output.len(),
|
|
output_sha256: format!("{:x}", Sha256::digest(output.as_bytes())),
|
|
source_changed: state.source_changed,
|
|
needs_reconciliation: state.needs_reconciliation,
|
|
sandbox_backend: sandbox_backend.to_string(),
|
|
sandbox_mode: sandbox_mode.to_string(),
|
|
network_access: network_access.to_string(),
|
|
sandbox_profile_version: sandbox_profile_version.to_string(),
|
|
sandbox_establishment: sandbox_establishment.to_string(),
|
|
target_exec: target_exec.to_string(),
|
|
launch_failure_kind: launch_failure_kind.map(str::to_string),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn poll_process_session_at(
|
|
root: &Path,
|
|
identity: &ProcessSessionIdentity,
|
|
process_id: &str,
|
|
cursor: Option<&str>,
|
|
max_chars: Option<usize>,
|
|
wait_ms: Option<u64>,
|
|
) -> Result<ProcessSessionPollResult, String> {
|
|
validate_process_session_identity(identity)?;
|
|
let max_chars = max_chars
|
|
.unwrap_or(PROCESS_SESSION_DEFAULT_POLL_CHARS)
|
|
.min(PROCESS_SESSION_MAX_POLL_CHARS);
|
|
let wait_ms = wait_ms.unwrap_or(0).min(PROCESS_SESSION_MAX_POLL_WAIT_MS);
|
|
if let Some(live) = live_process_session(process_id)? {
|
|
if live.root != root || live.identity != *identity {
|
|
return Err("process session 不属于当前 Agent run".to_string());
|
|
}
|
|
let mut output = live
|
|
.output
|
|
.lock()
|
|
.map_err(|_| "process session output 锁已损坏".to_string())?;
|
|
let initial_offset = parse_process_session_cursor(process_id, cursor, &output.text)?;
|
|
if wait_ms > 0 && initial_offset == output.text.len() && output.status == "running" {
|
|
let waited = live
|
|
.output_changed
|
|
.wait_timeout(output, Duration::from_millis(wait_ms))
|
|
.map_err(|_| "process session output 锁已损坏".to_string())?;
|
|
output = waited.0;
|
|
}
|
|
return poll_result_from_output(
|
|
process_id,
|
|
&output.text,
|
|
&output,
|
|
&live.sandbox_backend,
|
|
&live.sandbox_mode,
|
|
&live.network_access,
|
|
&live.sandbox_profile_version,
|
|
&live.sandbox_establishment,
|
|
&live.target_exec,
|
|
output.launch_failure_kind.as_deref(),
|
|
cursor,
|
|
max_chars,
|
|
);
|
|
}
|
|
|
|
let mut record = read_process_session_record(root, process_id)?
|
|
.ok_or_else(|| "process session 不存在".to_string())?;
|
|
validate_process_session_access(&record, identity)?;
|
|
if matches!(
|
|
record.status.as_str(),
|
|
"prepared" | "launching" | "running" | "terminating"
|
|
) && record.owner_boot_id != process_session_boot_id()
|
|
{
|
|
reconcile_stale_active_process_session(&mut record);
|
|
write_process_session_record(root, &record)?;
|
|
}
|
|
let transcript = if let Some(output_ref) = record.output_ref.as_deref() {
|
|
read_agent_runtime_json_sidecar_with_max_bytes::<ProcessSessionTranscript>(
|
|
root,
|
|
output_ref,
|
|
"Agent Runtime process transcript",
|
|
PROCESS_SESSION_TRANSCRIPT_MAX_BYTES,
|
|
)?
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(transcript) = &transcript {
|
|
if let Err(error) = validate_process_session_transcript(transcript, &record) {
|
|
record.status = "needs-reconciliation".to_string();
|
|
record.stdin_open = false;
|
|
record.needs_reconciliation = true;
|
|
record.terminal_at = Some(unix_timestamp());
|
|
record.updated_at = unix_timestamp();
|
|
let _ = write_process_session_record(root, &record);
|
|
return Err(error);
|
|
}
|
|
}
|
|
let output = transcript
|
|
.as_ref()
|
|
.map(|value| value.output.as_str())
|
|
.unwrap_or_default();
|
|
let state = ProcessOutputState {
|
|
text: output.to_string(),
|
|
status: record.status,
|
|
exit_code: record.exit_code,
|
|
signal: record.signal,
|
|
stdin_open: record.stdin_open,
|
|
reader_finished: true,
|
|
output_limit_exceeded: false,
|
|
source_fingerprint_after: record.source_fingerprint_after,
|
|
source_changed: record.source_changed,
|
|
needs_reconciliation: record.needs_reconciliation,
|
|
launch_failure_kind: record.launch_failure_kind.clone(),
|
|
};
|
|
poll_result_from_output(
|
|
process_id,
|
|
output,
|
|
&state,
|
|
&record.sandbox_backend,
|
|
&record.sandbox_mode,
|
|
&record.network_access,
|
|
&record.sandbox_profile_version,
|
|
&record.sandbox_establishment,
|
|
&record.target_exec,
|
|
record.launch_failure_kind.as_deref(),
|
|
cursor,
|
|
max_chars,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn write_process_session_stdin_at(
|
|
root: &Path,
|
|
identity: &ProcessSessionIdentity,
|
|
process_id: &str,
|
|
data: &str,
|
|
append_newline: bool,
|
|
eof: bool,
|
|
) -> Result<ProcessSessionStdinResult, String> {
|
|
write_process_session_stdin_at_with_after_write(
|
|
root,
|
|
identity,
|
|
process_id,
|
|
data,
|
|
append_newline,
|
|
eof,
|
|
|_| {},
|
|
)
|
|
}
|
|
|
|
pub(super) fn write_process_session_stdin_at_with_after_write<F>(
|
|
root: &Path,
|
|
identity: &ProcessSessionIdentity,
|
|
process_id: &str,
|
|
data: &str,
|
|
append_newline: bool,
|
|
eof: bool,
|
|
after_write: F,
|
|
) -> Result<ProcessSessionStdinResult, String>
|
|
where
|
|
F: FnOnce(&LiveProcessSession),
|
|
{
|
|
validate_process_session_identity(identity)?;
|
|
let live = live_process_session(process_id)?
|
|
.ok_or_else(|| "process session 不在当前 Runner 中运行".to_string())?;
|
|
if live.root != root || live.identity != *identity {
|
|
return Err("process session 不属于当前 Agent run".to_string());
|
|
}
|
|
let mut bytes = data.as_bytes().to_vec();
|
|
if append_newline {
|
|
#[cfg(windows)]
|
|
bytes.extend_from_slice(b"\r\n");
|
|
#[cfg(not(windows))]
|
|
bytes.push(b'\n');
|
|
}
|
|
if bytes.len() > PROCESS_SESSION_MAX_STDIN_BYTES {
|
|
return Err(format!(
|
|
"command.stdin 单次最多写入 {PROCESS_SESSION_MAX_STDIN_BYTES} 字节"
|
|
));
|
|
}
|
|
if bytes.iter().any(|byte| *byte == 0) {
|
|
return Err("command.stdin 不接受 NUL 或二进制正文".to_string());
|
|
}
|
|
let content_sha256 = format!("{:x}", Sha256::digest(&bytes));
|
|
let mut writer = live
|
|
.writer
|
|
.lock()
|
|
.map_err(|_| "process session stdin 锁已损坏".to_string())?;
|
|
if live
|
|
.output
|
|
.lock()
|
|
.map_err(|_| "process session output 锁已损坏".to_string())?
|
|
.status
|
|
!= "running"
|
|
{
|
|
return Err("process session 已进入终态".to_string());
|
|
}
|
|
if !bytes.is_empty() {
|
|
let stream = writer
|
|
.as_mut()
|
|
.ok_or_else(|| "process session stdin 已关闭".to_string())?;
|
|
stream
|
|
.write_all(&bytes)
|
|
.and_then(|()| stream.flush())
|
|
.map_err(|error| format!("写入 process session stdin 失败:{error}"))?;
|
|
}
|
|
if eof {
|
|
writer.take();
|
|
}
|
|
drop(writer);
|
|
after_write(&live);
|
|
let mut output = live
|
|
.output
|
|
.lock()
|
|
.map_err(|_| "process session output 锁已损坏".to_string())?;
|
|
if eof || output.status != "running" {
|
|
output.stdin_open = false;
|
|
}
|
|
let record = process_session_record_from_live(&live, &output);
|
|
if let Err(error) = write_process_session_record(root, &record) {
|
|
output.status = "needs-reconciliation".to_string();
|
|
output.needs_reconciliation = true;
|
|
output.stdin_open = false;
|
|
let reconciliation = process_session_record_from_live(&live, &output);
|
|
let _ = write_process_session_record(root, &reconciliation);
|
|
let _ = live.control.send(ProcessControl::Terminate);
|
|
return Err(format!(
|
|
"command.stdin 已写入但状态无法落盘,需要人工核对:{error}"
|
|
));
|
|
}
|
|
Ok(ProcessSessionStdinResult {
|
|
process_id: process_id.to_string(),
|
|
bytes_written: data.len() + usize::from(append_newline),
|
|
content_sha256,
|
|
stdin_open: output.stdin_open,
|
|
eof,
|
|
sandbox_backend: live.sandbox_backend.clone(),
|
|
sandbox_mode: live.sandbox_mode.clone(),
|
|
network_access: live.network_access.clone(),
|
|
sandbox_profile_version: live.sandbox_profile_version.clone(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn terminate_process_session_at(
|
|
root: &Path,
|
|
identity: &ProcessSessionIdentity,
|
|
process_id: &str,
|
|
cursor: Option<&str>,
|
|
) -> Result<ProcessSessionPollResult, String> {
|
|
validate_process_session_identity(identity)?;
|
|
if let Some(live) = live_process_session(process_id)? {
|
|
if live.root != root || live.identity != *identity {
|
|
return Err("process session 不属于当前 Agent run".to_string());
|
|
}
|
|
let running = live
|
|
.output
|
|
.lock()
|
|
.map_err(|_| "process session output 锁已损坏".to_string())?
|
|
.status
|
|
== "running";
|
|
if running {
|
|
live.control
|
|
.send(ProcessControl::Terminate)
|
|
.map_err(|_| "process session 监督线程已结束".to_string())?;
|
|
let mut output = live
|
|
.output
|
|
.lock()
|
|
.map_err(|_| "process session output 锁已损坏".to_string())?;
|
|
let deadline = std::time::Instant::now()
|
|
+ Duration::from_millis(PROCESS_SESSION_TERMINATE_GRACE_MS + 1_500);
|
|
while output.status == "running" && std::time::Instant::now() < deadline {
|
|
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
|
let waited = live
|
|
.output_changed
|
|
.wait_timeout(output, remaining.min(Duration::from_millis(100)))
|
|
.map_err(|_| "process session output 锁已损坏".to_string())?;
|
|
output = waited.0;
|
|
}
|
|
}
|
|
let mut result =
|
|
poll_process_session_at(root, identity, process_id, cursor, Some(1), Some(0))?;
|
|
let cursor_offset = result
|
|
.cursor
|
|
.rsplit_once(':')
|
|
.and_then(|(_, offset)| offset.parse::<usize>().ok())
|
|
.ok_or_else(|| "command.terminate 返回了无效 cursor".to_string())?;
|
|
result.output.clear();
|
|
result.next_cursor = result.cursor.clone();
|
|
result.has_more = cursor_offset < result.output_bytes;
|
|
return Ok(result);
|
|
}
|
|
let mut result = poll_process_session_at(root, identity, process_id, cursor, Some(1), Some(0))?;
|
|
let cursor_offset = result
|
|
.cursor
|
|
.rsplit_once(':')
|
|
.and_then(|(_, offset)| offset.parse::<usize>().ok())
|
|
.ok_or_else(|| "command.terminate 返回了无效 cursor".to_string())?;
|
|
result.output.clear();
|
|
result.next_cursor = result.cursor.clone();
|
|
result.has_more = cursor_offset < result.output_bytes;
|
|
Ok(result)
|
|
}
|
|
|
|
pub(crate) fn mark_process_session_start_audit_failure_at(
|
|
root: &Path,
|
|
process_id: &str,
|
|
error: &str,
|
|
) -> Result<(), String> {
|
|
if let Some(live) = live_process_session(process_id)? {
|
|
if live.root != root {
|
|
return Err("process session 不属于当前项目".to_string());
|
|
}
|
|
mark_process_session_reconciliation(
|
|
&live,
|
|
&format!("command.start audit persistence failed: {error}"),
|
|
);
|
|
if let Ok(mut output) = live.output.lock() {
|
|
output.launch_failure_kind = Some("start-audit-failed".to_string());
|
|
}
|
|
let _ = live.control.send(ProcessControl::Terminate);
|
|
}
|
|
let mut record = read_process_session_record(root, process_id)?
|
|
.ok_or_else(|| "command.start audit 失败后 process record 缺失".to_string())?;
|
|
record.status = "needs-reconciliation".to_string();
|
|
record.stdin_open = false;
|
|
record.needs_reconciliation = true;
|
|
record.launch_failure_kind = Some("start-audit-failed".to_string());
|
|
record.signal = Some(redact_agent_runtime_project_paths(root, error, 240));
|
|
record.terminal_at = Some(unix_timestamp());
|
|
record.updated_at = unix_timestamp();
|
|
write_process_session_record(root, &record)
|
|
}
|