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:
parent
acf0d5f80b
commit
5b042a65fe
11 changed files with 657 additions and 11 deletions
|
|
@ -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 实例在线;其他实例拉取后需演习+真实核对才可宣称上线 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
推进、错误归因和损坏事件链一律保留主锁并要求人工审查;恢复器不会按时间猜测主控已死亡。
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
×tamp,
|
||||
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,
|
||||
×tamp,
|
||||
Some(&organ),
|
||||
)?;
|
||||
}
|
||||
append_event(
|
||||
runtime_root,
|
||||
&record,
|
||||
&mut events,
|
||||
"ORGAN_RELEASED",
|
||||
&observed_head,
|
||||
×tamp,
|
||||
Some(&organ),
|
||||
)?;
|
||||
let dormant = append_event(
|
||||
runtime_root,
|
||||
&record,
|
||||
&mut events,
|
||||
"DORMANT",
|
||||
&observed_head,
|
||||
×tamp,
|
||||
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, ×tamp)
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue