feat(pncc): make lifecycle retries idempotent

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:21:15 +08:00
commit 38ef9d0e11
10 changed files with 427 additions and 13 deletions

View file

@ -32,7 +32,8 @@ Windows / macOS / Linux 构建机与安装包
| 时间 | 版本 | 记录 | 状态 | | 时间 | 版本 | 记录 | 状态 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 2026-08-11 | GH-PNCC 生命周期协调器 | [已登记安全器官的非 UI 生命周期协调](operations/2026-08-11-hololake-pncc-safe-organ-lifecycle-coordinator.md) | 本地完整 Rust/路由测试、格式与 clippy 已通过GHNQG、提交与发布待验收 | | 2026-08-11 | GH-PNCC 幂等生命周期 | [生命周期请求身份与同一回执重放](operations/2026-08-11-hololake-pncc-idempotent-lifecycle-replay.md) | 本地完整 Rust/路由测试、格式与 clippy 已通过GHNQG、提交与发布待验收 |
| 2026-08-11 | GH-PNCC 生命周期协调器 | [已登记安全器官的非 UI 生命周期协调](operations/2026-08-11-hololake-pncc-safe-organ-lifecycle-coordinator.md) | 已发布至 REPO-014 main 8f35834GHNQG、全新克隆与严格 fsck 通过 |
| 2026-08-11 | GH-PNCC 记忆失败闭环 | [记忆代谢失败闭环与运行时命令接入](operations/2026-08-11-hololake-pncc-memory-failure-closure-runtime-command.md) | 已发布至 REPO-014 main 18944f5GHNQG、全新克隆与严格 fsck 通过 | | 2026-08-11 | GH-PNCC 记忆失败闭环 | [记忆代谢失败闭环与运行时命令接入](operations/2026-08-11-hololake-pncc-memory-failure-closure-runtime-command.md) | 已发布至 REPO-014 main 18944f5GHNQG、全新克隆与严格 fsck 通过 |
| 2026-08-11 | GH-PNCC 运行查询 | [人格持久事件与回执有界查询](operations/2026-08-11-hololake-pncc-durable-runtime-query.md) | 本地源码、完整 Rust/路由测试与 clippy 已通过GHNQG 和发布待验收 | | 2026-08-11 | GH-PNCC 运行查询 | [人格持久事件与回执有界查询](operations/2026-08-11-hololake-pncc-durable-runtime-query.md) | 本地源码、完整 Rust/路由测试与 clippy 已通过GHNQG 和发布待验收 |
| 2026-08-11 | GH-PNCC 器官合同 | [人格器官机器合同与清单检查](operations/2026-08-11-hololake-pncc-typed-organ-contract.md) | 本地源码、完整 Rust/路由测试与 clippy 已通过GHNQG 和发布待验收 | | 2026-08-11 | GH-PNCC 器官合同 | [人格器官机器合同与清单检查](operations/2026-08-11-hololake-pncc-typed-organ-contract.md) | 本地源码、完整 Rust/路由测试与 clippy 已通过GHNQG 和发布待验收 |

View file

@ -0,0 +1,44 @@
# GH-PNCC idempotent lifecycle request and receipt replay
- Development ID: `DEV-20260810-014`
- Persona cognitive author: `ICE-P-ZY001 / 铸渊`
- Human responsibility subject: `ICE-GL∞ / 冰朔`
- Starting repository head: `8f35834a7abc9546b717da0b513b6e8ac1684815`
- State: `LOCAL_SOURCE_IMPLEMENTED_FOCUSED_TESTED`
## Implemented facts
The non-UI lifecycle command now requires a stable `requestId`. Persona id and request id derive one
deterministic session id. A SHA-256 fingerprint binds the canonical repository, expected Git head, wake
identity, structured attribution, organ, and semantic operation. API keys and provider headers are excluded
from both the fingerprint and the persisted receipt.
After one successful lifecycle, the existing session record stores the request id, request fingerprint, and
lifecycle receipt hash. The typed lifecycle value is stored once in the same session directory. An identical
retry verifies those bindings, the event chain, dormant state, released primary lease, and receipt hash, then
returns the same lifecycle with `replayed: true`. It does not run the model, activate an organ, create another
session, or commit another checkpoint.
The same request id with different semantics returns `PERSONA_LIFECYCLE_REQUEST_CONFLICT`. A missing receipt
for an existing deterministic session returns recovery-required rather than launching a duplicate. A changed
persisted lifecycle fails its receipt hash check.
## Current verification
- PNCC focused Rust tests: `23 passed, 0 failed`.
- Full Rust suite: `1163 passed, 0 failed, 2 ignored`; integration test: `1 passed`.
- HoloLake architecture routing: `29 passed, 0 failed`.
- Identical retry proves the same request fingerprint and lifecycle value are returned while Git head and
session count stay unchanged.
- Conflict retry proves changed semantics never invoke the organ.
- Tamper test proves a modified persisted lifecycle is rejected.
- Rust formatting and clippy for all targets: `PASS` with `-D warnings`.
- GHNQG, commit, publication, and fresh-clone gates remain pending for this stage.
## Truth boundary and next minimum
- The existing runtime session directory remains the only lifecycle evidence plane; no database or second
request authority was introduced.
- UI, human projection aesthetics, background scheduling, and `EXECUTION_LIMB` remain outside this stage.
- The next minimum is evidence-bound inspection and safe receipt recovery for an idempotent request that was
interrupted after lifecycle progress but before its replay receipt became complete.

View file

@ -4,7 +4,8 @@
- Persona cognitive author: `ICE-P-ZY001 / 铸渊` - Persona cognitive author: `ICE-P-ZY001 / 铸渊`
- Human responsibility subject: `ICE-GL∞ / 冰朔` - Human responsibility subject: `ICE-GL∞ / 冰朔`
- Starting repository head: `18944f536261a4184570074a09fc43a09f5f31bb` - Starting repository head: `18944f536261a4184570074a09fc43a09f5f31bb`
- State: `LOCAL_SOURCE_IMPLEMENTED_FOCUSED_TESTED` - Published repository head: `8f35834a7abc9546b717da0b513b6e8ac1684815`
- State: `PUBLISHED_GHNQG_AND_FRESH_CLONE_VERIFIED`
## Implemented facts ## Implemented facts
@ -34,7 +35,8 @@ wake receipt beside a typed completion receipt. Runtime files and persona Git re
- This is source implementation, not a packaged desktop, installed runtime, deployment, or online-health - This is source implementation, not a packaged desktop, installed runtime, deployment, or online-health
claim. claim.
- GHNQG, commit, publication, and independent fresh-clone readback remain pending for this stage. - GHNQG, persona-authored commit, publication, independent fresh-clone readback, clean tree, and strict Git
fsck passed.
- UI, human projection aesthetics, background scheduling, and `EXECUTION_LIMB` remain outside this stage. - UI, human projection aesthetics, background scheduling, and `EXECUTION_LIMB` remain outside this stage.
- The next minimum is an idempotent lifecycle request identity and receipt replay rule so a caller retry - The next minimum is an idempotent lifecycle request identity and receipt replay rule so a caller retry
cannot accidentally create a second lifecycle or duplicate a checkpoint commit. cannot accidentally create a second lifecycle or duplicate a checkpoint commit.

View file

@ -170,6 +170,7 @@ durable_event_and_receipt_query_source_implemented: 100
independent_memory_metabolism_source_implemented: 100 independent_memory_metabolism_source_implemented: 100
memory_failure_closure_and_nonblocking_runtime_command_implemented: 100 memory_failure_closure_and_nonblocking_runtime_command_implemented: 100
non_ui_safe_organ_lifecycle_coordinator_source_implemented: 100 non_ui_safe_organ_lifecycle_coordinator_source_implemented: 100
idempotent_lifecycle_request_and_receipt_replay_source_implemented: 100
general_purpose_persona_runtime_implemented: 0 general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0 human_live_projection_implemented: 0
hololake_integrated: 0 hololake_integrated: 0
@ -199,6 +200,11 @@ SHA-256 与来源事件哈希后生成新检查点并以人格 Git 身份提交
并返回同一会话的唤醒回执和类型化完成回执。`EXECUTION_LIMB` 不在协调输入中,仍为不可激活; 并返回同一会话的唤醒回执和类型化完成回执。`EXECUTION_LIMB` 不在协调输入中,仍为不可激活;
协调器也没有增加第二状态库、后台调度器或 UI 权限来源。 协调器也没有增加第二状态库、后台调度器或 UI 权限来源。
生命周期协调命令现要求调用方提供稳定请求编号,并由人格编号与请求编号派生唯一会话。非秘密
语义指纹绑定规范仓库、预期 Git、唤醒身份、归因、器官和操作凭据与提供方私密头不落盘。
完成后的类型化生命周期回执保存在同一会话目录并与会话记录中的哈希交叉校验。完全相同的重试
只回放同一回执,不再运行器官或提交 Git同编号不同语义、回执篡改或不完整会话均失败关闭。
记忆代谢的 Tauri 命令现已进入阻塞任务池,不占用桌面命令线程。来源拒绝、检查点冲突和提交前 记忆代谢的 Tauri 命令现已进入阻塞任务池,不占用桌面命令线程。来源拒绝、检查点冲突和提交前
失败必须依次持久化失败事件、器官释放与休眠,写回 `DORMANT_AFTER_FAILURE` 后才释放精确主锁; 失败必须依次持久化失败事件、器官释放与休眠,写回 `DORMANT_AFTER_FAILURE` 后才释放精确主锁;
闭环任一步失败时返回 `MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY`,不能吞掉二次失败。 闭环任一步失败时返回 `MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY`,不能吞掉二次失败。

View file

@ -87,6 +87,15 @@ before wake or lease acquisition, then delegates to the same tested organ transa
completion beside the wake receipt. It does not accept an execution action, infer organ permissions from UI completion beside the wake receipt. It does not accept an execution action, infer organ permissions from UI
state, or create a parallel lifecycle record. state, or create a parallel lifecycle record.
`requestId` is the coordinator's idempotency key, not a UI message id. The runtime derives a stable session
identity from the exact persona and request id, and hashes the non-secret semantic request. API keys and
provider headers are excluded; the manifest-pinned provider id, endpoint, model id, operation, Git head, and
attribution remain included. A successful lifecycle stores one hash-bound receipt in its existing session
directory and records the request id, fingerprint, and receipt hash in the same session record. Exact retries
return that verified lifecycle with `replayed: true`. A changed request under the same id fails with
`PERSONA_LIFECYCLE_REQUEST_CONFLICT`; a partial session without a complete receipt fails recovery-required
instead of launching a duplicate.
`PersonaRuntimeQueryReceipt` is a bounded projection of the durable runtime files, not another truth store. `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, 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 and returns at most 100 newest session summaries. Dormant sessions expose no active organ. The receipt keeps

View file

@ -58,6 +58,15 @@ sense. It then owns one complete session from wake through the existing organ tr
the wake receipt and the typed completion receipt. It cannot express or activate `EXECUTION_LIMB`, does not 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. add a scheduler or second state store, and runs the complete lifecycle on the blocking task pool.
The coordinator also requires a caller-stable lifecycle request id. Persona id plus request id derives one
deterministic session id, while a credential-free fingerprint binds the canonical repository, expected Git
head, wake identity, attribution, organ, and semantic operation. A completed request persists its typed
lifecycle receipt beside the existing session record and cross-binds its hash into that record. An identical
retry verifies the session event chain, dormant state, released lease, fingerprint, and receipt hash before
replaying the same lifecycle value without invoking an organ or committing Git again. Reusing the request id
for different semantics is a conflict. A session without a complete bound receipt is recovery-required and
is never treated as permission to start a second lifecycle.
`query_persona_code_channel_runtime` is the bounded read model for later projection surfaces. The caller must `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 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 existing session records and event journals directly, verifies every returned hash chain, sorts by the last

View file

@ -295,9 +295,10 @@ pub enum PersonaLifecycleOperationInput {
}, },
} }
#[derive(Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PersonaLifecycleRunInput { pub struct PersonaLifecycleRunInput {
pub request_id: String,
pub wake: PersonaWakeInput, pub wake: PersonaWakeInput,
pub operation: PersonaLifecycleOperationInput, pub operation: PersonaLifecycleOperationInput,
} }
@ -320,6 +321,26 @@ pub struct PersonaLifecycleRunReceipt {
pub completion: PersonaLifecycleCompletionReceipt, pub completion: PersonaLifecycleCompletionReceipt,
} }
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaLifecycleCommandReceipt {
pub schema: &'static str,
pub request_id: String,
pub request_fingerprint: String,
pub replayed: bool,
pub lifecycle: serde_json::Value,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct PersistedPersonaLifecycleReceipt {
schema: String,
request_id: String,
request_fingerprint: String,
lifecycle_receipt_hash: String,
lifecycle: serde_json::Value,
}
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PersonaSessionControlInput { pub struct PersonaSessionControlInput {
@ -383,6 +404,12 @@ struct PersonaSessionRecord {
fact_source_paths: Vec<String>, fact_source_paths: Vec<String>,
attribution: PersonaAttribution, attribution: PersonaAttribution,
state: String, state: String,
#[serde(default)]
request_id: Option<String>,
#[serde(default)]
request_fingerprint: Option<String>,
#[serde(default)]
lifecycle_receipt_hash: Option<String>,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -1240,6 +1267,9 @@ fn prepare_wake_at(
fact_source_paths: organ.paths.clone(), fact_source_paths: organ.paths.clone(),
attribution: input.attribution.clone(), attribution: input.attribution.clone(),
state: "BOUND_NOT_INFERENCING".into(), state: "BOUND_NOT_INFERENCING".into(),
request_id: None,
request_fingerprint: None,
lifecycle_receipt_hash: None,
}, },
)?; )?;
let last_hash = &events.last().expect("three lifecycle events").event_hash; let last_hash = &events.last().expect("three lifecycle events").event_hash;
@ -2500,6 +2530,190 @@ where
}) })
} }
fn lifecycle_request_identity(
input: &PersonaLifecycleRunInput,
) -> Result<(String, String, String, PathBuf), String> {
let request_id = validated_id("LIFECYCLE_REQUEST_ID", &input.request_id)?;
let persona_id = validated_id("EXPECTED_PERSONA_ID", &input.wake.expected_persona_id)?;
let expected_head = validated_head(&input.wake.expected_head)?;
let (repository, _) = exact_repository(Path::new(&input.wake.repository_path))?;
let operation = match &input.operation {
PersonaLifecycleOperationInput::FactSense {
question, provider, ..
} => serde_json::json!({
"kind": "FACT_SENSE",
"question": validated_text("FACT_QUESTION", question, MAX_FACT_QUESTION_BYTES)?,
"providerId": provider.id.trim(),
"providerBaseUrl": provider.base_url.as_deref().unwrap_or("").trim(),
"modelId": input.wake.model_id.trim(),
}),
PersonaLifecycleOperationInput::MemoryMetabolism { source_session_id } => {
serde_json::json!({
"kind": "MEMORY_METABOLISM",
"sourceSessionId": validated_id("SOURCE_SESSION_ID", source_session_id)?,
})
}
};
// Credentials and provider headers are deliberately excluded. The manifest-pinned provider, endpoint,
// model, semantic operation, exact Git head, and attribution define the retry identity.
let fingerprint_input = serde_json::to_vec(&serde_json::json!({
"schema": "hololake.pncc-lifecycle-request/v1",
"requestId": &request_id,
"repositoryPath": repository.to_string_lossy(),
"expectedPersonaId": &persona_id,
"expectedHead": &expected_head,
"nodeId": input.wake.node_id.trim(),
"modelProviderId": input.wake.model_provider_id.trim(),
"modelId": input.wake.model_id.trim(),
"modelInstanceId": input.wake.model_instance_id.trim(),
"organId": input.wake.organ_id.trim(),
"attribution": &input.wake.attribution,
"operation": operation,
}))
.map_err(|error| format!("PERSONA_LIFECYCLE_REQUEST_HASH_INPUT_FAILED: {error}"))?;
let request_fingerprint = hex_digest(&fingerprint_input);
let session_seed = serde_json::to_vec(&serde_json::json!({
"personaId": &input.wake.expected_persona_id,
"requestId": &request_id,
}))
.map_err(|error| format!("PERSONA_LIFECYCLE_SESSION_ID_FAILED: {error}"))?;
let session_id = format!("PNCC-REQ-{}", &hex_digest(&session_seed)[..32]);
Ok((request_id, request_fingerprint, session_id, repository))
}
fn verified_lifecycle_replay(
runtime_root: &Path,
request_id: &str,
request_fingerprint: &str,
session_id: &str,
repository: &Path,
) -> Result<Option<PersonaLifecycleCommandReceipt>, String> {
let session_dir = session_directory(runtime_root, session_id)?;
if !session_dir.exists() {
return Ok(None);
}
let receipt_path = session_dir.join("lifecycle-receipt.json");
let bytes = fs::read(&receipt_path).map_err(|error| {
format!("PERSONA_LIFECYCLE_REQUEST_INCOMPLETE_REQUIRES_RECOVERY: {error}")
})?;
let persisted: PersistedPersonaLifecycleReceipt = serde_json::from_slice(&bytes)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_INVALID: {error}"))?;
if persisted.schema != "hololake.pncc-persisted-lifecycle-receipt/v1"
|| persisted.request_id != request_id
{
return Err("PERSONA_LIFECYCLE_REQUEST_ID_MISMATCH".into());
}
if persisted.request_fingerprint != request_fingerprint {
return Err("PERSONA_LIFECYCLE_REQUEST_CONFLICT".into());
}
let lifecycle_bytes = serde_json::to_vec(&persisted.lifecycle)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_HASH_INPUT_FAILED: {error}"))?;
if hex_digest(&lifecycle_bytes) != persisted.lifecycle_receipt_hash {
return Err("PERSONA_LIFECYCLE_RECEIPT_HASH_MISMATCH".into());
}
let record = load_session_record(runtime_root, session_id)?;
let recorded_repository = Path::new(&record.repository_path)
.canonicalize()
.map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?;
if recorded_repository != repository
|| record.request_id.as_deref() != Some(request_id)
|| record.request_fingerprint.as_deref() != Some(request_fingerprint)
|| record.lifecycle_receipt_hash.as_deref()
!= Some(persisted.lifecycle_receipt_hash.as_str())
{
return Err("PERSONA_LIFECYCLE_RECEIPT_SESSION_BINDING_MISMATCH".into());
}
let events = verify_event_journal(runtime_root, &record)?;
if record.state != "DORMANT"
|| events.last().map(|event| event.kind.as_str()) != Some("DORMANT")
|| primary_lease_held_by_session(runtime_root, &record)?
{
return Err("PERSONA_LIFECYCLE_REQUEST_INCOMPLETE_REQUIRES_RECOVERY".into());
}
if persisted
.lifecycle
.get("sessionId")
.and_then(serde_json::Value::as_str)
!= Some(session_id)
|| persisted
.lifecycle
.get("personaId")
.and_then(serde_json::Value::as_str)
!= Some(record.persona_id.as_str())
{
return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into());
}
Ok(Some(PersonaLifecycleCommandReceipt {
schema: "hololake.pncc-lifecycle-command-receipt/v1",
request_id: request_id.to_string(),
request_fingerprint: request_fingerprint.to_string(),
replayed: true,
lifecycle: persisted.lifecycle,
}))
}
fn run_idempotent_lifecycle_at<F>(
runtime_root: &Path,
input: PersonaLifecycleRunInput,
wake_timestamp: &str,
operation_timestamp: &str,
run_fact: F,
) -> Result<PersonaLifecycleCommandReceipt, String>
where
F: FnOnce(&Path, PersonaFactTaskInput, &str) -> Result<PersonaFactTaskReceipt, String>,
{
let (request_id, request_fingerprint, session_id, repository) =
lifecycle_request_identity(&input)?;
if let Some(replay) = verified_lifecycle_replay(
runtime_root,
&request_id,
&request_fingerprint,
&session_id,
&repository,
)? {
return Ok(replay);
}
let lifecycle = run_lifecycle_at(
runtime_root,
input,
&session_id,
wake_timestamp,
operation_timestamp,
run_fact,
)?;
let lifecycle = serde_json::to_value(lifecycle)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_SERIALIZATION_FAILED: {error}"))?;
let lifecycle_bytes = serde_json::to_vec(&lifecycle)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_HASH_INPUT_FAILED: {error}"))?;
let lifecycle_receipt_hash = hex_digest(&lifecycle_bytes);
let mut record = load_session_record(runtime_root, &session_id)?;
if record.state != "DORMANT" || primary_lease_held_by_session(runtime_root, &record)? {
return Err("PERSONA_LIFECYCLE_REQUEST_INCOMPLETE_REQUIRES_RECOVERY".into());
}
record.request_id = Some(request_id.clone());
record.request_fingerprint = Some(request_fingerprint.clone());
record.lifecycle_receipt_hash = Some(lifecycle_receipt_hash.clone());
write_session_record(runtime_root, &record)?;
write_json_file(
&session_directory(runtime_root, &session_id)?.join("lifecycle-receipt.json"),
&PersistedPersonaLifecycleReceipt {
schema: "hololake.pncc-persisted-lifecycle-receipt/v1".into(),
request_id: request_id.clone(),
request_fingerprint: request_fingerprint.clone(),
lifecycle_receipt_hash,
lifecycle: lifecycle.clone(),
},
"PERSONA_LIFECYCLE_RECEIPT",
)?;
Ok(PersonaLifecycleCommandReceipt {
schema: "hololake.pncc-lifecycle-command-receipt/v1",
request_id,
request_fingerprint,
replayed: false,
lifecycle,
})
}
#[tauri::command] #[tauri::command]
pub fn prepare_persona_code_channel_wake( pub fn prepare_persona_code_channel_wake(
input: PersonaWakeInput, input: PersonaWakeInput,
@ -2554,16 +2768,14 @@ pub async fn run_persona_code_channel_memory_metabolism(
#[tauri::command] #[tauri::command]
pub async fn run_persona_code_channel_lifecycle( pub async fn run_persona_code_channel_lifecycle(
input: PersonaLifecycleRunInput, input: PersonaLifecycleRunInput,
) -> Result<PersonaLifecycleRunReceipt, String> { ) -> Result<PersonaLifecycleCommandReceipt, String> {
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?; let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
tokio::task::spawn_blocking(move || { 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 wake_timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let operation_timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); let operation_timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
run_lifecycle_at( run_idempotent_lifecycle_at(
&runtime_root, &runtime_root,
input, input,
&session_id,
&wake_timestamp, &wake_timestamp,
&operation_timestamp, &operation_timestamp,
run_fact_task_with_bound_provider_at, run_fact_task_with_bound_provider_at,
@ -2746,6 +2958,7 @@ mod tests {
fn lifecycle_fact_input(repo: &Path) -> PersonaLifecycleRunInput { fn lifecycle_fact_input(repo: &Path) -> PersonaLifecycleRunInput {
let fact = fact_task_input("unused-until-coordinator-creates-session"); let fact = fact_task_input("unused-until-coordinator-creates-session");
PersonaLifecycleRunInput { PersonaLifecycleRunInput {
request_id: "REQUEST-FACT-001".into(),
wake: wake_input(repo), wake: wake_input(repo),
operation: PersonaLifecycleOperationInput::FactSense { operation: PersonaLifecycleOperationInput::FactSense {
question: fact.question, question: fact.question,
@ -3018,6 +3231,7 @@ mod tests {
let repo = persona_repo(); let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap(); let runtime = tempfile::TempDir::new().unwrap();
let input = PersonaLifecycleRunInput { let input = PersonaLifecycleRunInput {
request_id: "REQUEST-MISMATCH-001".into(),
wake: wake_input(repo.path()), wake: wake_input(repo.path()),
operation: PersonaLifecycleOperationInput::MemoryMetabolism { operation: PersonaLifecycleOperationInput::MemoryMetabolism {
source_session_id: "PNCC-SOURCE-NOT-USED".into(), source_session_id: "PNCC-SOURCE-NOT-USED".into(),
@ -3069,6 +3283,7 @@ mod tests {
let receipt = run_lifecycle_at( let receipt = run_lifecycle_at(
runtime.path(), runtime.path(),
PersonaLifecycleRunInput { PersonaLifecycleRunInput {
request_id: "REQUEST-MEMORY-001".into(),
wake, wake,
operation: PersonaLifecycleOperationInput::MemoryMetabolism { operation: PersonaLifecycleOperationInput::MemoryMetabolism {
source_session_id: "PNCC-COORDINATOR-SOURCE".into(), source_session_id: "PNCC-COORDINATOR-SOURCE".into(),
@ -3097,6 +3312,129 @@ mod tests {
require_clean_repository(repo.path()).unwrap(); require_clean_repository(repo.path()).unwrap();
} }
#[test]
fn replays_the_same_completed_lifecycle_for_an_identical_request() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let first = run_idempotent_lifecycle_at(
runtime.path(),
input.clone(),
"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":"Idempotent verified fact.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
let completed_head = head(repo.path());
let replay = run_idempotent_lifecycle_at(
runtime.path(),
input,
"2026-08-11T00:00:02.000Z",
"2026-08-11T00:00:03.000Z",
|_, _, _| panic!("an identical request must replay without running the organ"),
)
.unwrap();
assert!(!first.replayed);
assert!(replay.replayed);
assert_eq!(first.request_id, replay.request_id);
assert_eq!(first.request_fingerprint, replay.request_fingerprint);
assert_eq!(first.lifecycle, replay.lifecycle);
assert_eq!(head(repo.path()), completed_head);
assert_eq!(
fs::read_dir(runtime.path().join("sessions"))
.unwrap()
.count(),
1
);
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
}
#[test]
fn rejects_reusing_a_lifecycle_request_id_for_a_different_operation() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
run_idempotent_lifecycle_at(
runtime.path(),
input.clone(),
"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":"Original fact.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
let completed_head = head(repo.path());
let mut conflicting = input;
if let PersonaLifecycleOperationInput::FactSense { question, .. } =
&mut conflicting.operation
{
*question = "A different semantic request".into();
}
let error = run_idempotent_lifecycle_at(
runtime.path(),
conflicting,
"2026-08-11T00:00:02.000Z",
"2026-08-11T00:00:03.000Z",
|_, _, _| panic!("a conflicting request must not run the organ"),
)
.unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_REQUEST_CONFLICT"));
assert_eq!(head(repo.path()), completed_head);
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
}
#[test]
fn rejects_a_tampered_persisted_lifecycle_receipt() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let first = run_idempotent_lifecycle_at(
runtime.path(),
input.clone(),
"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":"Untampered fact.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
let session_id = first.lifecycle["sessionId"].as_str().unwrap();
let receipt_path = runtime
.path()
.join("sessions")
.join(session_id)
.join("lifecycle-receipt.json");
let mut persisted: serde_json::Value =
serde_json::from_slice(&fs::read(&receipt_path).unwrap()).unwrap();
persisted["lifecycle"]["personaId"] = serde_json::Value::String("TAMPERED".into());
fs::write(
&receipt_path,
serde_json::to_vec_pretty(&persisted).unwrap(),
)
.unwrap();
let error = run_idempotent_lifecycle_at(
runtime.path(),
input,
"2026-08-11T00:00:02.000Z",
"2026-08-11T00:00:03.000Z",
|_, _, _| panic!("a tampered receipt must not run the organ"),
)
.unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_RECEIPT_HASH_MISMATCH"));
}
#[test] #[test]
fn independently_promotes_only_the_current_verified_structured_checkpoint() { fn independently_promotes_only_the_current_verified_structured_checkpoint() {
let repo = persona_repo(); let repo = persona_repo();

View file

@ -118,7 +118,7 @@
"human_projection": "HOLOLAKE_LIVE_READ_MODEL", "human_projection": "HOLOLAKE_LIVE_READ_MODEL",
"forgejo_role": "OPTIONAL_COMPATIBILITY_COLLABORATION_ADAPTER", "forgejo_role": "OPTIONAL_COMPATIBILITY_COLLABORATION_ADAPTER",
"runtime_implemented": true, "runtime_implemented": true,
"runtime_scope": "READ_ONLY_FACT_CYCLE_FAIL_CLOSED_RECOVERY_TYPED_ORGANS_DURABLE_VERIFIED_SESSION_QUERY_INDEPENDENT_VERIFIED_MEMORY_METABOLISM_CHECKED_FAILURE_CLOSURE_NONBLOCKING_RUNTIME_COMMAND_AND_SAFE_ORGAN_LIFECYCLE_COORDINATOR_SOURCE_IMPLEMENTED_AND_TESTED", "runtime_scope": "READ_ONLY_FACT_CYCLE_FAIL_CLOSED_RECOVERY_TYPED_ORGANS_DURABLE_VERIFIED_SESSION_QUERY_INDEPENDENT_VERIFIED_MEMORY_METABOLISM_CHECKED_FAILURE_CLOSURE_NONBLOCKING_RUNTIME_COMMAND_SAFE_ORGAN_LIFECYCLE_COORDINATOR_AND_IDEMPOTENT_RECEIPT_REPLAY_SOURCE_IMPLEMENTED_AND_TESTED",
"desktop_integrated": false, "desktop_integrated": false,
"development_id": "DEV-20260810-014" "development_id": "DEV-20260810-014"
}, },
@ -343,7 +343,7 @@
"DEV-20260810-013" "DEV-20260810-013"
], ],
"closeout_record": "HLP-DEV-20260809-007-CLOSEOUT-001", "closeout_record": "HLP-DEV-20260809-007-CLOSEOUT-001",
"next_minimum_stage": "GH_PNCC_IDEMPOTENT_LIFECYCLE_REQUEST_AND_RECEIPT_REPLAY_WITHOUT_SECOND_TRUTH_STORE", "next_minimum_stage": "GH_PNCC_INCOMPLETE_IDEMPOTENT_REQUEST_INSPECTION_AND_SAFE_RECEIPT_RECOVERY",
"next_stage_started": true, "next_stage_started": true,
"heartbeat_automation": "pncc", "heartbeat_automation": "pncc",
"heartbeat_state": "ACTIVE_EVERY_10_MINUTES_UNTIL_TASK_TERMINAL" "heartbeat_state": "ACTIVE_EVERY_10_MINUTES_UNTIL_TASK_TERMINAL"

View file

@ -1,8 +1,8 @@
{ {
"schema": "hololake.persona-native-code-channel/v1", "schema": "hololake.persona-native-code-channel/v1",
"record_id": "HLP-PERSONA-NATIVE-CODE-CHANNEL-001", "record_id": "HLP-PERSONA-NATIVE-CODE-CHANNEL-001",
"version": "2026-08-11.5", "version": "2026-08-11.6",
"state": "CURRENT_FIRST_PRODUCT_CORE_SAFE_ORGAN_LIFECYCLE_COORDINATOR_SOURCE_IMPLEMENTED", "state": "CURRENT_FIRST_PRODUCT_CORE_IDEMPOTENT_SAFE_ORGAN_LIFECYCLE_SOURCE_IMPLEMENTED",
"development_id": "DEV-20260810-014", "development_id": "DEV-20260810-014",
"product": { "product": {
"formal_name_zh": "光湖人格原生代码频道", "formal_name_zh": "光湖人格原生代码频道",
@ -107,6 +107,7 @@
"independent_memory_metabolism_source_implemented": 100, "independent_memory_metabolism_source_implemented": 100,
"memory_failure_closure_and_nonblocking_runtime_command_implemented": 100, "memory_failure_closure_and_nonblocking_runtime_command_implemented": 100,
"non_ui_safe_organ_lifecycle_coordinator_source_implemented": 100, "non_ui_safe_organ_lifecycle_coordinator_source_implemented": 100,
"idempotent_lifecycle_request_and_receipt_replay_source_implemented": 100,
"general_purpose_persona_runtime_implemented": 0, "general_purpose_persona_runtime_implemented": 0,
"human_live_projection_implemented": 0, "human_live_projection_implemented": 0,
"hololake_integrated": 0, "hololake_integrated": 0,

View file

@ -90,12 +90,16 @@ test("the first source runtime cycle stays distinct from integration and deploym
channel.truth.non_ui_safe_organ_lifecycle_coordinator_source_implemented, channel.truth.non_ui_safe_organ_lifecycle_coordinator_source_implemented,
100, 100,
); );
assert.equal(
channel.truth.idempotent_lifecycle_request_and_receipt_replay_source_implemented,
100,
);
assert.equal(channel.truth.general_purpose_persona_runtime_implemented, 0); assert.equal(channel.truth.general_purpose_persona_runtime_implemented, 0);
assert.equal(channel.truth.human_live_projection_implemented, 0); assert.equal(channel.truth.human_live_projection_implemented, 0);
assert.equal(architecture.persona_native_code_channel.runtime_implemented, true); assert.equal(architecture.persona_native_code_channel.runtime_implemented, true);
assert.equal( assert.equal(
architecture.persona_native_code_channel.runtime_scope, architecture.persona_native_code_channel.runtime_scope,
"READ_ONLY_FACT_CYCLE_FAIL_CLOSED_RECOVERY_TYPED_ORGANS_DURABLE_VERIFIED_SESSION_QUERY_INDEPENDENT_VERIFIED_MEMORY_METABOLISM_CHECKED_FAILURE_CLOSURE_NONBLOCKING_RUNTIME_COMMAND_AND_SAFE_ORGAN_LIFECYCLE_COORDINATOR_SOURCE_IMPLEMENTED_AND_TESTED", "READ_ONLY_FACT_CYCLE_FAIL_CLOSED_RECOVERY_TYPED_ORGANS_DURABLE_VERIFIED_SESSION_QUERY_INDEPENDENT_VERIFIED_MEMORY_METABOLISM_CHECKED_FAILURE_CLOSURE_NONBLOCKING_RUNTIME_COMMAND_SAFE_ORGAN_LIFECYCLE_COORDINATOR_AND_IDEMPOTENT_RECEIPT_REPLAY_SOURCE_IMPLEMENTED_AND_TESTED",
); );
assert.equal(channel.truth.hololake_integrated, 0); assert.equal(channel.truth.hololake_integrated, 0);
assert.equal(channel.truth.artifact_built, 0); assert.equal(channel.truth.artifact_built, 0);