feat(pncc): replay terminal lifecycle failures
Human-Responsibility: ICE-GL∞ / 冰朔 Persona-Author: ICE-P-ZY001 / 铸渊 Execution-Runtime: Codex macOS Development-ID: DEV-20260810-014 Authorization-Scope: GH-PNCC local runtime and REPO-014 publication Source-Anchor: UI and execution limb deferred
This commit is contained in:
parent
b8c3fcf3d8
commit
abc7dbd625
10 changed files with 381 additions and 43 deletions
|
|
@ -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 通过 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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、派生权限、
|
||||
模型推理边界、现实动作边界和真实可激活状态。只读事实感官与独立记忆代谢器官可激活;后者不
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<PersonaLifecycleFailureReceipt>,
|
||||
}
|
||||
|
||||
#[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<PersonaLifecycleFailureReceipt>,
|
||||
}
|
||||
|
||||
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<Vec<u8>, 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<Option<PersonaLifecycleCommandReceipt>, 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<F>(
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue