82 lines
3.1 KiB
Rust
82 lines
3.1 KiB
Rust
//! `LogEntry::Extension { domain: "memory.extract", payload }` の payload 形式と
|
||
//! restore 時の fold ヘルパー。memory crate がドメインを所有するので、
|
||
//! session-store / Pod は payload 構造を知らない。
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use super::EXTRACT_DOMAIN;
|
||
|
||
/// Phase 1 完了境界の永続化 payload。session log の Extension entry
|
||
/// として 1 回ずつ書かれ、最新の 1 件が現行 pointer として有効になる。
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||
pub struct ExtractPointerPayload {
|
||
/// 直近 extract が処理した最後の session-store HashedEntry の index。
|
||
/// 次回の `source.range.start` はこの値 + 1。
|
||
pub processed_through_entry: usize,
|
||
/// 直近 extract 時点の `history.len()`。次回入力は
|
||
/// `history[processed_through_history_len..]` を切り出す。
|
||
pub processed_through_history_len: usize,
|
||
/// 書き出した staging file の UUIDv7 文字列。LLM が空 payload を返した
|
||
/// 場合は staging file を作らず空文字列で記録する(pointer は前進する)。
|
||
pub staging_id: String,
|
||
}
|
||
|
||
/// `RestoredState.extensions` から最新の Phase 1 pointer を取り出す。
|
||
/// 未抽出セッションでは `None`。
|
||
pub fn fold_pointer(extensions: &[(String, serde_json::Value)]) -> Option<ExtractPointerPayload> {
|
||
extensions
|
||
.iter()
|
||
.rev()
|
||
.find(|(domain, _)| domain == EXTRACT_DOMAIN)
|
||
.and_then(|(_, value)| serde_json::from_value(value.clone()).ok())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn fold_returns_latest_when_multiple_present() {
|
||
let exts = vec![
|
||
(
|
||
EXTRACT_DOMAIN.to_string(),
|
||
serde_json::json!({
|
||
"processed_through_entry": 5,
|
||
"processed_through_history_len": 4,
|
||
"staging_id": "old"
|
||
}),
|
||
),
|
||
("other.domain".to_string(), serde_json::json!({ "x": 1 })),
|
||
(
|
||
EXTRACT_DOMAIN.to_string(),
|
||
serde_json::json!({
|
||
"processed_through_entry": 11,
|
||
"processed_through_history_len": 8,
|
||
"staging_id": "new"
|
||
}),
|
||
),
|
||
];
|
||
let p = fold_pointer(&exts).unwrap();
|
||
assert_eq!(p.processed_through_entry, 11);
|
||
assert_eq!(p.processed_through_history_len, 8);
|
||
assert_eq!(p.staging_id, "new");
|
||
}
|
||
|
||
#[test]
|
||
fn fold_returns_none_when_absent() {
|
||
let exts = vec![("other.domain".to_string(), serde_json::json!({ "x": 1 }))];
|
||
assert!(fold_pointer(&exts).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn fold_skips_malformed_entries() {
|
||
let exts = vec![(
|
||
EXTRACT_DOMAIN.to_string(),
|
||
serde_json::json!({ "wrong_shape": true }),
|
||
)];
|
||
// 現状は最新を取り出して JSON 不一致なら None。古いものに fallback
|
||
// しないのは、壊れた最新を黙って無視すると意図しない再抽出を招くため。
|
||
assert!(fold_pointer(&exts).is_none());
|
||
}
|
||
}
|