Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/process_session/persistence.rs
T
AIGameCreator App 0aaa191f8d 并行拆分客户端运行时与项目摘要模块
将 runtime_tools 拆为十五个职责模块并保留 Agent 可见性

将 Runner 拆为协议端点分发客户端与所有权模块

将进程会话拆为模型持久化生命周期 IO 恢复与测试模块

将项目摘要拆为十四个无环模块并保留一百一十二个导出

记录并行拆分边界与稳定树验收规则
2026-07-22 14:45:36 +08:00

630 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
pub(super) fn validate_process_session_identity(
identity: &ProcessSessionIdentity,
) -> Result<(), String> {
for (label, value, max_chars) in [
("projectId", identity.project_id.as_str(), 160),
("agentId", identity.agent_id.as_str(), 96),
("taskId", identity.task_id.as_str(), 96),
(
"conversationSessionId",
identity.conversation_session_id.as_str(),
160,
),
("runId", identity.run_id.as_str(), 160),
("startActionId", identity.start_action_id.as_str(), 160),
] {
let trimmed = value.trim();
if trimmed.is_empty()
|| trimmed.chars().count() > max_chars
|| trimmed.chars().any(|character| character.is_control())
{
return Err(format!("command.start {label} 无效"));
}
}
if identity.start_action_fingerprint.len() != 64
|| !identity
.start_action_fingerprint
.bytes()
.all(|byte| byte.is_ascii_hexdigit())
{
return Err("command.start action fingerprint 无效".to_string());
}
Ok(())
}
pub(super) fn validate_process_id(process_id: &str) -> Result<(), String> {
if process_id.len() != 37
|| !process_id.starts_with("proc-")
|| !process_id[5..].bytes().all(|byte| byte.is_ascii_hexdigit())
{
return Err("processId 格式无效".to_string());
}
Ok(())
}
pub(super) fn process_session_id(identity: &ProcessSessionIdentity) -> String {
let payload = serde_json::to_vec(&serde_json::json!({
"projectId": identity.project_id,
"agentId": identity.agent_id,
"taskId": identity.task_id,
"conversationSessionId": identity.conversation_session_id,
"runId": identity.run_id,
"startActionId": identity.start_action_id,
"startActionFingerprint": identity.start_action_fingerprint,
"ownerBootId": process_session_boot_id(),
}))
.unwrap_or_default();
let value = format!("{:x}", Sha256::digest(payload));
format!("proc-{}", &value[..32])
}
pub(super) fn process_session_record_relative_path(process_id: &str) -> String {
format!(".agent/runtime/process-sessions/{process_id}.json")
}
pub(super) fn process_session_transcript_relative_path(process_id: &str) -> String {
format!(".agent/runtime/process-sessions/{process_id}.output.json")
}
pub(super) fn process_session_cursor(process_id: &str, offset: usize) -> String {
format!("{PROCESS_SESSION_CURSOR_VERSION}:{process_id}:{offset}")
}
pub(super) fn parse_process_session_cursor(
process_id: &str,
cursor: Option<&str>,
output: &str,
) -> Result<usize, String> {
let Some(cursor) = cursor.filter(|value| !value.trim().is_empty()) else {
return Ok(0);
};
let mut parts = cursor.split(':');
let version = parts.next().unwrap_or_default();
let cursor_process_id = parts.next().unwrap_or_default();
let offset = parts
.next()
.ok_or_else(|| "command.poll cursor 无效".to_string())?
.parse::<usize>()
.map_err(|_| "command.poll cursor offset 无效".to_string())?;
if parts.next().is_some()
|| version != PROCESS_SESSION_CURSOR_VERSION
|| cursor_process_id != process_id
|| offset > output.len()
|| !output.is_char_boundary(offset)
{
return Err("command.poll cursor 与当前进程输出不匹配".to_string());
}
Ok(offset)
}
pub(super) fn write_process_session_record(
root: &Path,
record: &ProcessSessionRecord,
) -> Result<(), String> {
write_agent_runtime_json_sidecar_with_max_bytes(
root,
&process_session_record_relative_path(&record.process_id),
"Agent Runtime process session",
record,
PROCESS_SESSION_RECORD_MAX_BYTES,
)
}
pub(super) fn read_process_session_record(
root: &Path,
process_id: &str,
) -> Result<Option<ProcessSessionRecord>, String> {
validate_process_id(process_id)?;
let mut record = read_agent_runtime_json_sidecar_with_max_bytes::<ProcessSessionRecord>(
root,
&process_session_record_relative_path(process_id),
"Agent Runtime process session",
PROCESS_SESSION_RECORD_MAX_BYTES,
)?;
if let Some(record) = record.as_mut() {
let normalized = normalize_process_session_record(record);
validate_process_session_record(root, record, process_id)?;
if normalized {
write_process_session_record(root, record)?;
}
}
Ok(record)
}
pub(super) fn normalize_process_session_record(record: &mut ProcessSessionRecord) -> bool {
let legacy_schema = matches!(record.schema_version.as_str(), "1" | "2");
if !legacy_schema {
return false;
}
if record.schema_version == "1" {
record.sandbox_backend = "legacy-unknown".to_string();
record.sandbox_mode = "unknown".to_string();
record.network_access = "unknown".to_string();
record.sandbox_profile_version = "legacy-v1".to_string();
}
let legacy_active = matches!(
record.status.as_str(),
"prepared" | "launching" | "running" | "terminating"
);
record.schema_version = PROCESS_SESSION_SCHEMA_VERSION.to_string();
record.sandbox_establishment = "unknown".to_string();
record.target_exec = "unknown".to_string();
record.launch_failure_kind = Some(
if legacy_active {
"legacy-active-record"
} else {
"legacy-record"
}
.to_string(),
);
record.sandbox_ready_at = None;
record.exec_established_at = None;
if legacy_active {
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();
}
legacy_active
}
pub(super) fn validate_process_session_record(
root: &Path,
record: &ProcessSessionRecord,
process_id: &str,
) -> Result<(), String> {
if record.schema_version != PROCESS_SESSION_SCHEMA_VERSION
|| record.process_id != process_id
|| record.project_id != game_creator_agent_runtime_context_project_id(root)?
|| record.sandbox_backend.is_empty()
|| record.sandbox_mode.is_empty()
|| record.network_access.is_empty()
|| record.sandbox_profile_version.is_empty()
|| !matches!(
record.sandbox_establishment.as_str(),
"not-established" | "established" | "unknown"
)
|| !matches!(
record.target_exec.as_str(),
"not-attempted" | "established" | "failed" | "unknown"
)
|| record.launch_failure_kind.as_deref().is_some_and(|value| {
!matches!(
value,
"pre-exec-failed"
| "durable-commit-failed"
| "target-exec-failed"
| "launch-unknown"
| "legacy-active-record"
| "legacy-record"
| "start-audit-failed"
)
})
{
return Err("Agent Runtime process session 身份不匹配".to_string());
}
validate_process_id(&record.process_id)?;
if !matches!(
record.status.as_str(),
"prepared"
| "launching"
| "running"
| "terminating"
| "exited"
| "terminated"
| "timed-out"
| "output-limit-exceeded"
| "needs-reconciliation"
| "failed"
) {
return Err("Agent Runtime process session 状态无效".to_string());
}
if matches!(
record.status.as_str(),
"prepared" | "launching" | "running" | "terminating"
) && record.terminal_at.is_some()
{
return Err("运行中的 process session 不应有 terminalAt".to_string());
}
if !matches!(
record.status.as_str(),
"prepared" | "launching" | "running" | "terminating"
) && record.terminal_at.is_none()
{
return Err("终态 process session 缺少 terminalAt".to_string());
}
let launch_state_valid = match record.launch_failure_kind.as_deref() {
None => match record.status.as_str() {
"prepared" => {
record.sandbox_establishment == "not-established"
&& record.target_exec == "not-attempted"
&& !record.needs_reconciliation
}
"launching" => {
matches!(
record.sandbox_establishment.as_str(),
"not-established" | "established"
) && record.target_exec == "not-attempted"
&& !record.needs_reconciliation
}
"running" | "terminating" => {
record.sandbox_establishment == "established"
&& record.target_exec == "established"
&& !record.needs_reconciliation
}
"needs-reconciliation" => {
record.sandbox_establishment == "established"
&& record.target_exec == "established"
&& record.needs_reconciliation
}
"exited" | "terminated" | "timed-out" | "output-limit-exceeded" | "failed" => {
record.sandbox_establishment == "established" && record.target_exec == "established"
}
_ => false,
},
Some("pre-exec-failed") => {
record.status == "failed"
&& record.sandbox_establishment == "not-established"
&& record.target_exec == "not-attempted"
&& !record.needs_reconciliation
}
Some("durable-commit-failed") => {
record.status == "failed"
&& matches!(
record.sandbox_establishment.as_str(),
"not-established" | "established"
)
&& record.target_exec == "not-attempted"
&& !record.needs_reconciliation
}
Some("target-exec-failed") => {
matches!(record.status.as_str(), "failed" | "needs-reconciliation")
&& record.sandbox_establishment == "established"
&& record.target_exec == "failed"
&& (record.status == "needs-reconciliation") == record.needs_reconciliation
}
Some("launch-unknown") => {
record.status == "needs-reconciliation"
&& record.needs_reconciliation
&& record.target_exec == "unknown"
&& matches!(
record.sandbox_establishment.as_str(),
"established" | "unknown"
)
}
Some("legacy-active-record") => {
record.status == "needs-reconciliation"
&& record.needs_reconciliation
&& record.sandbox_establishment == "unknown"
&& record.target_exec == "unknown"
}
Some("legacy-record") => {
!matches!(
record.status.as_str(),
"prepared" | "launching" | "running" | "terminating"
) && record.sandbox_establishment == "unknown"
&& record.target_exec == "unknown"
&& (record.status != "needs-reconciliation" || record.needs_reconciliation)
}
Some("start-audit-failed") => {
record.status == "needs-reconciliation"
&& record.needs_reconciliation
&& record.sandbox_establishment == "established"
&& record.target_exec == "established"
}
Some(_) => false,
};
if !launch_state_valid {
return Err("process session 可信 launch 状态组合无效".to_string());
}
if record.started_at > record.updated_at
|| record.terminal_at.is_some_and(|terminal_at| {
record.started_at > terminal_at || terminal_at > record.updated_at
})
{
return Err("process session 生命周期时间顺序无效".to_string());
}
match record.sandbox_establishment.as_str() {
"established" if record.sandbox_ready_at.is_none() => {
return Err("已建立的 process session sandbox 缺少 ready 时间".to_string());
}
"not-established" | "unknown" if record.sandbox_ready_at.is_some() => {
return Err("未建立或未知的 process session sandbox 不应有 ready 时间".to_string());
}
_ => {}
}
match record.target_exec.as_str() {
"established" if record.exec_established_at.is_none() => {
return Err("已建立的 process session target 缺少 exec 时间".to_string());
}
"not-attempted" | "failed" | "unknown" if record.exec_established_at.is_some() => {
return Err("未建立的 process session target 不应有 exec 时间".to_string());
}
_ => {}
}
if let Some(sandbox_ready_at) = record.sandbox_ready_at {
if record.started_at > sandbox_ready_at
|| sandbox_ready_at > record.updated_at
|| record
.terminal_at
.is_some_and(|terminal_at| sandbox_ready_at > terminal_at)
{
return Err("process session sandbox-ready 时间顺序无效".to_string());
}
}
if let Some(exec_established_at) = record.exec_established_at {
if record
.sandbox_ready_at
.is_none_or(|sandbox_ready_at| sandbox_ready_at > exec_established_at)
|| exec_established_at > record.updated_at
|| record
.terminal_at
.is_some_and(|terminal_at| exec_established_at > terminal_at)
{
return Err("process session exec-established 时间顺序无效".to_string());
}
}
Ok(())
}
pub(super) fn reconcile_stale_active_process_session(record: &mut ProcessSessionRecord) {
let previous_status = record.status.clone();
record.status = "needs-reconciliation".to_string();
record.stdin_open = false;
record.needs_reconciliation = true;
if previous_status == "prepared" {
record.sandbox_establishment = "unknown".to_string();
record.sandbox_ready_at = None;
record.target_exec = "unknown".to_string();
record.exec_established_at = None;
record.launch_failure_kind = Some("launch-unknown".to_string());
} else if previous_status == "launching" {
if record.sandbox_establishment != "established" {
record.sandbox_establishment = "unknown".to_string();
record.sandbox_ready_at = None;
}
record.target_exec = "unknown".to_string();
record.exec_established_at = None;
record.launch_failure_kind = Some("launch-unknown".to_string());
}
record.terminal_at = Some(unix_timestamp());
record.updated_at = unix_timestamp();
}
pub(super) fn process_session_record_from_live(
live: &LiveProcessSession,
output: &ProcessOutputState,
) -> ProcessSessionRecord {
let output_sha256 = format!("{:x}", Sha256::digest(output.text.as_bytes()));
let terminal = output.status != "running";
ProcessSessionRecord {
schema_version: PROCESS_SESSION_SCHEMA_VERSION.to_string(),
project_id: live.identity.project_id.clone(),
agent_id: live.identity.agent_id.clone(),
task_id: live.identity.task_id.clone(),
conversation_session_id: live.identity.conversation_session_id.clone(),
run_id: live.identity.run_id.clone(),
start_action_id: live.identity.start_action_id.clone(),
start_action_fingerprint: live.identity.start_action_fingerprint.clone(),
process_id: live.process_id.clone(),
owner_boot_id: process_session_boot_id().to_string(),
command_id: live.command_id.clone(),
program: live.program.clone(),
cwd: live.cwd.clone(),
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(),
sandbox_establishment: live.sandbox_establishment.clone(),
target_exec: live.target_exec.clone(),
launch_failure_kind: output.launch_failure_kind.clone(),
sandbox_ready_at: live.sandbox_ready_at,
exec_established_at: live.exec_established_at,
status: output.status.clone(),
exit_code: output.exit_code,
signal: output.signal.clone(),
stdin_open: output.stdin_open,
output_bytes: output.text.len(),
output_sha256,
output_ref: Some(process_session_transcript_relative_path(&live.process_id)),
source_fingerprint_before: live.source_fingerprint_before.clone(),
source_fingerprint_after: output.source_fingerprint_after.clone(),
source_changed: output.source_changed,
needs_reconciliation: output.needs_reconciliation,
started_at: live.started_at,
terminal_at: terminal.then(unix_timestamp),
updated_at: unix_timestamp(),
}
}
pub(super) fn initial_process_session_record(
identity: &ProcessSessionIdentity,
process_id: &str,
command_id: &str,
spec: &ProjectCommandSpec,
launch: Option<&ProjectCommandLaunchSpec>,
source_fingerprint_before: &str,
status: &str,
) -> ProcessSessionRecord {
let now = unix_timestamp();
let sandbox_backend = launch
.map(|value| value.sandbox_backend.clone())
.unwrap_or_else(|| "test-unknown".to_string());
let sandbox_mode = launch
.map(|value| value.sandbox_mode.clone())
.unwrap_or_else(|| "unknown".to_string());
let network_access = launch
.map(|value| value.network_access.clone())
.unwrap_or_else(|| "unknown".to_string());
let sandbox_profile_version = launch
.map(|value| value.sandbox_profile_version.clone())
.unwrap_or_else(|| "test-v1".to_string());
ProcessSessionRecord {
schema_version: PROCESS_SESSION_SCHEMA_VERSION.to_string(),
project_id: identity.project_id.clone(),
agent_id: identity.agent_id.clone(),
task_id: identity.task_id.clone(),
conversation_session_id: identity.conversation_session_id.clone(),
run_id: identity.run_id.clone(),
start_action_id: identity.start_action_id.clone(),
start_action_fingerprint: identity.start_action_fingerprint.clone(),
process_id: process_id.to_string(),
owner_boot_id: process_session_boot_id().to_string(),
command_id: command_id.to_string(),
program: spec.program.clone(),
cwd: spec.cwd_relative.clone(),
sandbox_backend,
sandbox_mode,
network_access,
sandbox_profile_version,
sandbox_establishment: "not-established".to_string(),
target_exec: "not-attempted".to_string(),
launch_failure_kind: None,
sandbox_ready_at: None,
exec_established_at: None,
status: status.to_string(),
exit_code: None,
signal: None,
stdin_open: false,
output_bytes: 0,
output_sha256: format!("{:x}", Sha256::digest([])),
output_ref: None,
source_fingerprint_before: source_fingerprint_before.to_string(),
source_fingerprint_after: None,
source_changed: None,
needs_reconciliation: false,
started_at: now,
terminal_at: None,
updated_at: now,
}
}
#[cfg(not(target_os = "linux"))]
pub(super) fn process_session_launch_failed(
root: &Path,
record: &mut ProcessSessionRecord,
error: String,
) -> String {
record.status = "failed".to_string();
record.target_exec = "not-attempted".to_string();
record.launch_failure_kind = Some("pre-exec-failed".to_string());
record.stdin_open = false;
record.terminal_at = Some(unix_timestamp());
record.updated_at = unix_timestamp();
match write_process_session_record(root, record) {
Ok(()) => error,
Err(record_error) => format!("{error}process session 失败终态无法落盘:{record_error}"),
}
}
pub(super) fn validate_process_session_access(
record: &ProcessSessionRecord,
identity: &ProcessSessionIdentity,
) -> Result<(), String> {
if record.project_id != identity.project_id
|| record.agent_id != identity.agent_id
|| record.task_id != identity.task_id
|| record.conversation_session_id != identity.conversation_session_id
|| record.run_id != identity.run_id
{
return Err("process session 不属于当前 Agent run".to_string());
}
Ok(())
}
pub(crate) fn process_session_identity_for_run_at(
root: &Path,
agent_id: &str,
task_id: &str,
conversation_session_id: &str,
run_id: &str,
process_id: &str,
) -> Result<ProcessSessionIdentity, String> {
let record = read_process_session_record(root, process_id)?
.ok_or_else(|| "process session 不存在".to_string())?;
if record.agent_id != agent_id
|| record.task_id != task_id
|| record.conversation_session_id != conversation_session_id
|| record.run_id != run_id
{
return Err("process session 不属于当前 Agent run".to_string());
}
Ok(ProcessSessionIdentity {
project_id: record.project_id,
agent_id: record.agent_id,
task_id: record.task_id,
conversation_session_id: record.conversation_session_id,
run_id: record.run_id,
start_action_id: record.start_action_id,
start_action_fingerprint: record.start_action_fingerprint,
})
}
pub(super) fn validate_process_session_transcript(
transcript: &ProcessSessionTranscript,
record: &ProcessSessionRecord,
) -> Result<(), String> {
let output_sha256 = format!("{:x}", Sha256::digest(transcript.output.as_bytes()));
if !matches!(transcript.schema_version.as_str(), "1" | "2")
|| transcript.project_id != record.project_id
|| transcript.agent_id != record.agent_id
|| transcript.task_id != record.task_id
|| transcript.conversation_session_id != record.conversation_session_id
|| transcript.run_id != record.run_id
|| transcript.start_action_id != record.start_action_id
|| transcript.start_action_fingerprint != record.start_action_fingerprint
|| transcript.process_id != record.process_id
|| transcript.output_bytes != transcript.output.len()
|| transcript.output_sha256 != output_sha256
|| record.output_bytes != transcript.output_bytes
|| record.output_sha256 != transcript.output_sha256
{
return Err("Agent Runtime process transcript 身份或摘要不匹配".to_string());
}
Ok(())
}
pub(super) fn find_existing_start_action_record(
root: &Path,
identity: &ProcessSessionIdentity,
) -> Result<Option<ProcessSessionRecord>, String> {
let directory = root.join(".agent/runtime/process-sessions");
let entries = match fs::read_dir(&directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(format!("读取 process session 目录失败:{error}")),
};
for entry in entries {
let entry = entry.map_err(|error| format!("读取 process session 目录项失败:{error}"))?;
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
let Some(process_id) = name
.strip_suffix(".json")
.filter(|value| !value.ends_with(".output"))
else {
continue;
};
if validate_process_id(process_id).is_err() {
continue;
}
let Some(record) = read_process_session_record(root, process_id)? else {
continue;
};
if record.agent_id == identity.agent_id
&& record.run_id == identity.run_id
&& record.start_action_id == identity.start_action_id
{
if record.start_action_fingerprint != identity.start_action_fingerprint {
return Err("command.start action identity 冲突".to_string());
}
return Ok(Some(record));
}
}
Ok(None)
}