feat(pncc): coordinate safe organ lifecycles

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 execution limb remain deferred
This commit is contained in:
铸渊 / ICE-P-ZY001 2026-08-11 03:00:12 +08:00
commit 8f35834a7a
11 changed files with 402 additions and 39 deletions

View file

@ -528,6 +528,7 @@ macro_rules! app_invoke_handler {
persona_code_channel::query_persona_code_channel_runtime,
persona_code_channel::run_persona_code_channel_fact_task,
persona_code_channel::run_persona_code_channel_memory_metabolism,
persona_code_channel::run_persona_code_channel_lifecycle,
persona_code_channel::inspect_persona_code_channel_session,
persona_code_channel::recover_persona_code_channel_session,
guanghu_router::guanghu_router_connect,

View file

@ -282,6 +282,44 @@ pub struct PersonaMemoryMetabolismReceipt {
pub attribution: PersonaAttribution,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "kind", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PersonaLifecycleOperationInput {
FactSense {
question: String,
provider: crate::ai_models::AiModelProvider,
api_key_override: Option<String>,
},
MemoryMetabolism {
source_session_id: String,
},
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaLifecycleRunInput {
pub wake: PersonaWakeInput,
pub operation: PersonaLifecycleOperationInput,
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind", content = "receipt", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PersonaLifecycleCompletionReceipt {
FactSense(PersonaFactTaskReceipt),
MemoryMetabolism(PersonaMemoryMetabolismReceipt),
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaLifecycleRunReceipt {
pub schema: &'static str,
pub session_id: String,
pub persona_id: String,
pub repository_path: String,
pub wake_receipt: PersonaWakeReceipt,
pub completion: PersonaLifecycleCompletionReceipt,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaSessionControlInput {
@ -2330,6 +2368,138 @@ fn run_memory_metabolism_at(
})
}
fn validate_lifecycle_operation_before_wake(
input: &PersonaLifecycleRunInput,
) -> Result<(), String> {
let inspection = inspect_manifest_at(PersonaManifestInspectionInput {
repository_path: input.wake.repository_path.clone(),
expected_persona_id: input.wake.expected_persona_id.clone(),
expected_head: input.wake.expected_head.clone(),
})?;
let contract = inspection
.organ_contracts
.iter()
.find(|contract| contract.organ_id == input.wake.organ_id)
.ok_or_else(|| "ORGAN_NOT_DECLARED".to_string())?;
let expected_kind = match &input.operation {
PersonaLifecycleOperationInput::FactSense { provider, .. } => {
let repository = Path::new(&input.wake.repository_path)
.canonicalize()
.map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?;
let manifest = load_manifest(&repository)?;
let provider_base_url = provider.base_url.as_deref().unwrap_or("").trim();
if provider.id != input.wake.model_provider_id
|| manifest.model_binding.base_url != provider_base_url
|| !provider
.models
.iter()
.any(|model| model.id == input.wake.model_id)
{
return Err("PERSONA_MODEL_BINDING_MISMATCH".into());
}
PersonaOrganKind::FactSense
}
PersonaLifecycleOperationInput::MemoryMetabolism { .. } => {
PersonaOrganKind::MemoryMetabolism
}
};
if contract.kind != expected_kind {
return Err("PERSONA_LIFECYCLE_OPERATION_ORGAN_MISMATCH".into());
}
if !contract.activatable || contract.kind == PersonaOrganKind::ExecutionLimb {
return Err("ORGAN_NOT_ACTIVATABLE".into());
}
Ok(())
}
fn run_fact_task_with_bound_provider_at(
runtime_root: &Path,
input: PersonaFactTaskInput,
timestamp: &str,
) -> Result<PersonaFactTaskReceipt, String> {
let provider = input.provider.clone();
let model_id = input.model_id.clone();
let api_key_override = input.api_key_override.clone();
run_fact_task_at(
runtime_root,
input,
timestamp,
move |system_prompt, message| {
let mut text = String::new();
crate::ai_models::run_ai_model_stream(
crate::ai_models::AiModelStreamRequest {
provider,
model_id,
message,
system_prompt: Some(system_prompt),
vault_path: None,
vault_paths: Vec::new(),
api_key_override,
event_name: None,
},
|event| {
if let crate::ai_agents::AiAgentStreamEvent::TextDelta { text: delta } = event {
text.push_str(&delta);
}
},
)?;
Ok(text)
},
)
}
fn run_lifecycle_at<F>(
runtime_root: &Path,
input: PersonaLifecycleRunInput,
session_id: &str,
wake_timestamp: &str,
operation_timestamp: &str,
run_fact: F,
) -> Result<PersonaLifecycleRunReceipt, String>
where
F: FnOnce(&Path, PersonaFactTaskInput, &str) -> Result<PersonaFactTaskReceipt, String>,
{
validate_lifecycle_operation_before_wake(&input)?;
let model_id = input.wake.model_id.clone();
let wake = input.wake;
let wake_receipt = prepare_wake_at(runtime_root, wake, session_id, wake_timestamp)?;
let completion = match input.operation {
PersonaLifecycleOperationInput::FactSense {
question,
provider,
api_key_override,
} => PersonaLifecycleCompletionReceipt::FactSense(run_fact(
runtime_root,
PersonaFactTaskInput {
session_id: wake_receipt.session_id.clone(),
question,
model_id,
provider,
api_key_override,
},
operation_timestamp,
)?),
PersonaLifecycleOperationInput::MemoryMetabolism { source_session_id } => {
PersonaLifecycleCompletionReceipt::MemoryMetabolism(run_memory_metabolism_at(
runtime_root,
PersonaMemoryMetabolismInput {
session_id: wake_receipt.session_id.clone(),
source_session_id,
},
operation_timestamp,
)?)
}
};
Ok(PersonaLifecycleRunReceipt {
schema: "hololake.pncc-lifecycle-run-receipt/v1",
session_id: wake_receipt.session_id.clone(),
persona_id: wake_receipt.persona_id.clone(),
repository_path: wake_receipt.repository_path.clone(),
wake_receipt,
completion,
})
}
#[tauri::command]
pub fn prepare_persona_code_channel_wake(
input: PersonaWakeInput,
@ -2362,37 +2532,7 @@ pub async fn run_persona_code_channel_fact_task(
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
tokio::task::spawn_blocking(move || {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let provider = input.provider.clone();
let model_id = input.model_id.clone();
let api_key_override = input.api_key_override.clone();
run_fact_task_at(
&runtime_root,
input,
&timestamp,
move |system_prompt, message| {
let mut text = String::new();
crate::ai_models::run_ai_model_stream(
crate::ai_models::AiModelStreamRequest {
provider,
model_id,
message,
system_prompt: Some(system_prompt),
vault_path: None,
vault_paths: Vec::new(),
api_key_override,
event_name: None,
},
|event| {
if let crate::ai_agents::AiAgentStreamEvent::TextDelta { text: delta } =
event
{
text.push_str(&delta);
}
},
)?;
Ok(text)
},
)
run_fact_task_with_bound_provider_at(&runtime_root, input, &timestamp)
})
.await
.map_err(|error| format!("PERSONA_FACT_TASK_JOIN_FAILED: {error}"))?
@ -2411,6 +2551,28 @@ pub async fn run_persona_code_channel_memory_metabolism(
.map_err(|error| format!("PERSONA_MEMORY_METABOLISM_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn run_persona_code_channel_lifecycle(
input: PersonaLifecycleRunInput,
) -> Result<PersonaLifecycleRunReceipt, String> {
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
tokio::task::spawn_blocking(move || {
let session_id = format!("PNCC-{}", Uuid::new_v4());
let wake_timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let operation_timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
run_lifecycle_at(
&runtime_root,
input,
&session_id,
&wake_timestamp,
&operation_timestamp,
run_fact_task_with_bound_provider_at,
)
})
.await
.map_err(|error| format!("PERSONA_LIFECYCLE_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub fn inspect_persona_code_channel_session(
input: PersonaSessionControlInput,
@ -2581,6 +2743,18 @@ mod tests {
}
}
fn lifecycle_fact_input(repo: &Path) -> PersonaLifecycleRunInput {
let fact = fact_task_input("unused-until-coordinator-creates-session");
PersonaLifecycleRunInput {
wake: wake_input(repo),
operation: PersonaLifecycleOperationInput::FactSense {
question: fact.question,
provider: fact.provider,
api_key_override: fact.api_key_override,
},
}
}
fn session_control(repo: &Path, session_id: &str) -> PersonaSessionControlInput {
PersonaSessionControlInput {
session_id: session_id.into(),
@ -2803,6 +2977,126 @@ mod tests {
require_clean_repository(repo.path()).unwrap();
}
#[test]
fn coordinates_one_registered_fact_lifecycle_without_ui_or_execution_limb() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let receipt = run_lifecycle_at(
runtime.path(),
lifecycle_fact_input(repo.path()),
"PNCC-COORDINATED-FACT",
"2026-08-11T00:00:00.000Z",
"2026-08-11T00:00:01.000Z",
|runtime_root, input, timestamp| {
run_fact_task_at(runtime_root, input, timestamp, |_, _| {
Ok(r#"{"summary":"Coordinated verified fact.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
assert_eq!(receipt.schema, "hololake.pncc-lifecycle-run-receipt/v1");
assert_eq!(receipt.session_id, "PNCC-COORDINATED-FACT");
assert_eq!(receipt.wake_receipt.events[0].kind, "WAKING");
assert_eq!(receipt.wake_receipt.events[2].kind, "ORGAN_ACTIVE");
match receipt.completion {
PersonaLifecycleCompletionReceipt::FactSense(completion) => {
assert_eq!(completion.runtime_state, "DORMANT");
assert_eq!(completion.events.last().unwrap().kind, "DORMANT");
assert!(completion.model_inference_completed);
}
PersonaLifecycleCompletionReceipt::MemoryMetabolism(_) => {
panic!("coordinator returned the wrong completion kind")
}
}
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
require_clean_repository(repo.path()).unwrap();
}
#[test]
fn rejects_a_lifecycle_operation_organ_mismatch_before_wake_or_lease() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = PersonaLifecycleRunInput {
wake: wake_input(repo.path()),
operation: PersonaLifecycleOperationInput::MemoryMetabolism {
source_session_id: "PNCC-SOURCE-NOT-USED".into(),
},
};
let error = run_lifecycle_at(
runtime.path(),
input,
"PNCC-COORDINATOR-MISMATCH",
"2026-08-11T00:00:00.000Z",
"2026-08-11T00:00:01.000Z",
|_, _, _| panic!("mismatched lifecycle must not run an organ"),
)
.unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_OPERATION_ORGAN_MISMATCH"));
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
assert!(!runtime
.path()
.join("sessions/PNCC-COORDINATOR-MISMATCH")
.exists());
}
#[test]
fn coordinates_registered_memory_metabolism_after_a_verified_fact_session() {
let repo = persona_repo();
declare_memory_organ(repo.path());
let runtime = tempfile::TempDir::new().unwrap();
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
"PNCC-COORDINATOR-SOURCE",
"2026-08-11T00:00:00.000Z",
)
.unwrap();
run_fact_task_at(
runtime.path(),
fact_task_input("PNCC-COORDINATOR-SOURCE"),
"2026-08-11T00:00:01.000Z",
|_, _| {
Ok(r#"{"summary":"Verified memory candidate.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
},
)
.unwrap();
let mut wake = wake_input(repo.path());
wake.expected_head = head(repo.path());
wake.organ_id = "memory-metabolism.checkpoint".into();
let receipt = run_lifecycle_at(
runtime.path(),
PersonaLifecycleRunInput {
wake,
operation: PersonaLifecycleOperationInput::MemoryMetabolism {
source_session_id: "PNCC-COORDINATOR-SOURCE".into(),
},
},
"PNCC-COORDINATED-MEMORY",
"2026-08-11T00:00:02.000Z",
"2026-08-11T00:00:03.000Z",
|_, _, _| panic!("memory lifecycle must not invoke a model"),
)
.unwrap();
match receipt.completion {
PersonaLifecycleCompletionReceipt::MemoryMetabolism(completion) => {
assert_eq!(completion.source_session_id, "PNCC-COORDINATOR-SOURCE");
assert!(!completion.model_inference_started);
assert_eq!(completion.runtime_state, "DORMANT");
assert_eq!(completion.events[3].kind, "MEMORY_CANDIDATE_VERIFIED");
assert_eq!(completion.events.last().unwrap().kind, "DORMANT");
}
PersonaLifecycleCompletionReceipt::FactSense(_) => {
panic!("coordinator returned the wrong completion kind")
}
}
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
require_clean_repository(repo.path()).unwrap();
}
#[test]
fn independently_promotes_only_the_current_verified_structured_checkpoint() {
let repo = persona_repo();