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:
parent
c3e7dbff10
commit
33b3b67ce7
10 changed files with 289 additions and 3 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue