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

@ -169,6 +169,7 @@ typed_organ_contract_and_manifest_inspection_source_implemented: 100
durable_event_and_receipt_query_source_implemented: 100
independent_memory_metabolism_source_implemented: 100
memory_failure_closure_and_nonblocking_runtime_command_implemented: 100
non_ui_safe_organ_lifecycle_coordinator_source_implemented: 100
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0
@ -193,6 +194,11 @@ runtime_health: 0
SHA-256 与来源事件哈希后生成新检查点并以人格 Git 身份提交。执行手脚仍不可激活,清单声明不会
自动获得 shell 或现实动作权限。
非 UI 生命周期协调器现已把一次精确唤醒和一个已实现安全器官的完整事务组合成单一原生命令。
它只接受 `FACT_SENSE``MEMORY_METABOLISM`,在取得人格主租约前校验操作类型与清单器官匹配,
并返回同一会话的唤醒回执和类型化完成回执。`EXECUTION_LIMB` 不在协调输入中,仍为不可激活;
协调器也没有增加第二状态库、后台调度器或 UI 权限来源。
记忆代谢的 Tauri 命令现已进入阻塞任务池,不占用桌面命令线程。来源拒绝、检查点冲突和提交前
失败必须依次持久化失败事件、器官释放与休眠,写回 `DORMANT_AFTER_FAILURE` 后才释放精确主锁;
闭环任一步失败时返回 `MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY`,不能吞掉二次失败。

View file

@ -80,6 +80,13 @@ command returns `MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY`; it never
candidate error while silently losing the closure failure. An incomplete closure preserves the lease for the
existing evidence-bound session inspection and recovery path.
`PersonaLifecycleRunInput` is the single-call, non-UI coordination envelope. Its wake member keeps the exact
persona Git, expected commit, node/model instance, organ id, and structured attribution explicit. Its tagged
operation can be only `FACT_SENSE` or `MEMORY_METABOLISM`. The coordinator rejects an operation/organ mismatch
before wake or lease acquisition, then delegates to the same tested organ transactions and returns a tagged
completion beside the wake receipt. It does not accept an execution action, infer organ permissions from UI
state, or create a parallel lifecycle record.
`PersonaRuntimeQueryReceipt` is a bounded projection of the durable runtime files, not another truth store.
It filters by the caller's expected persona and canonical repository, validates each matching event chain,
and returns at most 100 newest session summaries. Dormant sessions expose no active organ. The receipt keeps

View file

@ -50,6 +50,14 @@ the command returns an explicit recovery-required error and retains the lease wh
proven complete; it does not swallow the secondary failure.
The execution limb remains visible but non-activatable, so declaring it does not grant a shell or reality action.
`run_persona_code_channel_lifecycle` is the non-UI coordinator for the two implemented safe organ kinds. One
request supplies the exact wake envelope plus either a fact-sense operation or a memory-metabolism operation.
Before acquiring the primary lease, the coordinator inspects the same manifest, confirms that the requested
operation kind matches the named activatable organ, and preflights the manifest-pinned model binding for fact
sense. It then owns one complete session from wake through the existing organ transaction and returns both
the wake receipt and the typed completion receipt. It cannot express or activate `EXECUTION_LIMB`, does not
add a scheduler or second state store, and runs the complete lifecycle on the blocking task pool.
`query_persona_code_channel_runtime` is the bounded read model for later projection surfaces. The caller must
name one exact persona and canonical repository and may request at most 100 sessions. The kernel reads the
existing session records and event journals directly, verifies every returned hash chain, sorts by the last

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();