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:
parent
18944f5362
commit
8f35834a7a
11 changed files with 402 additions and 39 deletions
|
|
@ -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`,不能吞掉二次失败。
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
×tamp,
|
||||
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, ×tamp)
|
||||
})
|
||||
.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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue