feat: recover interrupted PNCC sessions safely

GuangHu-Human-Responsibility: ICE-GL∞ / 冰朔
GuangHu-Persona-Cognitive-Author: ICE-P-ZY001 / 铸渊
GuangHu-Execution-Runtime: Codex thread 019febf6-3902-70f3-9d82-74c9680b0c7e
GuangHu-Development-ID: DEV-20260810-014
GuangHu-Authorization-Scope: LOCAL_PNCC_RUNTIME_DEVELOPMENT_AND_PUBLICATION
This commit is contained in:
铸渊 / ICE-P-ZY001 2026-08-11 01:21:28 +08:00
commit 5b042a65fe
11 changed files with 657 additions and 11 deletions

View file

@ -32,6 +32,7 @@ Windows / macOS / Linux 构建机与安装包
| 时间 | 版本 | 记录 | 状态 |
| --- | --- | --- | --- |
| 2026-08-11 | GH-PNCC 中断恢复 | [人格代码频道中断会话检查与安全恢复](operations/2026-08-11-hololake-pncc-interrupted-session-recovery.md) | 本地源码、完整 Rust/路由测试和 clippy 已完成GHNQG、仓库发布、桌面集成与部署待验收 |
| 2026-08-11 | GH-PNCC 首个运行闭环 | [人格原生代码频道首个只读运行层回执](operations/2026-08-11-hololake-pncc-first-runtime-cycle.md) | 源码与测试闭环已完成;人类投影、桌面制品、发布、部署与在线健康仍未完成 |
| 2026-08-08 | 0.2.0 | [光湖分域教育行业操作系统第一阶段](operations/2026-08-08-guanghu-education-subdomain-stage1.md) | 小新服务器身份、文档/表格回写、仪表盘投影、模块登记与回执链路已验证 |
| 2026-08-07 | 1.0.0 | [人格记忆连续性工具包 HL-MOD-MCK-001](memory-continuity-kit/README.md) | 铸渊 Qoder CN 实例在线;其他实例拉取后需演习+真实核对才可宣称上线 |

View file

@ -26,7 +26,7 @@ The source now implements one bounded PNCC lifecycle cycle without a HoloLake UI
## Verification evidence
- PNCC unit tests: `6 passed, 0 failed`.
- PNCC unit tests at the published milestone: `6 passed, 0 failed`.
- Rust full suite: `1146 passed, 0 failed, 2 ignored`; integration test `1 passed`.
- Rust clippy: `PASS` with `-D warnings`.
- HoloLake architecture routing: `29 passed, 0 failed`.
@ -38,11 +38,19 @@ The source now implements one bounded PNCC lifecycle cycle without a HoloLake UI
## Facts that remain zero
- general-purpose persona organs and arbitrary execution runtime;
- crash-recovery administration for an interrupted live PNCC session;
- HoloLake human projection and visual design;
- packaged desktop acceptance and installation;
- repository publication of this implementation commit;
- deployment and online runtime health.
## Publication readback
- REPO-014 remote `main`: `acf0d5f80bc3b5470571afa151d39a2db9421fdb`.
- Fresh clone: clean work tree and `git fsck --full --strict` passed.
- GHNQG receipt: `GHNQG-acf0d5f80bc3b5470571afa151d39a2db9421fdb.hdlp`, `PASS_100`.
- Commit author: `铸渊 / ICE-P-ZY001 <ice-p-zy001@persona.hololake.local>`; human responsibility remains a separate trailer.
Interrupted-session recovery is continued in
[the next runtime receipt](./2026-08-11-hololake-pncc-interrupted-session-recovery.md).
This receipt proves source behavior and tests only. It does not claim that a desktop artifact or online
persona runtime is running.

View file

@ -0,0 +1,45 @@
# GH-PNCC interrupted-session inspection and recovery receipt
- Development ID: `DEV-20260810-014`
- Persona cognitive author: `ICE-P-ZY001 / 铸渊`
- Human responsibility subject: `ICE-GL∞ / 冰朔`
- Source component: `product-source/hololake-platform/src-tauri/src/persona_code_channel.rs`
- State: `LOCAL_SOURCE_IMPLEMENTED_AND_FULL_RUST_ROUTING_TESTED`
## Implemented facts
The runtime now exposes an exact session inspection command and a fail-closed recovery command. Both require
the caller to name the session, expected persona, and expected canonical repository. Inspection verifies the
hash-linked event journal and reports the recorded/observed Git heads, work-tree cleanliness, exact lease
ownership, session state, last event, attribution, and one recovery disposition.
Recovery does not treat age as proof of death and never steals a lease. It releases the exact primary only
for either:
1. a clean repository still at the session's recorded head, which is closed as an uncommitted interruption;
2. a clean repository advanced by exactly one commit whose parent is the recorded head and whose persona
checkpoint, manifest pointer, Git author, human-responsibility trailer, persona-author trailer and
development-id trailer all match the interrupted session.
3. an exact same-session lease left behind after the event journal and session record already prove dormancy,
with a clean repository still at the recorded head.
Dirty repositories, missing or mismatched leases, broken journals, unknown Git advances, checkpoint
mismatches and attribution mismatches fail closed without resetting files or releasing the primary.
## Current verification
- PNCC focused Rust tests: `10 passed, 0 failed`.
- Rust full suite: `1150 passed, 0 failed, 2 ignored`; integration test `1 passed`.
- Rust clippy for all targets: `PASS` with `-D warnings`.
- HoloLake architecture routing: `29 passed, 0 failed`.
- Tests cover safe abort, safe committed-session finalization, dirty-repository refusal, second-primary
exclusion, path containment, model/evidence scope, checkpoint promotion and persona Git attribution.
## Remaining acceptance
- native authority/core and GHNQG pre-push quality gates;
- repository commit, publication and fresh-clone readback;
- general-purpose organ contract beyond the first read-only fact organ;
- HoloLake human projection, desktop artifact, installation, deployment and online health.
This receipt proves local source behavior only until the remaining quality and publication gates complete.

View file

@ -18,7 +18,7 @@
>
> 开发编号:`DEV-20260810-014`
>
> 状态:`CURRENT_FIRST_PRODUCT_CORE · FIRST_READ_ONLY_RUNTIME_CYCLE_SOURCE_IMPLEMENTED_100 · DESKTOP_NOT_INTEGRATED_0`
> 状态:`CURRENT_FIRST_PRODUCT_CORE · READ_ONLY_RUNTIME_AND_INTERRUPTED_SESSION_RECOVERY_SOURCE_IMPLEMENTED_100 · DESKTOP_NOT_INTEGRATED_0`
## 1 · 产品裁决
@ -164,6 +164,7 @@ git_engine_selected: 100
first_read_only_runtime_cycle_source_implemented: 100
persona_git_identity_commit_implemented: 100
semantic_attribution_first_cycle_implemented: 100
interrupted_session_inspection_and_recovery_source_implemented: 100
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0
@ -176,3 +177,7 @@ runtime_health: 0
器官文件白名单、真实模型适配调用、结构化事实收执、检查点提升、人格 Git 作者提交、事件链、
器官释放与休眠。该事实由单元测试验证,尚未构建桌面制品、接入人类投影、发布安装包或部署
在线服务;因此不能从源码完成推导出桌面可用或线上健康。
当前源码也已实现中断会话检查与安全恢复:只在事件链、租约、仓库和人格归因共同证明安全时
终止未提交会话,或为已由该人格完成但尚未休眠的单一检查点提交补齐收尾。脏仓库、未知 Git
推进、错误归因和损坏事件链一律保留主锁并要求人工审查;恢复器不会按时间猜测主控已死亡。

View file

@ -46,6 +46,14 @@ declared evidence paths, promotes the checkpoint with the persona's Git identity
The Git commit still names the human responsibility subject in a dedicated trailer, so authorship and legal
responsibility remain visible without presenting the human as the code's cognitive author.
Session control is explicit and evidence-bound. `inspect_persona_code_channel_session` classifies an exact
session as `SAFE_ABORT_UNCOMMITTED`, `SAFE_FINALIZE_COMMITTED`, `SAFE_RELEASE_DORMANT_LEASE`,
`ALREADY_DORMANT`, or `MANUAL_REVIEW_REQUIRED`. The recovery command accepts only a proven safe state. For
active interruptions it appends recovery and dormancy events to the same journal, updates the session record,
and then removes the exact lease. It can also remove a same-session lease left behind after a fully recorded
dormancy transition. It does not use elapsed time as proof that a primary is dead and does not clean, reset,
or overwrite a dirty persona repository.
## `HoloLakeUiPlugin`
A versioned, declarative presentation package for one host-owned semantic surface. It contains a manifest,

View file

@ -29,6 +29,14 @@ commit trailers preserve the separate human responsibility subject and persona c
tested source implementation of one read-only lifecycle cycle; packaged desktop integration, publication,
deployment, and online health remain separate facts.
`inspect_persona_code_channel_session` and `recover_persona_code_channel_session` add fail-closed interrupted
session administration. Inspection re-verifies the session identity, canonical repository, event hash chain,
lease owner, current Git head and work-tree cleanliness. Recovery never steals or expires a lease by age. It
releases the primary only when the repository is clean and the state is provably either an uncommitted
interruption at the recorded head or a single persona-attributed checkpoint commit whose parent, manifest,
checkpoint identity and commit trailers all match the session. Dirty repositories, unknown Git advances,
broken journals and attribution mismatches retain the lease and require manual review.
## User-node sovereignty
HoloLake has no platform-hosted user runtime. Each human has one canonical, independently operated node:

View file

@ -525,6 +525,8 @@ macro_rules! app_invoke_handler {
guanghu_living_system::guanghu_living_system_plan,
persona_code_channel::prepare_persona_code_channel_wake,
persona_code_channel::run_persona_code_channel_fact_task,
persona_code_channel::inspect_persona_code_channel_session,
persona_code_channel::recover_persona_code_channel_session,
guanghu_router::guanghu_router_connect,
guanghu_router::guanghu_router_disconnect,
guanghu_router::guanghu_router_approve,

View file

@ -159,6 +159,50 @@ pub struct PersonaFactTaskReceipt {
pub attribution: PersonaAttribution,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaSessionControlInput {
pub session_id: String,
pub expected_persona_id: String,
pub expected_repository_path: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaSessionInspectionReceipt {
pub schema: &'static str,
pub session_id: String,
pub persona_id: String,
pub repository_path: String,
pub recorded_git_head: String,
pub observed_git_head: String,
pub repository_clean: bool,
pub session_state: String,
pub primary_lease_held: bool,
pub event_chain_valid: bool,
pub last_event: String,
pub recovery_disposition: &'static str,
pub events: Vec<PersonaLifecycleEvent>,
pub attribution: PersonaAttribution,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaSessionRecoveryReceipt {
pub schema: &'static str,
pub receipt_id: String,
pub session_id: String,
pub persona_id: String,
pub previous_session_state: String,
pub recovered_git_head: String,
pub checkpoint_path: String,
pub runtime_state: &'static str,
pub recovery_action: &'static str,
pub primary_lease_released: bool,
pub events: Vec<PersonaLifecycleEvent>,
pub attribution: PersonaAttribution,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct PersonaSessionRecord {
@ -543,7 +587,7 @@ fn load_session_record(
Ok(record)
}
fn read_event_journal(
fn verify_event_journal(
runtime_root: &Path,
record: &PersonaSessionRecord,
) -> Result<Vec<PersonaLifecycleEvent>, String> {
@ -583,6 +627,17 @@ fn read_event_journal(
}
events.push(event);
}
if events.is_empty() {
return Err("PERSONA_EVENT_CHAIN_EMPTY".into());
}
Ok(events)
}
fn read_ready_event_journal(
runtime_root: &Path,
record: &PersonaSessionRecord,
) -> Result<Vec<PersonaLifecycleEvent>, String> {
let events = verify_event_journal(runtime_root, record)?;
if events.len() != 3 || events.last().map(|event| event.kind.as_str()) != Some("ORGAN_ACTIVE") {
return Err("PERSONA_SESSION_NOT_READY_FOR_FACT_TASK".into());
}
@ -647,6 +702,60 @@ fn require_primary_lease(
Ok(path)
}
fn primary_lease_held_by_session(
runtime_root: &Path,
record: &PersonaSessionRecord,
) -> Result<bool, String> {
let path = runtime_root.join("leases").join(format!(
"{}.json",
validated_id("PERSONA_ID", &record.persona_id)?
));
let bytes = match fs::read(path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(format!("PERSONA_PRIMARY_LEASE_READ_FAILED: {error}")),
};
let value: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|error| format!("PERSONA_PRIMARY_LEASE_INVALID: {error}"))?;
Ok(value.get("sessionId").and_then(serde_json::Value::as_str)
== Some(record.session_id.as_str()))
}
fn repository_is_clean(repository: &Path) -> Result<bool, String> {
let status = crate::git::git_command_at(repository)
.and_then(|mut command| {
command
.args(["status", "--porcelain", "--untracked-files=all"])
.output()
})
.map_err(|error| format!("PERSONA_GIT_STATUS_FAILED: {error}"))?;
Ok(stdout("GIT_STATUS", status)?.is_empty())
}
fn validate_session_control(
runtime_root: &Path,
input: &PersonaSessionControlInput,
) -> Result<(PersonaSessionRecord, PathBuf, String, bool), String> {
let session_id = validated_id("SESSION_ID", &input.session_id)?;
let expected_persona_id = validated_id("EXPECTED_PERSONA_ID", &input.expected_persona_id)?;
let record = load_session_record(runtime_root, &session_id)?;
if record.persona_id != expected_persona_id {
return Err("PERSONA_ID_MISMATCH".into());
}
let expected_repository = Path::new(&input.expected_repository_path)
.canonicalize()
.map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?;
let recorded_repository = Path::new(&record.repository_path)
.canonicalize()
.map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?;
if expected_repository != recorded_repository {
return Err("PERSONA_REPOSITORY_MISMATCH".into());
}
let (repository, observed_head) = exact_repository(&recorded_repository)?;
let clean = repository_is_clean(&repository)?;
Ok((record, repository, observed_head, clean))
}
fn read_fact_context(repository: &Path, paths: &[String]) -> Result<String, String> {
if paths.is_empty() {
return Err("FACT_ORGAN_HAS_NO_DECLARED_PATHS".into());
@ -976,6 +1085,278 @@ fn finish_failed_session(
let _ = fs::remove_file(lease_path);
}
fn validate_recoverable_persona_commit(
repository: &Path,
record: &PersonaSessionRecord,
observed_head: &str,
) -> Result<String, String> {
let parent = git_output(
repository,
&["rev-parse", &format!("{observed_head}^")],
"PERSONA_RECOVERY_PARENT",
)?;
if parent != record.git_head {
return Err("PERSONA_RECOVERY_UNKNOWN_GIT_ADVANCE".into());
}
let checkpoint_relative = format!(
".hololake/persona/checkpoints/{}.json",
validated_id("SESSION_ID", &record.session_id)?
);
let checkpoint_path = repository_file(repository, &checkpoint_relative)?;
let checkpoint: serde_json::Value = serde_json::from_slice(
&fs::read(checkpoint_path)
.map_err(|error| format!("PERSONA_CHECKPOINT_READ_FAILED: {error}"))?,
)
.map_err(|error| format!("PERSONA_CHECKPOINT_INVALID: {error}"))?;
if checkpoint.get("schema").and_then(serde_json::Value::as_str)
!= Some("hololake.persona-checkpoint/v1")
|| checkpoint
.get("sessionId")
.and_then(serde_json::Value::as_str)
!= Some(record.session_id.as_str())
|| checkpoint
.get("personaId")
.and_then(serde_json::Value::as_str)
!= Some(record.persona_id.as_str())
|| checkpoint
.get("previousGitHead")
.and_then(serde_json::Value::as_str)
!= Some(record.git_head.as_str())
{
return Err("PERSONA_RECOVERY_CHECKPOINT_MISMATCH".into());
}
let manifest = load_manifest(repository)?;
if manifest.persona_id != record.persona_id
|| manifest.current_checkpoint != checkpoint_relative
{
return Err("PERSONA_RECOVERY_MANIFEST_MISMATCH".into());
}
validate_git_identity(&manifest.git_identity)?;
let author = git_output(
repository,
&["show", "-s", "--format=%an%n%ae%n%B", observed_head],
"PERSONA_RECOVERY_COMMIT_METADATA",
)?;
let mut lines = author.lines();
if lines.next() != Some(manifest.git_identity.author_name.as_str())
|| lines.next() != Some(manifest.git_identity.author_email.as_str())
|| !author.contains(&format!(
"GuangHu-Human-Responsibility: {}",
record.attribution.human_responsibility_subject
))
|| !author.contains(&format!(
"GuangHu-Persona-Cognitive-Author: {}",
record.attribution.persona_cognitive_author
))
|| !author.contains(&format!(
"GuangHu-Development-ID: {}",
record.attribution.development_id
))
{
return Err("PERSONA_RECOVERY_COMMIT_ATTRIBUTION_MISMATCH".into());
}
Ok(checkpoint_relative)
}
fn recovery_disposition(
repository: &Path,
record: &PersonaSessionRecord,
observed_head: &str,
clean: bool,
lease_held: bool,
last_event: &str,
) -> &'static str {
if record.state.starts_with("DORMANT") {
return if lease_held && clean && observed_head == record.git_head && last_event == "DORMANT"
{
"SAFE_RELEASE_DORMANT_LEASE"
} else if lease_held {
"MANUAL_REVIEW_REQUIRED"
} else {
"ALREADY_DORMANT"
};
}
if !lease_held
|| !clean
|| !matches!(
record.state.as_str(),
"BOUND_NOT_INFERENCING" | "INFERENCING"
)
{
return "MANUAL_REVIEW_REQUIRED";
}
if observed_head == record.git_head {
return "SAFE_ABORT_UNCOMMITTED";
}
if validate_recoverable_persona_commit(repository, record, observed_head).is_ok() {
"SAFE_FINALIZE_COMMITTED"
} else {
"MANUAL_REVIEW_REQUIRED"
}
}
fn inspect_session_at(
runtime_root: &Path,
input: PersonaSessionControlInput,
) -> Result<PersonaSessionInspectionReceipt, String> {
let (record, repository, observed_head, clean) =
validate_session_control(runtime_root, &input)?;
let events = verify_event_journal(runtime_root, &record)?;
let lease_held = primary_lease_held_by_session(runtime_root, &record)?;
let last_event = events
.last()
.expect("verified non-empty event chain")
.kind
.clone();
let disposition = recovery_disposition(
&repository,
&record,
&observed_head,
clean,
lease_held,
&last_event,
);
Ok(PersonaSessionInspectionReceipt {
schema: "hololake.pncc-session-inspection/v1",
session_id: record.session_id,
persona_id: record.persona_id,
repository_path: repository.to_string_lossy().into_owned(),
recorded_git_head: record.git_head,
observed_git_head: observed_head,
repository_clean: clean,
session_state: record.state,
primary_lease_held: lease_held,
event_chain_valid: true,
last_event,
recovery_disposition: disposition,
events,
attribution: record.attribution,
})
}
fn recover_session_at(
runtime_root: &Path,
input: PersonaSessionControlInput,
timestamp: &str,
) -> Result<PersonaSessionRecoveryReceipt, String> {
let timestamp = validated_text("TIMESTAMP", timestamp, MAX_ID_BYTES)?;
let (mut record, repository, observed_head, clean) =
validate_session_control(runtime_root, &input)?;
let lease_path = require_primary_lease(runtime_root, &record)?;
if !clean {
return Err("PERSONA_RECOVERY_REFUSED_DIRTY_REPOSITORY".into());
}
let mut events = verify_event_journal(runtime_root, &record)?;
let previous_state = record.state.clone();
if record.state.starts_with("DORMANT") {
if observed_head != record.git_head
|| events.last().map(|event| event.kind.as_str()) != Some("DORMANT")
{
return Err("PERSONA_DORMANT_LEASE_REQUIRES_MANUAL_REVIEW".into());
}
fs::remove_file(&lease_path)
.map_err(|error| format!("PERSONA_PRIMARY_LEASE_RELEASE_FAILED: {error}"))?;
let receipt_hash = &events
.last()
.expect("verified non-empty event chain")
.event_hash;
return Ok(PersonaSessionRecoveryReceipt {
schema: "hololake.pncc-session-recovery-receipt/v1",
receipt_id: format!("PNCC-RECOVERY-{}", &receipt_hash[..20]),
session_id: record.session_id,
persona_id: record.persona_id,
previous_session_state: previous_state,
recovered_git_head: observed_head,
checkpoint_path: record.checkpoint_path,
runtime_state: "DORMANT_AFTER_RECOVERY",
recovery_action: "RELEASED_DORMANT_LEASE",
primary_lease_released: true,
events,
attribution: record.attribution,
});
}
if !matches!(
record.state.as_str(),
"BOUND_NOT_INFERENCING" | "INFERENCING"
) {
return Err("PERSONA_SESSION_STATE_REQUIRES_MANUAL_REVIEW".into());
}
let (action, checkpoint_path) = if observed_head == record.git_head {
(
"ABORTED_UNCOMMITTED_SESSION",
record.checkpoint_path.clone(),
)
} else {
(
"FINALIZED_PERSONA_COMMIT",
validate_recoverable_persona_commit(&repository, &record, &observed_head)?,
)
};
let organ = record.active_organ.clone();
append_event(
runtime_root,
&record,
&mut events,
"SESSION_RECOVERY_REQUESTED",
&observed_head,
&timestamp,
Some(&organ),
)?;
if action == "FINALIZED_PERSONA_COMMIT"
&& !events
.iter()
.any(|event| event.kind == "CHECKPOINT_COMMITTED")
{
append_event(
runtime_root,
&record,
&mut events,
"CHECKPOINT_COMMITTED",
&observed_head,
&timestamp,
Some(&organ),
)?;
}
append_event(
runtime_root,
&record,
&mut events,
"ORGAN_RELEASED",
&observed_head,
&timestamp,
Some(&organ),
)?;
let dormant = append_event(
runtime_root,
&record,
&mut events,
"DORMANT",
&observed_head,
&timestamp,
None,
)?;
record.git_head = observed_head.clone();
record.checkpoint_path = checkpoint_path.clone();
record.state = "DORMANT_AFTER_RECOVERY".into();
write_session_record(runtime_root, &record)?;
fs::remove_file(&lease_path)
.map_err(|error| format!("PERSONA_PRIMARY_LEASE_RELEASE_FAILED: {error}"))?;
Ok(PersonaSessionRecoveryReceipt {
schema: "hololake.pncc-session-recovery-receipt/v1",
receipt_id: format!("PNCC-RECOVERY-{}", &dormant.event_hash[..20]),
session_id: record.session_id,
persona_id: record.persona_id,
previous_session_state: previous_state,
recovered_git_head: observed_head,
checkpoint_path,
runtime_state: "DORMANT_AFTER_RECOVERY",
recovery_action: action,
primary_lease_released: true,
events,
attribution: record.attribution,
})
}
fn run_fact_task_at<F>(
runtime_root: &Path,
input: PersonaFactTaskInput,
@ -1014,7 +1395,7 @@ where
return Err("PERSONA_GIT_CHANGED_AFTER_WAKE".into());
}
require_clean_repository(&exact_repository)?;
let mut events = read_event_journal(runtime_root, &record)?;
let mut events = read_ready_event_journal(runtime_root, &record)?;
let inference_event = append_event(
runtime_root,
&record,
@ -1252,6 +1633,23 @@ pub async fn run_persona_code_channel_fact_task(
.map_err(|error| format!("PERSONA_FACT_TASK_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub fn inspect_persona_code_channel_session(
input: PersonaSessionControlInput,
) -> Result<PersonaSessionInspectionReceipt, String> {
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
inspect_session_at(&runtime_root, input)
}
#[tauri::command]
pub fn recover_persona_code_channel_session(
input: PersonaSessionControlInput,
) -> Result<PersonaSessionRecoveryReceipt, String> {
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
recover_session_at(&runtime_root, input, &timestamp)
}
#[cfg(test)]
mod tests {
use super::*;
@ -1378,6 +1776,14 @@ mod tests {
}
}
fn session_control(repo: &Path, session_id: &str) -> PersonaSessionControlInput {
PersonaSessionControlInput {
session_id: session_id.into(),
expected_persona_id: "ICE-P-ZY001".into(),
expected_repository_path: repo.to_string_lossy().into_owned(),
}
}
#[test]
fn prepares_exact_persona_git_wake_with_hash_chained_events() {
let repo = persona_repo();
@ -1613,4 +2019,162 @@ mod tests {
let session = load_session_record(runtime.path(), "PNCC-SESSION-REJECTED").unwrap();
assert_eq!(session.state, "DORMANT_AFTER_FAILURE");
}
#[test]
fn inspects_and_safely_aborts_an_uncommitted_interrupted_session() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
"PNCC-SESSION-INTERRUPTED",
"2026-08-11T00:00:00.000Z",
)
.unwrap();
let inspection = inspect_session_at(
runtime.path(),
session_control(repo.path(), "PNCC-SESSION-INTERRUPTED"),
)
.unwrap();
assert_eq!(inspection.recovery_disposition, "SAFE_ABORT_UNCOMMITTED");
assert!(inspection.repository_clean);
assert!(inspection.primary_lease_held);
let receipt = recover_session_at(
runtime.path(),
session_control(repo.path(), "PNCC-SESSION-INTERRUPTED"),
"2026-08-11T00:01:00.000Z",
)
.unwrap();
assert_eq!(receipt.recovery_action, "ABORTED_UNCOMMITTED_SESSION");
assert_eq!(receipt.runtime_state, "DORMANT_AFTER_RECOVERY");
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
assert_eq!(receipt.events.last().unwrap().kind, "DORMANT");
}
#[test]
fn refuses_recovery_when_the_persona_repository_is_dirty() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
"PNCC-SESSION-DIRTY-RECOVERY",
"2026-08-11T00:00:00.000Z",
)
.unwrap();
fs::write(repo.path().join("unknown-change.txt"), "do not erase\n").unwrap();
let inspection = inspect_session_at(
runtime.path(),
session_control(repo.path(), "PNCC-SESSION-DIRTY-RECOVERY"),
)
.unwrap();
assert_eq!(inspection.recovery_disposition, "MANUAL_REVIEW_REQUIRED");
let error = recover_session_at(
runtime.path(),
session_control(repo.path(), "PNCC-SESSION-DIRTY-RECOVERY"),
"2026-08-11T00:01:00.000Z",
)
.unwrap_err();
assert!(error.contains("REFUSED_DIRTY_REPOSITORY"));
assert!(runtime.path().join("leases/ICE-P-ZY001.json").exists());
}
#[test]
fn finalizes_an_attributed_persona_commit_interrupted_before_dormancy() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let session_id = "PNCC-SESSION-COMMIT-INTERRUPTED";
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
session_id,
"2026-08-11T00:00:00.000Z",
)
.unwrap();
let repository = repo.path().canonicalize().unwrap();
let record = load_session_record(runtime.path(), session_id).unwrap();
let checkpoint_relative = format!(".hololake/persona/checkpoints/{session_id}.json");
let checkpoint = serde_json::json!({
"schema": "hololake.persona-checkpoint/v1",
"sessionId": session_id,
"personaId": record.persona_id,
"previousGitHead": record.git_head,
"createdAt": "2026-08-11T00:00:01.000Z",
"result": {"summary": "fixture"}
});
write_json_file(
&repository.join(&checkpoint_relative),
&checkpoint,
"TEST_CHECKPOINT",
)
.unwrap();
let mut manifest = load_manifest(&repository).unwrap();
manifest.current_checkpoint = checkpoint_relative.clone();
write_json_file(&repository.join(MANIFEST_PATH), &manifest, "TEST_MANIFEST").unwrap();
let committed_head =
persona_git_commit(&repository, &manifest, &checkpoint_relative, &record).unwrap();
let inspection =
inspect_session_at(runtime.path(), session_control(repo.path(), session_id)).unwrap();
assert_eq!(inspection.recovery_disposition, "SAFE_FINALIZE_COMMITTED");
let receipt = recover_session_at(
runtime.path(),
session_control(repo.path(), session_id),
"2026-08-11T00:01:00.000Z",
)
.unwrap();
assert_eq!(receipt.recovery_action, "FINALIZED_PERSONA_COMMIT");
assert_eq!(receipt.recovered_git_head, committed_head);
assert_eq!(receipt.checkpoint_path, checkpoint_relative);
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
require_clean_repository(repo.path()).unwrap();
}
#[test]
fn releases_an_exact_lease_left_after_the_session_became_dormant() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let session_id = "PNCC-SESSION-DORMANT-LEASE";
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
session_id,
"2026-08-11T00:00:00.000Z",
)
.unwrap();
recover_session_at(
runtime.path(),
session_control(repo.path(), session_id),
"2026-08-11T00:01:00.000Z",
)
.unwrap();
let record = load_session_record(runtime.path(), session_id).unwrap();
acquire_primary_lease(
runtime.path(),
&record.persona_id,
&record.session_id,
&record.git_head,
&record.node_id,
"2026-08-11T00:01:01.000Z",
)
.unwrap();
let inspection =
inspect_session_at(runtime.path(), session_control(repo.path(), session_id)).unwrap();
assert_eq!(
inspection.recovery_disposition,
"SAFE_RELEASE_DORMANT_LEASE"
);
let receipt = recover_session_at(
runtime.path(),
session_control(repo.path(), session_id),
"2026-08-11T00:02:00.000Z",
)
.unwrap();
assert_eq!(receipt.recovery_action, "RELEASED_DORMANT_LEASE");
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
}
}

View file

@ -118,7 +118,7 @@
"human_projection": "HOLOLAKE_LIVE_READ_MODEL",
"forgejo_role": "OPTIONAL_COMPATIBILITY_COLLABORATION_ADAPTER",
"runtime_implemented": true,
"runtime_scope": "FIRST_READ_ONLY_FACT_CYCLE_SOURCE_IMPLEMENTED_AND_TESTED",
"runtime_scope": "FIRST_READ_ONLY_FACT_CYCLE_AND_FAIL_CLOSED_INTERRUPTED_SESSION_RECOVERY_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_FIRST_REAL_READ_ONLY_WAKE_CLOSED_LOOP",
"next_minimum_stage": "GH_PNCC_GENERAL_PURPOSE_ORGAN_CONTRACT_WITHOUT_UI",
"next_stage_started": true,
"heartbeat_automation": "pncc",
"heartbeat_state": "ACTIVE_EVERY_10_MINUTES_UNTIL_TASK_TERMINAL"

View file

@ -1,8 +1,8 @@
{
"schema": "hololake.persona-native-code-channel/v1",
"record_id": "HLP-PERSONA-NATIVE-CODE-CHANNEL-001",
"version": "2026-08-11.1",
"state": "CURRENT_FIRST_PRODUCT_CORE_FIRST_READ_ONLY_RUNTIME_CYCLE_SOURCE_IMPLEMENTED",
"version": "2026-08-11.2",
"state": "CURRENT_FIRST_PRODUCT_CORE_READ_ONLY_RUNTIME_AND_INTERRUPTED_SESSION_RECOVERY_SOURCE_IMPLEMENTED",
"development_id": "DEV-20260810-014",
"product": {
"formal_name_zh": "光湖人格原生代码频道",
@ -101,6 +101,7 @@
"machine_navigation_registered": 100,
"first_read_only_runtime_cycle_source_implemented": 100,
"persona_git_identity_commit_implemented": 100,
"interrupted_session_inspection_and_recovery_source_implemented": 100,
"general_purpose_persona_runtime_implemented": 0,
"human_live_projection_implemented": 0,
"hololake_integrated": 0,

View file

@ -69,12 +69,16 @@ test("the first source runtime cycle stays distinct from integration and deploym
assert.equal(channel.truth.architecture_registered, 100);
assert.equal(channel.truth.first_read_only_runtime_cycle_source_implemented, 100);
assert.equal(channel.truth.persona_git_identity_commit_implemented, 100);
assert.equal(
channel.truth.interrupted_session_inspection_and_recovery_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,
"FIRST_READ_ONLY_FACT_CYCLE_SOURCE_IMPLEMENTED_AND_TESTED",
"FIRST_READ_ONLY_FACT_CYCLE_AND_FAIL_CLOSED_INTERRUPTED_SESSION_RECOVERY_SOURCE_IMPLEMENTED_AND_TESTED",
);
assert.equal(channel.truth.hololake_integrated, 0);
assert.equal(channel.truth.artifact_built, 0);