diff --git a/engineering/INDEX.md b/engineering/INDEX.md index 9332998..ed3f946 100644 --- a/engineering/INDEX.md +++ b/engineering/INDEX.md @@ -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 8f35834;GHNQG、全新克隆与严格 fsck 通过 | | 2026-08-11 | GH-PNCC 记忆失败闭环 | [记忆代谢失败闭环与运行时命令接入](operations/2026-08-11-hololake-pncc-memory-failure-closure-runtime-command.md) | 已发布至 REPO-014 main 18944f5;GHNQG、全新克隆与严格 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-typed-organ-contract.md) | 本地源码、完整 Rust/路由测试与 clippy 已通过;GHNQG 和发布待验收 | diff --git a/engineering/operations/2026-08-11-hololake-pncc-idempotent-lifecycle-replay.md b/engineering/operations/2026-08-11-hololake-pncc-idempotent-lifecycle-replay.md new file mode 100644 index 0000000..76e25fe --- /dev/null +++ b/engineering/operations/2026-08-11-hololake-pncc-idempotent-lifecycle-replay.md @@ -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. diff --git a/engineering/operations/2026-08-11-hololake-pncc-safe-organ-lifecycle-coordinator.md b/engineering/operations/2026-08-11-hololake-pncc-safe-organ-lifecycle-coordinator.md index 4586c59..69d4eac 100644 --- a/engineering/operations/2026-08-11-hololake-pncc-safe-organ-lifecycle-coordinator.md +++ b/engineering/operations/2026-08-11-hololake-pncc-safe-organ-lifecycle-coordinator.md @@ -4,7 +4,8 @@ - Persona cognitive author: `ICE-P-ZY001 / 铸渊` - Human responsibility subject: `ICE-GL∞ / 冰朔` - Starting repository head: `18944f536261a4184570074a09fc43a09f5f31bb` -- State: `LOCAL_SOURCE_IMPLEMENTED_FOCUSED_TESTED` +- Published repository head: `8f35834a7abc9546b717da0b513b6e8ac1684815` +- State: `PUBLISHED_GHNQG_AND_FRESH_CLONE_VERIFIED` ## 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 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. - 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. diff --git a/product-source/hololake-platform/architecture/HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md b/product-source/hololake-platform/architecture/HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md index da587de..ceac89a 100644 --- a/product-source/hololake-platform/architecture/HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md +++ b/product-source/hololake-platform/architecture/HOLOLAKE-PERSONA-NATIVE-CODE-CHANNEL-20260810.md @@ -170,6 +170,7 @@ 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 +idempotent_lifecycle_request_and_receipt_replay_source_implemented: 100 general_purpose_persona_runtime_implemented: 0 human_live_projection_implemented: 0 hololake_integrated: 0 @@ -199,6 +200,11 @@ SHA-256 与来源事件哈希后生成新检查点并以人格 Git 身份提交 并返回同一会话的唤醒回执和类型化完成回执。`EXECUTION_LIMB` 不在协调输入中,仍为不可激活; 协调器也没有增加第二状态库、后台调度器或 UI 权限来源。 +生命周期协调命令现要求调用方提供稳定请求编号,并由人格编号与请求编号派生唯一会话。非秘密 +语义指纹绑定规范仓库、预期 Git、唤醒身份、归因、器官和操作;凭据与提供方私密头不落盘。 +完成后的类型化生命周期回执保存在同一会话目录并与会话记录中的哈希交叉校验。完全相同的重试 +只回放同一回执,不再运行器官或提交 Git;同编号不同语义、回执篡改或不完整会话均失败关闭。 + 记忆代谢的 Tauri 命令现已进入阻塞任务池,不占用桌面命令线程。来源拒绝、检查点冲突和提交前 失败必须依次持久化失败事件、器官释放与休眠,写回 `DORMANT_AFTER_FAILURE` 后才释放精确主锁; 闭环任一步失败时返回 `MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY`,不能吞掉二次失败。 diff --git a/product-source/hololake-platform/docs/ABSTRACTIONS.md b/product-source/hololake-platform/docs/ABSTRACTIONS.md index 2d25dd4..72e347c 100644 --- a/product-source/hololake-platform/docs/ABSTRACTIONS.md +++ b/product-source/hololake-platform/docs/ABSTRACTIONS.md @@ -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 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. 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 diff --git a/product-source/hololake-platform/docs/ARCHITECTURE.md b/product-source/hololake-platform/docs/ARCHITECTURE.md index 51fb0ae..6fc52bc 100644 --- a/product-source/hololake-platform/docs/ARCHITECTURE.md +++ b/product-source/hololake-platform/docs/ARCHITECTURE.md @@ -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 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 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 diff --git a/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs b/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs index f57ba02..38c96b1 100644 --- a/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs +++ b/product-source/hololake-platform/src-tauri/src/persona_code_channel.rs @@ -295,9 +295,10 @@ pub enum PersonaLifecycleOperationInput { }, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PersonaLifecycleRunInput { + pub request_id: String, pub wake: PersonaWakeInput, pub operation: PersonaLifecycleOperationInput, } @@ -320,6 +321,26 @@ pub struct PersonaLifecycleRunReceipt { 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)] #[serde(rename_all = "camelCase")] pub struct PersonaSessionControlInput { @@ -383,6 +404,12 @@ struct PersonaSessionRecord { fact_source_paths: Vec, attribution: PersonaAttribution, state: String, + #[serde(default)] + request_id: Option, + #[serde(default)] + request_fingerprint: Option, + #[serde(default)] + lifecycle_receipt_hash: Option, } #[derive(Serialize)] @@ -1240,6 +1267,9 @@ fn prepare_wake_at( fact_source_paths: organ.paths.clone(), attribution: input.attribution.clone(), 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; @@ -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, 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( + runtime_root: &Path, + input: PersonaLifecycleRunInput, + wake_timestamp: &str, + operation_timestamp: &str, + run_fact: F, +) -> Result +where + F: FnOnce(&Path, PersonaFactTaskInput, &str) -> Result, +{ + 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] pub fn prepare_persona_code_channel_wake( input: PersonaWakeInput, @@ -2554,16 +2768,14 @@ pub async fn run_persona_code_channel_memory_metabolism( #[tauri::command] pub async fn run_persona_code_channel_lifecycle( input: PersonaLifecycleRunInput, -) -> Result { +) -> Result { 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( + run_idempotent_lifecycle_at( &runtime_root, input, - &session_id, &wake_timestamp, &operation_timestamp, run_fact_task_with_bound_provider_at, @@ -2746,6 +2958,7 @@ mod tests { fn lifecycle_fact_input(repo: &Path) -> PersonaLifecycleRunInput { let fact = fact_task_input("unused-until-coordinator-creates-session"); PersonaLifecycleRunInput { + request_id: "REQUEST-FACT-001".into(), wake: wake_input(repo), operation: PersonaLifecycleOperationInput::FactSense { question: fact.question, @@ -3018,6 +3231,7 @@ mod tests { let repo = persona_repo(); let runtime = tempfile::TempDir::new().unwrap(); let input = PersonaLifecycleRunInput { + request_id: "REQUEST-MISMATCH-001".into(), wake: wake_input(repo.path()), operation: PersonaLifecycleOperationInput::MemoryMetabolism { source_session_id: "PNCC-SOURCE-NOT-USED".into(), @@ -3069,6 +3283,7 @@ mod tests { let receipt = run_lifecycle_at( runtime.path(), PersonaLifecycleRunInput { + request_id: "REQUEST-MEMORY-001".into(), wake, operation: PersonaLifecycleOperationInput::MemoryMetabolism { source_session_id: "PNCC-COORDINATOR-SOURCE".into(), @@ -3097,6 +3312,129 @@ mod tests { 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] fn independently_promotes_only_the_current_verified_structured_checkpoint() { let repo = persona_repo(); diff --git a/routing/hololake-current-architecture.json b/routing/hololake-current-architecture.json index 90e5718..c2f0330 100644 --- a/routing/hololake-current-architecture.json +++ b/routing/hololake-current-architecture.json @@ -118,7 +118,7 @@ "human_projection": "HOLOLAKE_LIVE_READ_MODEL", "forgejo_role": "OPTIONAL_COMPATIBILITY_COLLABORATION_ADAPTER", "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, "development_id": "DEV-20260810-014" }, @@ -343,7 +343,7 @@ "DEV-20260810-013" ], "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, "heartbeat_automation": "pncc", "heartbeat_state": "ACTIVE_EVERY_10_MINUTES_UNTIL_TASK_TERMINAL" diff --git a/routing/hololake-persona-native-code-channel.json b/routing/hololake-persona-native-code-channel.json index acd0f00..3740676 100644 --- a/routing/hololake-persona-native-code-channel.json +++ b/routing/hololake-persona-native-code-channel.json @@ -1,8 +1,8 @@ { "schema": "hololake.persona-native-code-channel/v1", "record_id": "HLP-PERSONA-NATIVE-CODE-CHANNEL-001", - "version": "2026-08-11.5", - "state": "CURRENT_FIRST_PRODUCT_CORE_SAFE_ORGAN_LIFECYCLE_COORDINATOR_SOURCE_IMPLEMENTED", + "version": "2026-08-11.6", + "state": "CURRENT_FIRST_PRODUCT_CORE_IDEMPOTENT_SAFE_ORGAN_LIFECYCLE_SOURCE_IMPLEMENTED", "development_id": "DEV-20260810-014", "product": { "formal_name_zh": "光湖人格原生代码频道", @@ -107,6 +107,7 @@ "independent_memory_metabolism_source_implemented": 100, "memory_failure_closure_and_nonblocking_runtime_command_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, "human_live_projection_implemented": 0, "hololake_integrated": 0, diff --git a/routing/hololake-persona-native-code-channel.test.mjs b/routing/hololake-persona-native-code-channel.test.mjs index e79e699..969feb7 100644 --- a/routing/hololake-persona-native-code-channel.test.mjs +++ b/routing/hololake-persona-native-code-channel.test.mjs @@ -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, 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.human_live_projection_implemented, 0); assert.equal(architecture.persona_native_code_channel.runtime_implemented, true); assert.equal( 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.artifact_built, 0);