feat: query verified PNCC runtime history

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:51:02 +08:00
commit 33b3b67ce7
10 changed files with 289 additions and 3 deletions

View file

@ -525,6 +525,7 @@ macro_rules! app_invoke_handler {
guanghu_living_system::guanghu_living_system_plan,
persona_code_channel::prepare_persona_code_channel_wake,
persona_code_channel::inspect_persona_code_channel_manifest,
persona_code_channel::query_persona_code_channel_runtime,
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,

View file

@ -13,6 +13,7 @@ const MAX_FACT_QUESTION_BYTES: usize = 8_000;
const MAX_FACT_SOURCE_BYTES: usize = 128_000;
const MAX_FACT_CONTEXT_BYTES: usize = 512_000;
const MAX_FACT_RESPONSE_BYTES: usize = 128_000;
const MAX_SESSION_QUERY_LIMIT: usize = 100;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
@ -169,6 +170,44 @@ pub struct PersonaManifestInspectionReceipt {
pub organ_contracts: Vec<PersonaOrganContract>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaRuntimeQueryInput {
pub expected_persona_id: String,
pub expected_repository_path: String,
pub limit: Option<usize>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaRuntimeSessionProjection {
pub session_id: String,
pub state: String,
pub git_head: String,
pub node_id: String,
pub model_instance_id: String,
pub active_organ: Option<String>,
pub event_count: usize,
pub first_event_at: String,
pub last_event_at: String,
pub last_event: String,
pub event_chain_head: String,
pub attribution: PersonaAttribution,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaRuntimeQueryReceipt {
pub schema: &'static str,
pub persona_id: String,
pub repository_path: String,
pub observed_git_head: String,
pub repository_clean: bool,
pub matching_session_count: usize,
pub returned_session_count: usize,
pub sessions: Vec<PersonaRuntimeSessionProjection>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaFactTaskInput {
@ -1439,6 +1478,99 @@ fn inspect_session_at(
})
}
fn query_runtime_sessions_at(
runtime_root: &Path,
input: PersonaRuntimeQueryInput,
) -> Result<PersonaRuntimeQueryReceipt, String> {
let expected_persona_id = validated_id("EXPECTED_PERSONA_ID", &input.expected_persona_id)?;
let limit = input.limit.unwrap_or(25);
if limit == 0 || limit > MAX_SESSION_QUERY_LIMIT {
return Err("PERSONA_SESSION_QUERY_LIMIT_INVALID".into());
}
let expected_repository = Path::new(&input.expected_repository_path)
.canonicalize()
.map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?;
let (repository, observed_head) = exact_repository(&expected_repository)?;
let repository_clean = repository_is_clean(&repository)?;
let sessions_root = runtime_root.join("sessions");
let entries = match fs::read_dir(&sessions_root) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(PersonaRuntimeQueryReceipt {
schema: "hololake.pncc-runtime-query/v1",
persona_id: expected_persona_id,
repository_path: repository.to_string_lossy().into_owned(),
observed_git_head: observed_head,
repository_clean,
matching_session_count: 0,
returned_session_count: 0,
sessions: Vec::new(),
});
}
Err(error) => return Err(format!("PERSONA_SESSIONS_DIRECTORY_READ_FAILED: {error}")),
};
let mut projections = Vec::new();
for entry in entries {
let entry = entry.map_err(|error| format!("PERSONA_SESSION_ENTRY_FAILED: {error}"))?;
if !entry
.file_type()
.map_err(|error| format!("PERSONA_SESSION_ENTRY_TYPE_FAILED: {error}"))?
.is_dir()
{
continue;
}
let session_id = match entry.file_name().to_str() {
Some(value) => validated_id("SESSION_ID", value)?,
None => return Err("PERSONA_SESSION_ID_NOT_UTF8".into()),
};
let record = load_session_record(runtime_root, &session_id)?;
if record.persona_id != expected_persona_id
|| Path::new(&record.repository_path) != repository
{
continue;
}
let events = verify_event_journal(runtime_root, &record)?;
let first = events.first().expect("verified non-empty event chain");
let last = events.last().expect("verified non-empty event chain");
projections.push(PersonaRuntimeSessionProjection {
session_id: record.session_id,
state: record.state.clone(),
git_head: record.git_head,
node_id: record.node_id,
model_instance_id: record.model_instance_id,
active_organ: if record.state.starts_with("DORMANT") {
None
} else {
Some(record.active_organ)
},
event_count: events.len(),
first_event_at: first.timestamp.clone(),
last_event_at: last.timestamp.clone(),
last_event: last.kind.clone(),
event_chain_head: last.event_hash.clone(),
attribution: record.attribution,
});
}
projections.sort_by(|left, right| {
right
.last_event_at
.cmp(&left.last_event_at)
.then_with(|| right.session_id.cmp(&left.session_id))
});
let matching_session_count = projections.len();
projections.truncate(limit);
Ok(PersonaRuntimeQueryReceipt {
schema: "hololake.pncc-runtime-query/v1",
persona_id: expected_persona_id,
repository_path: repository.to_string_lossy().into_owned(),
observed_git_head: observed_head,
repository_clean,
matching_session_count,
returned_session_count: projections.len(),
sessions: projections,
})
}
fn recover_session_at(
runtime_root: &Path,
input: PersonaSessionControlInput,
@ -1802,6 +1934,14 @@ pub fn inspect_persona_code_channel_manifest(
inspect_manifest_at(input)
}
#[tauri::command]
pub fn query_persona_code_channel_runtime(
input: PersonaRuntimeQueryInput,
) -> Result<PersonaRuntimeQueryReceipt, String> {
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
query_runtime_sessions_at(&runtime_root, input)
}
#[tauri::command]
pub async fn run_persona_code_channel_fact_task(
input: PersonaFactTaskInput,
@ -2007,6 +2147,14 @@ mod tests {
}
}
fn runtime_query(repo: &Path, limit: Option<usize>) -> PersonaRuntimeQueryInput {
PersonaRuntimeQueryInput {
expected_persona_id: "ICE-P-ZY001".into(),
expected_repository_path: repo.to_string_lossy().into_owned(),
limit,
}
}
#[test]
fn prepares_exact_persona_git_wake_with_hash_chained_events() {
let repo = persona_repo();
@ -2471,4 +2619,79 @@ mod tests {
.unwrap_err()
.contains("ORGAN_NOT_ACTIVATABLE"));
}
#[test]
fn queries_bounded_verified_runtime_projections_without_a_second_store() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
"PNCC-SESSION-HISTORY-001",
"2026-08-11T00:00:00.000Z",
)
.unwrap();
recover_session_at(
runtime.path(),
session_control(repo.path(), "PNCC-SESSION-HISTORY-001"),
"2026-08-11T00:01:00.000Z",
)
.unwrap();
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
"PNCC-SESSION-HISTORY-002",
"2026-08-11T00:02:00.000Z",
)
.unwrap();
let receipt =
query_runtime_sessions_at(runtime.path(), runtime_query(repo.path(), Some(1))).unwrap();
assert_eq!(receipt.matching_session_count, 2);
assert_eq!(receipt.returned_session_count, 1);
assert_eq!(receipt.sessions[0].session_id, "PNCC-SESSION-HISTORY-002");
assert_eq!(receipt.sessions[0].last_event, "ORGAN_ACTIVE");
assert_eq!(
receipt.sessions[0].active_organ.as_deref(),
Some("fact-sense.repository")
);
assert_eq!(receipt.sessions[0].event_count, 3);
assert!(!receipt.sessions[0].event_chain_head.is_empty());
assert_eq!(
receipt.sessions[0].attribution.persona_cognitive_author,
"ICE-P-ZY001"
);
}
#[test]
fn runtime_query_rejects_invalid_limits_and_corrupted_event_chains() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
assert!(
query_runtime_sessions_at(runtime.path(), runtime_query(repo.path(), Some(0)))
.unwrap_err()
.contains("LIMIT_INVALID")
);
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
"PNCC-SESSION-CORRUPTED",
"2026-08-11T00:00:00.000Z",
)
.unwrap();
let journal = runtime
.path()
.join("sessions/PNCC-SESSION-CORRUPTED/events.jsonl");
let content = fs::read_to_string(&journal).unwrap();
fs::write(
&journal,
content.replacen("\"eventHash\":\"", "\"eventHash\":\"00", 1),
)
.unwrap();
assert!(
query_runtime_sessions_at(runtime.path(), runtime_query(repo.path(), Some(10)))
.unwrap_err()
.contains("EVENT_HASH_INVALID")
);
}
}