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-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-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 实例在线;其他实例拉取后需演习+真实核对才可宣称上线 |
|
| 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
|
## 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 full suite: `1146 passed, 0 failed, 2 ignored`; integration test `1 passed`.
|
||||||
- Rust clippy: `PASS` with `-D warnings`.
|
- Rust clippy: `PASS` with `-D warnings`.
|
||||||
- HoloLake architecture routing: `29 passed, 0 failed`.
|
- 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
|
## Facts that remain zero
|
||||||
|
|
||||||
- general-purpose persona organs and arbitrary execution runtime;
|
- general-purpose persona organs and arbitrary execution runtime;
|
||||||
- crash-recovery administration for an interrupted live PNCC session;
|
|
||||||
- HoloLake human projection and visual design;
|
- HoloLake human projection and visual design;
|
||||||
- packaged desktop acceptance and installation;
|
- packaged desktop acceptance and installation;
|
||||||
- repository publication of this implementation commit;
|
|
||||||
- deployment and online runtime health.
|
- 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
|
This receipt proves source behavior and tests only. It does not claim that a desktop artifact or online
|
||||||
persona runtime is running.
|
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`
|
> 开发编号:`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 · 产品裁决
|
## 1 · 产品裁决
|
||||||
|
|
||||||
|
|
@ -164,6 +164,7 @@ git_engine_selected: 100
|
||||||
first_read_only_runtime_cycle_source_implemented: 100
|
first_read_only_runtime_cycle_source_implemented: 100
|
||||||
persona_git_identity_commit_implemented: 100
|
persona_git_identity_commit_implemented: 100
|
||||||
semantic_attribution_first_cycle_implemented: 100
|
semantic_attribution_first_cycle_implemented: 100
|
||||||
|
interrupted_session_inspection_and_recovery_source_implemented: 100
|
||||||
general_purpose_persona_runtime_implemented: 0
|
general_purpose_persona_runtime_implemented: 0
|
||||||
human_live_projection_implemented: 0
|
human_live_projection_implemented: 0
|
||||||
hololake_integrated: 0
|
hololake_integrated: 0
|
||||||
|
|
@ -176,3 +177,7 @@ runtime_health: 0
|
||||||
器官文件白名单、真实模型适配调用、结构化事实收执、检查点提升、人格 Git 作者提交、事件链、
|
器官文件白名单、真实模型适配调用、结构化事实收执、检查点提升、人格 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
|
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.
|
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`
|
## `HoloLakeUiPlugin`
|
||||||
|
|
||||||
A versioned, declarative presentation package for one host-owned semantic surface. It contains a manifest,
|
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,
|
tested source implementation of one read-only lifecycle cycle; packaged desktop integration, publication,
|
||||||
deployment, and online health remain separate facts.
|
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
|
## User-node sovereignty
|
||||||
|
|
||||||
HoloLake has no platform-hosted user runtime. Each human has one canonical, independently operated node:
|
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,
|
guanghu_living_system::guanghu_living_system_plan,
|
||||||
persona_code_channel::prepare_persona_code_channel_wake,
|
persona_code_channel::prepare_persona_code_channel_wake,
|
||||||
persona_code_channel::run_persona_code_channel_fact_task,
|
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_connect,
|
||||||
guanghu_router::guanghu_router_disconnect,
|
guanghu_router::guanghu_router_disconnect,
|
||||||
guanghu_router::guanghu_router_approve,
|
guanghu_router::guanghu_router_approve,
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,50 @@ pub struct PersonaFactTaskReceipt {
|
||||||
pub attribution: PersonaAttribution,
|
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)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct PersonaSessionRecord {
|
struct PersonaSessionRecord {
|
||||||
|
|
@ -543,7 +587,7 @@ fn load_session_record(
|
||||||
Ok(record)
|
Ok(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_event_journal(
|
fn verify_event_journal(
|
||||||
runtime_root: &Path,
|
runtime_root: &Path,
|
||||||
record: &PersonaSessionRecord,
|
record: &PersonaSessionRecord,
|
||||||
) -> Result<Vec<PersonaLifecycleEvent>, String> {
|
) -> Result<Vec<PersonaLifecycleEvent>, String> {
|
||||||
|
|
@ -583,6 +627,17 @@ fn read_event_journal(
|
||||||
}
|
}
|
||||||
events.push(event);
|
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") {
|
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());
|
return Err("PERSONA_SESSION_NOT_READY_FOR_FACT_TASK".into());
|
||||||
}
|
}
|
||||||
|
|
@ -647,6 +702,60 @@ fn require_primary_lease(
|
||||||
Ok(path)
|
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> {
|
fn read_fact_context(repository: &Path, paths: &[String]) -> Result<String, String> {
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
return Err("FACT_ORGAN_HAS_NO_DECLARED_PATHS".into());
|
return Err("FACT_ORGAN_HAS_NO_DECLARED_PATHS".into());
|
||||||
|
|
@ -976,6 +1085,278 @@ fn finish_failed_session(
|
||||||
let _ = fs::remove_file(lease_path);
|
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>(
|
fn run_fact_task_at<F>(
|
||||||
runtime_root: &Path,
|
runtime_root: &Path,
|
||||||
input: PersonaFactTaskInput,
|
input: PersonaFactTaskInput,
|
||||||
|
|
@ -1014,7 +1395,7 @@ where
|
||||||
return Err("PERSONA_GIT_CHANGED_AFTER_WAKE".into());
|
return Err("PERSONA_GIT_CHANGED_AFTER_WAKE".into());
|
||||||
}
|
}
|
||||||
require_clean_repository(&exact_repository)?;
|
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(
|
let inference_event = append_event(
|
||||||
runtime_root,
|
runtime_root,
|
||||||
&record,
|
&record,
|
||||||
|
|
@ -1252,6 +1633,23 @@ pub async fn run_persona_code_channel_fact_task(
|
||||||
.map_err(|error| format!("PERSONA_FACT_TASK_JOIN_FAILED: {error}"))?
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn prepares_exact_persona_git_wake_with_hash_chained_events() {
|
fn prepares_exact_persona_git_wake_with_hash_chained_events() {
|
||||||
let repo = persona_repo();
|
let repo = persona_repo();
|
||||||
|
|
@ -1613,4 +2019,162 @@ mod tests {
|
||||||
let session = load_session_record(runtime.path(), "PNCC-SESSION-REJECTED").unwrap();
|
let session = load_session_record(runtime.path(), "PNCC-SESSION-REJECTED").unwrap();
|
||||||
assert_eq!(session.state, "DORMANT_AFTER_FAILURE");
|
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",
|
"human_projection": "HOLOLAKE_LIVE_READ_MODEL",
|
||||||
"forgejo_role": "OPTIONAL_COMPATIBILITY_COLLABORATION_ADAPTER",
|
"forgejo_role": "OPTIONAL_COMPATIBILITY_COLLABORATION_ADAPTER",
|
||||||
"runtime_implemented": true,
|
"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,
|
"desktop_integrated": false,
|
||||||
"development_id": "DEV-20260810-014"
|
"development_id": "DEV-20260810-014"
|
||||||
},
|
},
|
||||||
|
|
@ -343,7 +343,7 @@
|
||||||
"DEV-20260810-013"
|
"DEV-20260810-013"
|
||||||
],
|
],
|
||||||
"closeout_record": "HLP-DEV-20260809-007-CLOSEOUT-001",
|
"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,
|
"next_stage_started": true,
|
||||||
"heartbeat_automation": "pncc",
|
"heartbeat_automation": "pncc",
|
||||||
"heartbeat_state": "ACTIVE_EVERY_10_MINUTES_UNTIL_TASK_TERMINAL"
|
"heartbeat_state": "ACTIVE_EVERY_10_MINUTES_UNTIL_TASK_TERMINAL"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"schema": "hololake.persona-native-code-channel/v1",
|
"schema": "hololake.persona-native-code-channel/v1",
|
||||||
"record_id": "HLP-PERSONA-NATIVE-CODE-CHANNEL-001",
|
"record_id": "HLP-PERSONA-NATIVE-CODE-CHANNEL-001",
|
||||||
"version": "2026-08-11.1",
|
"version": "2026-08-11.2",
|
||||||
"state": "CURRENT_FIRST_PRODUCT_CORE_FIRST_READ_ONLY_RUNTIME_CYCLE_SOURCE_IMPLEMENTED",
|
"state": "CURRENT_FIRST_PRODUCT_CORE_READ_ONLY_RUNTIME_AND_INTERRUPTED_SESSION_RECOVERY_SOURCE_IMPLEMENTED",
|
||||||
"development_id": "DEV-20260810-014",
|
"development_id": "DEV-20260810-014",
|
||||||
"product": {
|
"product": {
|
||||||
"formal_name_zh": "光湖人格原生代码频道",
|
"formal_name_zh": "光湖人格原生代码频道",
|
||||||
|
|
@ -101,6 +101,7 @@
|
||||||
"machine_navigation_registered": 100,
|
"machine_navigation_registered": 100,
|
||||||
"first_read_only_runtime_cycle_source_implemented": 100,
|
"first_read_only_runtime_cycle_source_implemented": 100,
|
||||||
"persona_git_identity_commit_implemented": 100,
|
"persona_git_identity_commit_implemented": 100,
|
||||||
|
"interrupted_session_inspection_and_recovery_source_implemented": 100,
|
||||||
"general_purpose_persona_runtime_implemented": 0,
|
"general_purpose_persona_runtime_implemented": 0,
|
||||||
"human_live_projection_implemented": 0,
|
"human_live_projection_implemented": 0,
|
||||||
"hololake_integrated": 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.architecture_registered, 100);
|
||||||
assert.equal(channel.truth.first_read_only_runtime_cycle_source_implemented, 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.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.general_purpose_persona_runtime_implemented, 0);
|
||||||
assert.equal(channel.truth.human_live_projection_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_implemented, true);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
architecture.persona_native_code_channel.runtime_scope,
|
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.hololake_integrated, 0);
|
||||||
assert.equal(channel.truth.artifact_built, 0);
|
assert.equal(channel.truth.artifact_built, 0);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue