diff --git a/engineering/INDEX.md b/engineering/INDEX.md index 34dd3c9..4a4c8c4 100644 --- a/engineering/INDEX.md +++ b/engineering/INDEX.md @@ -32,7 +32,8 @@ Windows / macOS / Linux 构建机与安装包 | 时间 | 版本 | 记录 | 状态 | | --- | --- | --- | --- | -| 2026-08-11 | GH-PNCC 幂等中断恢复 | [不完整幂等请求检查与安全回执恢复](operations/2026-08-11-hololake-pncc-incomplete-idempotent-request-recovery.md) | 本地源码与聚焦测试已通过;完整门禁、发布与独立回读待验收 | +| 2026-08-11 | GH-PNCC 终态失败重放 | [终态失败结构化回执与幂等重放](operations/2026-08-11-hololake-pncc-idempotent-terminal-failure-replay.md) | 本地源码、完整 Rust/路由测试与 clippy 已通过;GHNQG 和发布待验收 | +| 2026-08-11 | GH-PNCC 幂等中断恢复 | [不完整幂等请求检查与安全回执恢复](operations/2026-08-11-hololake-pncc-incomplete-idempotent-request-recovery.md) | 已发布至 REPO-014 main b8c3fcf;GHNQG、全新克隆与严格 fsck 通过 | | 2026-08-11 | GH-PNCC 幂等生命周期 | [生命周期请求身份与同一回执重放](operations/2026-08-11-hololake-pncc-idempotent-lifecycle-replay.md) | 已发布至 REPO-014 main 38ef9d0;GHNQG、全新克隆与严格 fsck 通过 | | 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 通过 | diff --git a/engineering/operations/2026-08-11-hololake-pncc-idempotent-terminal-failure-replay.md b/engineering/operations/2026-08-11-hololake-pncc-idempotent-terminal-failure-replay.md new file mode 100644 index 0000000..64f5a51 --- /dev/null +++ b/engineering/operations/2026-08-11-hololake-pncc-idempotent-terminal-failure-replay.md @@ -0,0 +1,42 @@ +# GH-PNCC idempotent terminal failure receipt and replay + +- Development ID: `DEV-20260810-014` +- Persona cognitive author: `ICE-P-ZY001 / 铸渊` +- Human responsibility subject: `ICE-GL∞ / 冰朔` +- Starting repository head: `b8c3fcf3d8c2928be724291fdf9c1e5dc3512d71` +- State: `LOCAL_SOURCE_IMPLEMENTED_FULLY_TESTED` + +## Implemented facts + +A lifecycle command that reaches the kernel's already verified failure closure now persists one structured +terminal-failure receipt in the existing session directory. It uses the same atomic receipt-first, +session-binding-second protocol as successful lifecycle completion and does not create another request store. + +The failure payload contains a stable machine error code, terminal event hash, exact persona/repository +identity, and structured human/persona attribution. Raw model-provider diagnostics are deliberately excluded. +The receipt can be issued only after the session journal verifies, the state is `DORMANT_AFTER_FAILURE`, the +last event is `DORMANT`, the exact primary lease is released, and the repository remains clean at the recorded +Git head. + +An exact retry verifies the semantic fingerprint, receipt hash, event chain, terminal state, lease, and +session bindings, then returns the same `FAILED` result with `replayed: true`. It does not invoke the model, +activate an organ, write a checkpoint, or commit Git again. The existing inspection and safe-binding recovery +commands accept the same proven terminal outcome and still reject partial bindings or unproven closure. + +## Current verification + +- PNCC focused Rust tests: `27 passed, 0 failed`. +- Full Rust suite: `1167 passed, 2 ignored`; integration test: `1 passed`. +- Routing suite: `29 passed, 0 failed`; `cargo fmt`, strict clippy, and diff checks passed. +- The terminal-failure test proves exact retry does not call the model again and that raw provider detail is + absent from the persisted receipt. +- The crash-window test proves an unbound failure receipt is safely inspected, bound, and replayed. +- GHNQG, commit, publication, and fresh-clone readback remain pending for this stage. + +## Truth boundary + +- This is command-level terminal failure idempotency, not arbitrary recovery of an interrupted or active + session. If closure cannot be proven, the original error and established session recovery boundary remain. +- UI, projection aesthetics, background scheduling, and `EXECUTION_LIMB` remain outside this stage. +- The next runtime increment must be selected by a new source-backed gap assessment rather than inferred from + the human projection roadmap. diff --git a/engineering/operations/2026-08-11-hololake-pncc-incomplete-idempotent-request-recovery.md b/engineering/operations/2026-08-11-hololake-pncc-incomplete-idempotent-request-recovery.md index 94ecb5e..ed60b39 100644 --- a/engineering/operations/2026-08-11-hololake-pncc-incomplete-idempotent-request-recovery.md +++ b/engineering/operations/2026-08-11-hololake-pncc-incomplete-idempotent-request-recovery.md @@ -4,7 +4,11 @@ - Persona cognitive author: `ICE-P-ZY001 / 铸渊` - Human responsibility subject: `ICE-GL∞ / 冰朔` - Starting repository head: `38ef9d0e113e25737b1be456a705da5ddce0f27c` -- State: `LOCAL_SOURCE_IMPLEMENTED_FOCUSED_TESTED` +- Published repository head: `b8c3fcf3d8c2928be724291fdf9c1e5dc3512d71` +- Published tree: `6218fb109913649151609a0f956b699ae375198a` +- Publication queue: `PUB-20260810194731692-1ad8ad9a` +- GHNQG manual SHA-256: `691d600d1f8743212bfb16bbd47a7609ec22b0219a2af76b35bc281feb364f77` +- State: `PUBLISHED_VERIFIED` ## Implemented facts @@ -29,12 +33,13 @@ never reconstructed from guesses and remain owned by the established session rec - A simulated crash after receipt persistence proves inspection and binding recovery return the exact original lifecycle without another model call, session, organ activation, or Git commit. - A session interrupted before receipt persistence proves recovery refuses to fabricate a success receipt. -- Full repository gates, GHNQG, commit, publication, and fresh-clone readback remain pending for this stage. +- Full repository gates, GHNQG, publication, strict fsck, and fresh-clone readback from + `/tmp/pncc-verify-b8c3fcf-readback` passed for the exact published head and tree. ## Truth boundary and next minimum - Runtime files in the existing session directory remain the only evidence plane; no database or second request authority was introduced. - UI, projection aesthetics, background scheduling, and `EXECUTION_LIMB` remain outside this stage. -- Terminal failed lifecycle calls still do not have an idempotently replayable command-level failure receipt; - that is the next minimum runtime boundary. +- The next stage implements the separately bounded terminal-failure receipt and replay rule; this published + stage remains the success-receipt and crash-window recovery authority. 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 10e1765..95b9ac5 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 @@ -172,6 +172,7 @@ 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 incomplete_idempotent_request_inspection_and_safe_receipt_recovery_source_implemented: 100 +idempotent_terminal_failure_receipt_and_replay_source_implemented: 100 general_purpose_persona_runtime_implemented: 0 human_live_projection_implemented: 0 hololake_integrated: 0 @@ -189,6 +190,11 @@ runtime_health: 0 终止未提交会话,或为已由该人格完成但尚未休眠的单一检查点提交补齐收尾。脏仓库、未知 Git 推进、错误归因和损坏事件链一律保留主锁并要求人工审查;恢复器不会按时间猜测主控已死亡。 +生命周期命令的已闭合失败也使用同一会话目录、同一回执文件和同一绑定恢复路径。回执只保留 +稳定机器错误码、终态事件哈希与结构化归因,不落盘模型提供方原始错误细节;同一语义请求在 +终态、Git、租约、事件链和回执哈希全部复核后只重放原失败,不再次启动模型或器官。未完成 +闭合、脏仓库或仍持有租约的失败不能被伪装成终态回执。 + 人格器官现已拥有机器可读类型合同。系统能够在不唤醒人格、不取得主锁、不运行模型的情况下, 检查 `FACT_SENSE`、`MEMORY_METABOLISM` 与 `EXECUTION_LIMB` 的固定模式、输入输出 schema、派生权限、 模型推理边界、现实动作边界和真实可激活状态。只读事实感官与独立记忆代谢器官可激活;后者不 diff --git a/product-source/hololake-platform/docs/ABSTRACTIONS.md b/product-source/hololake-platform/docs/ABSTRACTIONS.md index a3bdc68..bc7754f 100644 --- a/product-source/hololake-platform/docs/ABSTRACTIONS.md +++ b/product-source/hololake-platform/docs/ABSTRACTIONS.md @@ -103,6 +103,13 @@ receipt beside a verified dormant session with no lease and all three binding fi verifier. Missing receipts, partial bindings, active leases, damaged chains, or changed semantics remain recovery/manual-review states and cannot be converted into a successful receipt. +The same persisted receipt envelope also represents a verified terminal failure. Its `outcome` is `FAILED`, +its successful lifecycle payload is null, and `failure` carries only the stable error code, terminal event +hash, identity, repository, and attribution. The runtime hashes that structured failure payload, never the +raw provider diagnostic. Replay and safe binding recovery require `DORMANT_AFTER_FAILURE`, a final `DORMANT` +event, a released lease, and an unchanged clean Git head. A failure that cannot prove those conditions is not +terminal and remains recovery-required. + `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 a8afc3a..50fa619 100644 --- a/product-source/hololake-platform/docs/ARCHITECTURE.md +++ b/product-source/hololake-platform/docs/ARCHITECTURE.md @@ -73,6 +73,14 @@ but unbound, and session-recovery states from the same session evidence. The rec only the receipt-present/unbound window after rechecking the receipt hash, identity, event chain, dormancy, and released lease. It never fabricates a receipt for an earlier interruption or repairs partial bindings. +A lifecycle that reaches the kernel's verified terminal failure closure now uses that same receipt file and +two-write binding protocol. The persisted value contains only a stable machine error code, terminal event +hash, repository/persona identity, and structured attribution; raw provider or model error detail is not +stored. Exact retries verify `DORMANT_AFTER_FAILURE`, the final `DORMANT` event, released lease, unchanged +clean Git head, semantic fingerprint, receipt hash, and session bindings before returning the same failure +with `replayed: true`. Active, dirty, partially closed, or otherwise unproven failures retain the original +recovery path and cannot be converted into a terminal receipt. + `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 e52f912..9642686 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 @@ -328,7 +328,21 @@ pub struct PersonaLifecycleCommandReceipt { pub request_id: String, pub request_fingerprint: String, pub replayed: bool, + pub outcome: &'static str, pub lifecycle: serde_json::Value, + pub failure: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaLifecycleFailureReceipt { + pub schema: String, + pub session_id: String, + pub persona_id: String, + pub repository_path: String, + pub error_code: String, + pub terminal_event_hash: String, + pub attribution: PersonaAttribution, } #[derive(Debug, Serialize)] @@ -353,7 +367,15 @@ struct PersistedPersonaLifecycleReceipt { request_id: String, request_fingerprint: String, lifecycle_receipt_hash: String, + #[serde(default = "completed_lifecycle_outcome")] + outcome: String, lifecycle: serde_json::Value, + #[serde(default)] + failure: Option, +} + +fn completed_lifecycle_outcome() -> String { + "COMPLETED".into() } #[derive(Clone, Debug, Deserialize)] @@ -2596,6 +2618,66 @@ fn lifecycle_request_identity( Ok((request_id, request_fingerprint, session_id, repository)) } +fn persisted_lifecycle_payload_bytes( + persisted: &PersistedPersonaLifecycleReceipt, +) -> Result, String> { + match persisted.outcome.as_str() { + "COMPLETED" if persisted.failure.is_none() && !persisted.lifecycle.is_null() => { + serde_json::to_vec(&persisted.lifecycle) + .map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_HASH_INPUT_FAILED: {error}")) + } + "FAILED" if persisted.lifecycle.is_null() => serde_json::to_vec( + persisted + .failure + .as_ref() + .ok_or("PERSONA_LIFECYCLE_FAILURE_RECEIPT_MISSING")?, + ) + .map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_HASH_INPUT_FAILED: {error}")), + _ => Err("PERSONA_LIFECYCLE_RECEIPT_OUTCOME_INVALID".into()), + } +} + +fn persisted_lifecycle_identity_matches( + persisted: &PersistedPersonaLifecycleReceipt, + session_id: &str, + persona_id: &str, +) -> bool { + match persisted.outcome.as_str() { + "COMPLETED" => { + 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(persona_id) + } + "FAILED" => persisted.failure.as_ref().is_some_and(|failure| { + failure.schema == "hololake.pncc-lifecycle-failure-receipt/v1" + && failure.session_id == session_id + && failure.persona_id == persona_id + }), + _ => false, + } +} + +fn lifecycle_failure_code(error: &str) -> String { + let candidate = error.split([':', ';']).next().unwrap_or_default().trim(); + if !candidate.is_empty() + && candidate.len() <= MAX_ID_BYTES + && candidate + .bytes() + .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_') + { + candidate.into() + } else { + "PERSONA_LIFECYCLE_OPERATION_FAILED".into() + } +} + fn verified_lifecycle_replay( runtime_root: &Path, request_id: &str, @@ -2621,9 +2703,9 @@ fn verified_lifecycle_replay( 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 { + if hex_digest(&persisted_lifecycle_payload_bytes(&persisted)?) + != persisted.lifecycle_receipt_hash + { return Err("PERSONA_LIFECYCLE_RECEIPT_HASH_MISMATCH".into()); } let record = load_session_record(runtime_root, session_id)?; @@ -2639,23 +2721,18 @@ fn verified_lifecycle_replay( return Err("PERSONA_LIFECYCLE_RECEIPT_SESSION_BINDING_MISMATCH".into()); } let events = verify_event_journal(runtime_root, &record)?; - if record.state != "DORMANT" + let expected_state = if persisted.outcome == "FAILED" { + "DORMANT_AFTER_FAILURE" + } else { + "DORMANT" + }; + if record.state != expected_state || 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()) - { + if !persisted_lifecycle_identity_matches(&persisted, session_id, &record.persona_id) { return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into()); } Ok(Some(PersonaLifecycleCommandReceipt { @@ -2663,7 +2740,13 @@ fn verified_lifecycle_replay( request_id: request_id.to_string(), request_fingerprint: request_fingerprint.to_string(), replayed: true, + outcome: if persisted.outcome == "FAILED" { + "FAILED" + } else { + "COMPLETED" + }, lifecycle: persisted.lifecycle, + failure: persisted.failure, })) } @@ -2726,22 +2809,12 @@ fn inspect_lifecycle_request_at( 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 { + if hex_digest(&persisted_lifecycle_payload_bytes(&persisted)?) + != persisted.lifecycle_receipt_hash + { return Err("PERSONA_LIFECYCLE_RECEIPT_HASH_MISMATCH".into()); } - if persisted - .lifecycle - .get("sessionId") - .and_then(serde_json::Value::as_str) - != Some(session_id.as_str()) - || persisted - .lifecycle - .get("personaId") - .and_then(serde_json::Value::as_str) - != Some(record.persona_id.as_str()) - { + if !persisted_lifecycle_identity_matches(&persisted, &session_id, &record.persona_id) { return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into()); } let complete = record.request_id.as_deref() == Some(request_id.as_str()) @@ -2751,7 +2824,12 @@ fn inspect_lifecycle_request_at( let unbound = record.request_id.is_none() && record.request_fingerprint.is_none() && record.lifecycle_receipt_hash.is_none(); - let safely_dormant = record.state == "DORMANT" + let expected_state = if persisted.outcome == "FAILED" { + "DORMANT_AFTER_FAILURE" + } else { + "DORMANT" + }; + let safely_dormant = record.state == expected_state && events.last().map(|event| event.kind.as_str()) == Some("DORMANT") && !lease_held; let (status, safe_to_bind_receipt) = if complete && safely_dormant { @@ -2814,6 +2892,83 @@ fn recover_lifecycle_request_at( .ok_or_else(|| "PERSONA_LIFECYCLE_RECEIPT_RECOVERY_LOST_SESSION".into()) } +fn persist_terminal_lifecycle_failure( + runtime_root: &Path, + request_id: &str, + request_fingerprint: &str, + session_id: &str, + repository: &Path, + error: &str, +) -> Result, String> { + let session_dir = session_directory(runtime_root, session_id)?; + if !session_dir.exists() { + return Ok(None); + } + let mut record = load_session_record(runtime_root, session_id)?; + let recorded_repository = Path::new(&record.repository_path) + .canonicalize() + .map_err(|cause| format!("PERSONA_REPOSITORY_UNAVAILABLE: {cause}"))?; + if recorded_repository != repository { + return Err("PERSONA_LIFECYCLE_REQUEST_SESSION_IDENTITY_MISMATCH".into()); + } + let events = verify_event_journal(runtime_root, &record)?; + let terminal_event = events.last().ok_or("PERSONA_EVENT_CHAIN_EMPTY")?; + let (_, observed_head) = exact_repository(repository)?; + let safely_terminal = record.state == "DORMANT_AFTER_FAILURE" + && terminal_event.kind == "DORMANT" + && !primary_lease_held_by_session(runtime_root, &record)? + && observed_head == record.git_head + && require_clean_repository(repository).is_ok(); + if !safely_terminal { + return Ok(None); + } + if record.request_id.is_some() + || record.request_fingerprint.is_some() + || record.lifecycle_receipt_hash.is_some() + { + return Err("PERSONA_LIFECYCLE_FAILURE_BINDING_ALREADY_PRESENT".into()); + } + let failure = PersonaLifecycleFailureReceipt { + schema: "hololake.pncc-lifecycle-failure-receipt/v1".into(), + session_id: session_id.into(), + persona_id: record.persona_id.clone(), + repository_path: record.repository_path.clone(), + error_code: lifecycle_failure_code(error), + terminal_event_hash: terminal_event.event_hash.clone(), + attribution: record.attribution.clone(), + }; + let lifecycle_receipt_hash = hex_digest( + &serde_json::to_vec(&failure) + .map_err(|cause| format!("PERSONA_LIFECYCLE_RECEIPT_HASH_INPUT_FAILED: {cause}"))?, + ); + write_json_file( + &session_dir.join("lifecycle-receipt.json"), + &PersistedPersonaLifecycleReceipt { + schema: "hololake.pncc-persisted-lifecycle-receipt/v1".into(), + request_id: request_id.into(), + request_fingerprint: request_fingerprint.into(), + lifecycle_receipt_hash: lifecycle_receipt_hash.clone(), + outcome: "FAILED".into(), + lifecycle: serde_json::Value::Null, + failure: Some(failure.clone()), + }, + "PERSONA_LIFECYCLE_RECEIPT", + )?; + record.request_id = Some(request_id.into()); + record.request_fingerprint = Some(request_fingerprint.into()); + record.lifecycle_receipt_hash = Some(lifecycle_receipt_hash); + write_session_record(runtime_root, &record)?; + Ok(Some(PersonaLifecycleCommandReceipt { + schema: "hololake.pncc-lifecycle-command-receipt/v1", + request_id: request_id.into(), + request_fingerprint: request_fingerprint.into(), + replayed: false, + outcome: "FAILED", + lifecycle: serde_json::Value::Null, + failure: Some(failure), + })) +} + fn run_idempotent_lifecycle_at( runtime_root: &Path, input: PersonaLifecycleRunInput, @@ -2835,14 +2990,29 @@ where )? { return Ok(replay); } - let lifecycle = run_lifecycle_at( + let lifecycle = match run_lifecycle_at( runtime_root, input, &session_id, wake_timestamp, operation_timestamp, run_fact, - )?; + ) { + Ok(lifecycle) => lifecycle, + Err(error) => { + if let Some(receipt) = persist_terminal_lifecycle_failure( + runtime_root, + &request_id, + &request_fingerprint, + &session_id, + &repository, + &error, + )? { + return Ok(receipt); + } + return Err(error); + } + }; 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) @@ -2861,7 +3031,9 @@ where request_id: request_id.clone(), request_fingerprint: request_fingerprint.clone(), lifecycle_receipt_hash: lifecycle_receipt_hash.clone(), + outcome: "COMPLETED".into(), lifecycle: lifecycle.clone(), + failure: None, }, "PERSONA_LIFECYCLE_RECEIPT", )?; @@ -2874,7 +3046,9 @@ where request_id, request_fingerprint, replayed: false, + outcome: "COMPLETED", lifecycle, + failure: None, }) } @@ -3680,6 +3854,96 @@ mod tests { assert!(error.contains("PERSONA_LIFECYCLE_RECEIPT_BINDING_NOT_SAFE")); } + #[test] + fn persists_and_replays_the_same_structured_terminal_failure_without_rerunning_the_organ() { + let repo = persona_repo(); + let runtime = tempfile::TempDir::new().unwrap(); + let input = lifecycle_fact_input(repo.path()); + let original_head = head(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, |_, _| { + Err("provider-secret-detail-must-not-persist".into()) + }) + }, + ) + .unwrap(); + let replay = run_idempotent_lifecycle_at( + runtime.path(), + input, + "2026-08-11T00:00:02.000Z", + "2026-08-11T00:00:03.000Z", + |_, _, _| panic!("a terminal failure replay must not rerun the organ"), + ) + .unwrap(); + + assert_eq!(first.outcome, "FAILED"); + assert!(!first.replayed); + assert!(first.lifecycle.is_null()); + assert_eq!( + first.failure.as_ref().unwrap().error_code, + "PERSONA_MODEL_INFERENCE_FAILED" + ); + assert_eq!(replay.outcome, "FAILED"); + assert!(replay.replayed); + assert_eq!( + replay.failure.as_ref().unwrap().terminal_event_hash, + first.failure.as_ref().unwrap().terminal_event_hash + ); + assert_eq!(head(repo.path()), original_head); + let persisted = fs::read_to_string( + session_directory( + runtime.path(), + first.failure.as_ref().unwrap().session_id.as_str(), + ) + .unwrap() + .join("lifecycle-receipt.json"), + ) + .unwrap(); + assert!(!persisted.contains("provider-secret-detail-must-not-persist")); + assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists()); + } + + #[test] + fn safely_recovers_an_unbound_terminal_failure_receipt() { + let repo = persona_repo(); + let runtime = tempfile::TempDir::new().unwrap(); + let input = lifecycle_fact_input(repo.path()); + let failed = 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, |_, _| { + Err("bounded provider failure".into()) + }) + }, + ) + .unwrap(); + let session_id = failed.failure.as_ref().unwrap().session_id.clone(); + let mut record = load_session_record(runtime.path(), &session_id).unwrap(); + record.request_id = None; + record.request_fingerprint = None; + record.lifecycle_receipt_hash = None; + write_session_record(runtime.path(), &record).unwrap(); + + let inspection = inspect_lifecycle_request_at(runtime.path(), &input).unwrap(); + assert_eq!(inspection.status, "SAFE_BIND_PERSISTED_RECEIPT"); + assert!(inspection.safe_to_bind_receipt); + let recovered = recover_lifecycle_request_at(runtime.path(), &input).unwrap(); + assert_eq!(recovered.outcome, "FAILED"); + assert!(recovered.replayed); + assert_eq!( + recovered.failure.unwrap().error_code, + "PERSONA_MODEL_INFERENCE_FAILED" + ); + } + #[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 767cad7..143e13c 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_SAFE_ORGAN_LIFECYCLE_COORDINATOR_IDEMPOTENT_RECEIPT_REPLAY_AND_SAFE_RECEIPT_BINDING_RECOVERY_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_IDEMPOTENT_SUCCESS_AND_TERMINAL_FAILURE_RECEIPT_REPLAY_AND_SAFE_RECEIPT_BINDING_RECOVERY_SOURCE_IMPLEMENTED_AND_TESTED", "desktop_integrated": false, "development_id": "DEV-20260810-014" }, @@ -343,8 +343,8 @@ "DEV-20260810-013" ], "closeout_record": "HLP-DEV-20260809-007-CLOSEOUT-001", - "next_minimum_stage": "GH_PNCC_IDEMPOTENT_TERMINAL_FAILURE_RECEIPT_AND_REPLAY", - "next_stage_started": true, + "next_minimum_stage": "GH_PNCC_RUNTIME_GAP_REASSESSMENT_FROM_CURRENT_SOURCE", + "next_stage_started": false, "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 f2f5591..c6f073a 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.7", - "state": "CURRENT_FIRST_PRODUCT_CORE_RECOVERABLE_IDEMPOTENT_SAFE_ORGAN_LIFECYCLE_SOURCE_IMPLEMENTED", + "version": "2026-08-11.8", + "state": "CURRENT_FIRST_PRODUCT_CORE_IDEMPOTENT_TERMINAL_FAILURE_REPLAY_SOURCE_IMPLEMENTED", "development_id": "DEV-20260810-014", "product": { "formal_name_zh": "光湖人格原生代码频道", @@ -109,6 +109,7 @@ "non_ui_safe_organ_lifecycle_coordinator_source_implemented": 100, "idempotent_lifecycle_request_and_receipt_replay_source_implemented": 100, "incomplete_idempotent_request_inspection_and_safe_receipt_recovery_source_implemented": 100, + "idempotent_terminal_failure_receipt_and_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 5e6a3d2..6f60c53 100644 --- a/routing/hololake-persona-native-code-channel.test.mjs +++ b/routing/hololake-persona-native-code-channel.test.mjs @@ -98,12 +98,16 @@ test("the first source runtime cycle stays distinct from integration and deploym channel.truth.incomplete_idempotent_request_inspection_and_safe_receipt_recovery_source_implemented, 100, ); + assert.equal( + channel.truth.idempotent_terminal_failure_receipt_and_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_SAFE_ORGAN_LIFECYCLE_COORDINATOR_IDEMPOTENT_RECEIPT_REPLAY_AND_SAFE_RECEIPT_BINDING_RECOVERY_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_IDEMPOTENT_SUCCESS_AND_TERMINAL_FAILURE_RECEIPT_REPLAY_AND_SAFE_RECEIPT_BINDING_RECOVERY_SOURCE_IMPLEMENTED_AND_TESTED", ); assert.equal(channel.truth.hololake_integrated, 0); assert.equal(channel.truth.artifact_built, 0);