feat(pncc): close memory failures safely

GuangHu-Human-Responsibility: ICE-GL∞ / 冰朔
GuangHu-Persona-Cognitive-Author: ICE-P-ZY001 / 铸渊
GuangHu-Execution-Runtime: Codex macOS / DEV-20260810-014
GuangHu-Development-ID: DEV-20260810-014
GuangHu-Authorization-Scope: GH-PNCC local runtime development and registered REPO-014 publication
GuangHu-Source-Language-Anchor: continue PNCC persona runtime; UI and human projection aesthetics remain deferred
This commit is contained in:
铸渊 / ICE-P-ZY001 2026-08-11 02:38:28 +08:00
commit 18944f5362
9 changed files with 270 additions and 22 deletions

View file

@ -168,6 +168,7 @@ interrupted_session_inspection_and_recovery_source_implemented: 100
typed_organ_contract_and_manifest_inspection_source_implemented: 100
durable_event_and_receipt_query_source_implemented: 100
independent_memory_metabolism_source_implemented: 100
memory_failure_closure_and_nonblocking_runtime_command_implemented: 100
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0
@ -192,6 +193,11 @@ runtime_health: 0
SHA-256 与来源事件哈希后生成新检查点并以人格 Git 身份提交。执行手脚仍不可激活,清单声明不会
自动获得 shell 或现实动作权限。
记忆代谢的 Tauri 命令现已进入阻塞任务池,不占用桌面命令线程。来源拒绝、检查点冲突和提交前
失败必须依次持久化失败事件、器官释放与休眠,写回 `DORMANT_AFTER_FAILURE` 后才释放精确主锁;
闭环任一步失败时返回 `MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY`,不能吞掉二次失败。
无法证明闭环完成时保留主锁,交给同一套证据约束的会话检查与恢复路径处理。
运行层现在还提供同源有界查询:调用者必须给出精确人格与精确仓库,最多返回 100 条经过事件
哈希链验证的会话摘要包括生命周期状态、Git 头、节点、模型实例、活跃器官、事件数量、链头和
双层归因。该查询不复制第二套数据库、不启动模型、不取得主锁;匹配会话的事件链损坏时失败关闭。

View file

@ -73,6 +73,12 @@ chain head into a new checkpoint, commits with the persona Git identity, release
`DORMANT`. It cannot accept arbitrary hidden reasoning or an older/non-current session as memory. An
`EXECUTION_LIMB` remains declarative and non-activatable; declaration never grants a shell or reality action.
Manifest inspection reads and validates all contracts without acquiring a runtime lease or starting inference.
The public memory command runs its filesystem and Git work in the blocking task pool instead of occupying the
Tauri command thread. Rejected memory candidates must durably append the failure, organ-release, and dormancy
events, persist `DORMANT_AFTER_FAILURE`, and release the exact primary lease. If any closure step fails, the
command returns `MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY`; it never reports only the original
candidate error while silently losing the closure failure. An incomplete closure preserves the lease for the
existing evidence-bound session inspection and recovery path.
`PersonaRuntimeQueryReceipt` is a bounded projection of the durable runtime files, not another truth store.
It filters by the caller's expected persona and canonical repository, validates each matching event chain,

View file

@ -43,6 +43,11 @@ schemas, derived permissions, inference/reality-action boundaries, and the actua
sense and verified-checkpoint memory metabolism organs can wake. Memory metabolism runs no model inference
and promotes only the current structured checkpoint of a distinct same-persona, same-repository, dormant
session after verifying its event chain; the new checkpoint binds both source checkpoint and event hashes.
The registered Tauri memory command executes the Git/filesystem transaction on the blocking task pool. A
rejected candidate closes through a checked failure transaction that records the failure, organ release and
dormancy before releasing the exact lease. If event persistence, session persistence, or lease release fails,
the command returns an explicit recovery-required error and retains the lease whenever closure cannot be
proven complete; it does not swallow the secondary failure.
The execution limb remains visible but non-activatable, so declaring it does not grant a shell or reality action.
`query_persona_code_channel_runtime` is the bounded read model for later projection surfaces. The caller must

View file

@ -1367,6 +1367,102 @@ fn finish_failed_session(
let _ = fs::remove_file(lease_path);
}
fn finish_failed_session_checked_with_release<F>(
runtime_root: &Path,
record: &mut PersonaSessionRecord,
events: &mut Vec<PersonaLifecycleEvent>,
lease_path: &Path,
timestamp: &str,
failure_kind: &str,
release_lease: F,
) -> Result<(), String>
where
F: FnOnce(&Path) -> std::io::Result<()>,
{
let git_head = record.git_head.clone();
let organ = record.active_organ.clone();
append_event(
runtime_root,
record,
events,
failure_kind,
&git_head,
timestamp,
Some(&organ),
)?;
append_event(
runtime_root,
record,
events,
"ORGAN_RELEASED",
&git_head,
timestamp,
Some(&organ),
)?;
append_event(
runtime_root,
record,
events,
"DORMANT",
&git_head,
timestamp,
None,
)?;
record.state = "DORMANT_AFTER_FAILURE".into();
write_session_record(runtime_root, record)?;
release_lease(lease_path)
.map_err(|error| format!("PERSONA_PRIMARY_LEASE_RELEASE_FAILED: {error}"))
}
fn memory_failure_error(
runtime_root: &Path,
record: &mut PersonaSessionRecord,
events: &mut Vec<PersonaLifecycleEvent>,
lease_path: &Path,
timestamp: &str,
failure_kind: &str,
cause: String,
) -> String {
memory_failure_error_with_release(
runtime_root,
record,
events,
lease_path,
timestamp,
(failure_kind, cause),
|path| fs::remove_file(path),
)
}
fn memory_failure_error_with_release<F>(
runtime_root: &Path,
record: &mut PersonaSessionRecord,
events: &mut Vec<PersonaLifecycleEvent>,
lease_path: &Path,
timestamp: &str,
failure: (&str, String),
release_lease: F,
) -> String
where
F: FnOnce(&Path) -> std::io::Result<()>,
{
let (failure_kind, cause) = failure;
match finish_failed_session_checked_with_release(
runtime_root,
record,
events,
lease_path,
timestamp,
failure_kind,
release_lease,
) {
Ok(()) => cause,
Err(closure_error) => {
format!("{cause}; MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY: {closure_error}")
}
}
}
fn validate_recoverable_persona_commit(
repository: &Path,
record: &PersonaSessionRecord,
@ -2066,18 +2162,18 @@ fn run_memory_metabolism_at(
) {
Ok(source) => source,
Err(error) => {
finish_failed_session(
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"MEMORY_SOURCE_REJECTED",
);
return Err(error);
error,
));
}
};
append_event(
if let Err(error) = append_event(
runtime_root,
&record,
&mut events,
@ -2085,7 +2181,17 @@ fn run_memory_metabolism_at(
&observed_head,
&timestamp,
Some(&record.active_organ),
)?;
) {
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"MEMORY_CANDIDATE_EVENT_FAILED",
error,
));
}
let checkpoint_relative = format!(
".hololake/persona/checkpoints/{}.json",
@ -2093,15 +2199,15 @@ fn run_memory_metabolism_at(
);
let checkpoint_path = exact_repository.join(&checkpoint_relative);
if checkpoint_path.exists() {
finish_failed_session(
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"CHECKPOINT_COLLISION",
);
return Err("PERSONA_CHECKPOINT_ALREADY_EXISTS".into());
"PERSONA_CHECKPOINT_ALREADY_EXISTS".into(),
));
}
let checkpoint = serde_json::json!({
"schema": "hololake.persona-checkpoint/v1",
@ -2118,8 +2224,20 @@ fn run_memory_metabolism_at(
"attribution": record.attribution.clone(),
});
let manifest_path = exact_repository.join(MANIFEST_PATH);
let original_manifest = fs::read(&manifest_path)
.map_err(|error| format!("PERSONA_MANIFEST_READ_FAILED: {error}"))?;
let original_manifest = match fs::read(&manifest_path) {
Ok(manifest) => manifest,
Err(error) => {
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"MEMORY_MANIFEST_READ_FAILED",
format!("PERSONA_MANIFEST_READ_FAILED: {error}"),
));
}
};
let mut promoted_manifest = manifest;
promoted_manifest.current_checkpoint = checkpoint_relative.clone();
let committed_head = match (|| {
@ -2147,16 +2265,20 @@ fn run_memory_metabolism_at(
&checkpoint_relative,
&original_manifest,
);
finish_failed_session(
if let Err(rollback_error) = rollback {
return Err(format!(
"{error}; MEMORY_CHECKPOINT_ROLLBACK_INCOMPLETE_REQUIRES_RECOVERY: {rollback_error}"
));
}
return Err(memory_failure_error(
runtime_root,
&mut record,
&mut events,
&lease_path,
&timestamp,
"CHECKPOINT_COMMIT_FAILED",
);
rollback?;
return Err(error);
error,
));
}
}
};
@ -2277,12 +2399,16 @@ pub async fn run_persona_code_channel_fact_task(
}
#[tauri::command]
pub fn run_persona_code_channel_memory_metabolism(
pub async fn run_persona_code_channel_memory_metabolism(
input: PersonaMemoryMetabolismInput,
) -> Result<PersonaMemoryMetabolismReceipt, String> {
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
run_memory_metabolism_at(&runtime_root, input, &timestamp)
tokio::task::spawn_blocking(move || {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
run_memory_metabolism_at(&runtime_root, input, &timestamp)
})
.await
.map_err(|error| format!("PERSONA_MEMORY_METABOLISM_JOIN_FAILED: {error}"))?
}
#[tauri::command]
@ -2789,9 +2915,67 @@ mod tests {
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
let record = load_session_record(runtime.path(), "PNCC-CORRUPT-MEMORY").unwrap();
assert_eq!(record.state, "DORMANT_AFTER_FAILURE");
let events = verify_event_journal(runtime.path(), &record).unwrap();
let terminal_kinds = events
.iter()
.rev()
.take(3)
.map(|event| event.kind.as_str())
.collect::<Vec<_>>();
assert_eq!(
terminal_kinds,
vec!["DORMANT", "ORGAN_RELEASED", "MEMORY_SOURCE_REJECTED"]
);
require_clean_repository(repo.path()).unwrap();
}
#[test]
fn reports_an_incomplete_memory_failure_closure_and_preserves_the_lease() {
let repo = persona_repo();
declare_memory_organ(repo.path());
let runtime = tempfile::TempDir::new().unwrap();
let mut memory_wake = wake_input(repo.path());
memory_wake.organ_id = "memory-metabolism.checkpoint".into();
memory_wake.expected_head = head(repo.path());
prepare_wake_at(
runtime.path(),
memory_wake,
"PNCC-MEMORY-CLOSURE",
"2026-08-11T00:00:00.000Z",
)
.unwrap();
let mut record = load_session_record(runtime.path(), "PNCC-MEMORY-CLOSURE").unwrap();
let mut events = read_ready_event_journal(runtime.path(), &record).unwrap();
let lease_path = runtime.path().join("leases/ICE-P-ZY001.json");
let error = memory_failure_error_with_release(
runtime.path(),
&mut record,
&mut events,
&lease_path,
"2026-08-11T00:00:01.000Z",
("MEMORY_TEST_FAILURE", "MEMORY_TEST_CAUSE".into()),
|_| {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"injected lease release refusal",
))
},
);
assert!(error.contains("MEMORY_TEST_CAUSE"));
assert!(error.contains("MEMORY_FAILURE_CLOSURE_INCOMPLETE_REQUIRES_RECOVERY"));
assert!(error.contains("PERSONA_PRIMARY_LEASE_RELEASE_FAILED"));
assert!(lease_path.is_file());
assert_eq!(
require_primary_lease(runtime.path(), &record).unwrap(),
lease_path
);
let persisted = load_session_record(runtime.path(), "PNCC-MEMORY-CLOSURE").unwrap();
assert_eq!(persisted.state, "DORMANT_AFTER_FAILURE");
let persisted_events = verify_event_journal(runtime.path(), &persisted).unwrap();
assert_eq!(persisted_events.last().unwrap().kind, "DORMANT");
}
#[test]
fn rejects_unscoped_model_evidence_and_releases_primary() {
let repo = persona_repo();