feat(pncc): close memory failures safely

GuangHu-Human-Responsibility: ICE-GL∞ / 冰朔
GuangHu-Persona-Cognitive-Author: ICE-P-ZY001 / 铸渊
GuangHu-Execution-Runtime: Codex macOS / DEV-20260810-014
GuangHu-Development-ID: DEV-20260810-014
GuangHu-Authorization-Scope: GH-PNCC local runtime development and registered REPO-014 publication
GuangHu-Source-Language-Anchor: continue PNCC persona runtime; UI and human projection aesthetics remain deferred
This commit is contained in:
铸渊 / ICE-P-ZY001 2026-08-11 02:38:28 +08:00
commit 18944f5362
9 changed files with 270 additions and 22 deletions

View file

@ -1367,6 +1367,102 @@ fn finish_failed_session(
let _ = fs::remove_file(lease_path);
}
fn finish_failed_session_checked_with_release<F>(
runtime_root: &Path,
record: &mut PersonaSessionRecord,
events: &mut Vec<PersonaLifecycleEvent>,
lease_path: &Path,
timestamp: &str,
failure_kind: &str,
release_lease: F,
) -> Result<(), String>
where
F: FnOnce(&Path) -> std::io::Result<()>,
{
let git_head = record.git_head.clone();
let organ = record.active_organ.clone();
append_event(
runtime_root,
record,
events,
failure_kind,
&git_head,
timestamp,
Some(&organ),
)?;
append_event(
runtime_root,
record,
events,
"ORGAN_RELEASED",
&git_head,
timestamp,
Some(&organ),
)?;
append_event(
runtime_root,
record,
events,
"DORMANT",
&git_head,
timestamp,
None,
)?;
record.state = "DORMANT_AFTER_FAILURE".into();
write_session_record(runtime_root, record)?;
release_lease(lease_path)
.map_err(|error| format!("PERSONA_PRIMARY_LEASE_RELEASE_FAILED: {error}"))
}
fn memory_failure_error(
runtime_root: &Path,
record: &mut PersonaSessionRecord,
events: &mut Vec<PersonaLifecycleEvent>,
lease_path: &Path,
timestamp: &str,
failure_kind: &str,
cause: String,
) -> String {
memory_failure_error_with_release(
runtime_root,
record,
events,
lease_path,
timestamp,
(failure_kind, cause),
|path| fs::remove_file(path),
)
}
fn memory_failure_error_with_release<F>(
runtime_root: &Path,
record: &mut PersonaSessionRecord,
events: &mut Vec<PersonaLifecycleEvent>,
lease_path: &Path,
timestamp: &str,
failure: (&str, String),
release_lease: F,
) -> String
where
F: FnOnce(&Path) -> std::io::Result<()>,
{
let (failure_kind, cause) = failure;
match finish_failed_session_checked_with_release(
runtime_root,
record,
events,
lease_path,
timestamp,
failure_kind,
release_lease,
) {
Ok(()) => cause,
Err(closure_error) => {
format!("{cause}; MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY: {closure_error}")
}
}
}
fn validate_recoverable_persona_commit(
repository: &Path,
record: &PersonaSessionRecord,
@ -2066,18 +2162,18 @@ fn run_memory_metabolism_at(
) {
Ok(source) => source,
Err(error) => {
finish_failed_session(
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"MEMORY_SOURCE_REJECTED",
);
return Err(error);
error,
));
}
};
append_event(
if let Err(error) = append_event(
runtime_root,
&record,
&mut events,
@ -2085,7 +2181,17 @@ fn run_memory_metabolism_at(
&observed_head,
&timestamp,
Some(&record.active_organ),
)?;
) {
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"MEMORY_CANDIDATE_EVENT_FAILED",
error,
));
}
let checkpoint_relative = format!(
".hololake/persona/checkpoints/{}.json",
@ -2093,15 +2199,15 @@ fn run_memory_metabolism_at(
);
let checkpoint_path = exact_repository.join(&checkpoint_relative);
if checkpoint_path.exists() {
finish_failed_session(
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"CHECKPOINT_COLLISION",
);
return Err("PERSONA_CHECKPOINT_ALREADY_EXISTS".into());
"PERSONA_CHECKPOINT_ALREADY_EXISTS".into(),
));
}
let checkpoint = serde_json::json!({
"schema": "hololake.persona-checkpoint/v1",
@ -2118,8 +2224,20 @@ fn run_memory_metabolism_at(
"attribution": record.attribution.clone(),
});
let manifest_path = exact_repository.join(MANIFEST_PATH);
let original_manifest = fs::read(&manifest_path)
.map_err(|error| format!("PERSONA_MANIFEST_READ_FAILED: {error}"))?;
let original_manifest = match fs::read(&manifest_path) {
Ok(manifest) => manifest,
Err(error) => {
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"MEMORY_MANIFEST_READ_FAILED",
format!("PERSONA_MANIFEST_READ_FAILED: {error}"),
));
}
};
let mut promoted_manifest = manifest;
promoted_manifest.current_checkpoint = checkpoint_relative.clone();
let committed_head = match (|| {
@ -2147,16 +2265,20 @@ fn run_memory_metabolism_at(
&checkpoint_relative,
&original_manifest,
);
finish_failed_session(
if let Err(rollback_error) = rollback {
return Err(format!(
"{error}; MEMORY_CHECKPOINT_ROLLBACK_INCOMPLETE_REQUIRES_RECOVERY: {rollback_error}"
));
}
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"CHECKPOINT_COMMIT_FAILED",
);
rollback?;
return Err(error);
error,
));
}
}
};
@ -2277,12 +2399,16 @@ pub async fn run_persona_code_channel_fact_task(
}
#[tauri::command]
pub fn run_persona_code_channel_memory_metabolism(
pub async fn run_persona_code_channel_memory_metabolism(
input: PersonaMemoryMetabolismInput,
) -> Result<PersonaMemoryMetabolismReceipt, String> {
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
run_memory_metabolism_at(&runtime_root, input, &timestamp)
tokio::task::spawn_blocking(move || {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
run_memory_metabolism_at(&runtime_root, input, &timestamp)
})
.await
.map_err(|error| format!("PERSONA_MEMORY_METABOLISM_JOIN_FAILED: {error}"))?
}
#[tauri::command]
@ -2789,9 +2915,67 @@ mod tests {
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
let record = load_session_record(runtime.path(), "PNCC-CORRUPT-MEMORY").unwrap();
assert_eq!(record.state, "DORMANT_AFTER_FAILURE");
let events = verify_event_journal(runtime.path(), &record).unwrap();
let terminal_kinds = events
.iter()
.rev()
.take(3)
.map(|event| event.kind.as_str())
.collect::<Vec<_>>();
assert_eq!(
terminal_kinds,
vec!["DORMANT", "ORGAN_RELEASED", "MEMORY_SOURCE_REJECTED"]
);
require_clean_repository(repo.path()).unwrap();
}
#[test]
fn reports_an_incomplete_memory_failure_closure_and_preserves_the_lease() {
let repo = persona_repo();
declare_memory_organ(repo.path());
let runtime = tempfile::TempDir::new().unwrap();
let mut memory_wake = wake_input(repo.path());
memory_wake.organ_id = "memory-metabolism.checkpoint".into();
memory_wake.expected_head = head(repo.path());
prepare_wake_at(
runtime.path(),
memory_wake,
"PNCC-MEMORY-CLOSURE",
"2026-08-11T00:00:00.000Z",
)
.unwrap();
let mut record = load_session_record(runtime.path(), "PNCC-MEMORY-CLOSURE").unwrap();
let mut events = read_ready_event_journal(runtime.path(), &record).unwrap();
let lease_path = runtime.path().join("leases/ICE-P-ZY001.json");
let error = memory_failure_error_with_release(
runtime.path(),
&mut record,
&mut events,
&lease_path,
"2026-08-11T00:00:01.000Z",
("MEMORY_TEST_FAILURE", "MEMORY_TEST_CAUSE".into()),
|_| {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"injected lease release refusal",
))
},
);
assert!(error.contains("MEMORY_TEST_CAUSE"));
assert!(error.contains("MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY"));
assert!(error.contains("PERSONA_PRIMARY_LEASE_RELEASE_FAILED"));
assert!(lease_path.is_file());
assert_eq!(
require_primary_lease(runtime.path(), &record).unwrap(),
lease_path
);
let persisted = load_session_record(runtime.path(), "PNCC-MEMORY-CLOSURE").unwrap();
assert_eq!(persisted.state, "DORMANT_AFTER_FAILURE");
let persisted_events = verify_event_journal(runtime.path(), &persisted).unwrap();
assert_eq!(persisted_events.last().unwrap().kind, "DORMANT");
}
#[test]
fn rejects_unscoped_model_evidence_and_releases_primary() {
let repo = persona_repo();