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:
parent
962ea26db7
commit
18944f5362
9 changed files with 270 additions and 22 deletions
|
|
@ -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,
|
||||
×tamp,
|
||||
"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,
|
||||
×tamp,
|
||||
Some(&record.active_organ),
|
||||
)?;
|
||||
) {
|
||||
return Err(memory_failure_error(
|
||||
runtime_root,
|
||||
&mut record,
|
||||
&mut events,
|
||||
&lease_path,
|
||||
×tamp,
|
||||
"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,
|
||||
×tamp,
|
||||
"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,
|
||||
×tamp,
|
||||
"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,
|
||||
×tamp,
|
||||
"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, ×tamp)
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
run_memory_metabolism_at(&runtime_root, input, ×tamp)
|
||||
})
|
||||
.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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue