fix: read retained sessions from canonical aggregate

This commit is contained in:
2026-09-29 05:43:01 +09:00
parent f33e737d5c
commit ebc98e3de5
+221 -6
View File
@@ -1016,12 +1016,12 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
worker_ref: &WorkerRef, worker_ref: &WorkerRef,
) -> Result<session_store::RetainedSessionSnapshot, session_store::RetainedSnapshotReadError> ) -> Result<session_store::RetainedSessionSnapshot, session_store::RetainedSnapshotReadError>
{ {
let Some(root) = self.worker_aggregate_root.as_ref() else { let aggregate_dir = self
return Err(session_store::RetainedSnapshotReadError::RetentionMissing); .worker_aggregate_dir(worker_ref)
}; .map_err(|_| session_store::RetainedSnapshotReadError::RetentionMissing)?;
let worker_name = Self::runtime_worker_name_for_ref(worker_ref); let worker_name = Self::runtime_worker_name_for_ref(worker_ref);
session_store::read_retained_session_snapshot( session_store::read_retained_session_snapshot(
&root.join(&worker_name), &aggregate_dir,
&worker_name, &worker_name,
session_store::DEFAULT_RETAINED_SNAPSHOT_MAX_BYTES, session_store::DEFAULT_RETAINED_SNAPSHOT_MAX_BYTES,
) )
@@ -3135,11 +3135,15 @@ mod tests {
use crate::working_directory::RuntimeGitMaterializer; use crate::working_directory::RuntimeGitMaterializer;
use agen::Engine; use agen::Engine;
use agen::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent}; use agen::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent};
use agen::llm_client::{ClientError, LlmClient, Request}; use agen::llm_client::{ClientError, LlmClient, Request, RequestConfig};
use async_trait::async_trait; use async_trait::async_trait;
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use manifest::{Scope, WorkerManifest}; use manifest::{Scope, WorkerManifest};
use session_store::{LogEntry, WorkerMetadataStore}; use session_store::{
LogEntry, LoggedContentPart, LoggedHistoryEntry, LoggedItem, LoggedRole,
LoggedSessionHistoryEntryId, LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin,
Store, WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore,
};
#[test] #[test]
fn production_source_has_no_repository_derived_runtime_store() { fn production_source_has_no_repository_derived_runtime_store() {
@@ -3155,6 +3159,217 @@ mod tests {
} }
} }
fn persisted_files(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
fn collect(root: &Path, current: &Path, files: &mut BTreeMap<PathBuf, Vec<u8>>) {
if !current.exists() {
return;
}
for entry in fs::read_dir(current).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
collect(root, &path, files);
} else {
files.insert(
path.strip_prefix(root).unwrap().to_path_buf(),
fs::read(path).unwrap(),
);
}
}
}
let mut files = BTreeMap::new();
collect(root, root, &mut files);
files
}
fn write_retained_profile_worker(
runtime_store_dir: &Path,
worker_ref: &WorkerRef,
) -> (session_store::SessionId, session_store::SegmentId, String) {
let worker_name = ProfileRuntimeWorkerFactory::runtime_worker_name_for_ref(worker_ref);
let aggregate_dir = runtime_store_dir
.join("workers")
.join(worker_ref.worker_id.to_string());
let aggregate = WorkerAggregateStore::new(&aggregate_dir, &worker_name).unwrap();
let session_id = session_store::new_session_id();
let segment_id = session_store::new_segment_id();
aggregate
.write(&WorkerMetadata::new(
&worker_name,
Some(WorkerActiveSegmentRef::active_segment(
session_id, segment_id,
)),
))
.unwrap();
let entry_id = LoggedSessionHistoryEntryId::new();
let expected_entry_id = entry_id.0.clone();
let log = [LogEntry::AnnotatedSegmentStart {
ts: 17,
session_id,
system_prompt: None,
config: RequestConfig::default(),
history: vec![LoggedHistoryEntry {
item: LoggedItem::Message {
role: LoggedRole::Assistant,
content: vec![LoggedContentPart::Text {
text: "retained response".to_string(),
}],
},
metadata: LoggedSessionHistoryMetadata {
entry_id,
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
},
}],
forked_from: None,
compacted_from: None,
}];
WorkerSessionStore::new(aggregate_dir.join("session"))
.unwrap()
.create_segment(session_id, segment_id, &log)
.unwrap();
(session_id, segment_id, expected_entry_id)
}
#[test]
fn profile_factory_reads_retained_session_from_canonical_worker_aggregate() {
let root = tempfile::tempdir().unwrap();
let runtime_store_dir = root.path().join("runtime");
let worker_ref = WorkerRef::new(WorkerId::now_v7());
let (session_id, segment_id, entry_id) =
write_retained_profile_worker(&runtime_store_dir, &worker_ref);
let canonical_dir = runtime_store_dir
.join("workers")
.join(worker_ref.worker_id.to_string());
let legacy_prefixed_dir = runtime_store_dir.join("workers").join(
ProfileRuntimeWorkerFactory::runtime_worker_name_for_ref(&worker_ref),
);
assert!(!legacy_prefixed_dir.exists());
let files_before = persisted_files(&runtime_store_dir);
let backend = WorkerRuntimeExecutionBackend::new(
ProfileRuntimeWorkerFactory::new(root.path().join("profiles"))
.with_runtime_store_dir(&runtime_store_dir),
)
.unwrap();
let retained = backend.worker_session(WorkerSessionObservationRequest {
worker_ref: worker_ref.clone(),
});
let runtime_api::WorkerSessionAvailability::RetainedSnapshot { identity, snapshot } =
retained
else {
panic!("canonical aggregate must produce a retained snapshot");
};
assert_eq!(identity.session_id, session_id.to_string());
assert_eq!(identity.segment_id, segment_id.to_string());
assert_eq!(identity.entry_count, 1);
assert_eq!(snapshot.entries.len(), 1);
assert_eq!(snapshot.entries[0].entry_id, entry_id);
assert_eq!(
snapshot.entries[0].data,
protocol::SessionSnapshotEntryData::Message {
role: protocol::SessionMessageRole::Assistant,
content: vec![protocol::SessionContentPart::Text {
text: "retained response".to_string(),
}],
}
);
assert!(canonical_dir.is_dir());
assert!(!legacy_prefixed_dir.exists());
assert_eq!(persisted_files(&runtime_store_dir), files_before);
assert!(backend.workers.lock().unwrap().is_empty());
assert!(
backend
.factory
.observation_hub
.workers
.lock()
.unwrap()
.is_empty()
);
}
#[test]
fn profile_factory_retained_read_keeps_missing_aggregate_typed_and_absent() {
let root = tempfile::tempdir().unwrap();
let runtime_store_dir = root.path().join("runtime");
let worker_ref = WorkerRef::new(WorkerId::now_v7());
let unconfigured_backend = WorkerRuntimeExecutionBackend::new(
ProfileRuntimeWorkerFactory::new(root.path().join("unconfigured-profiles")),
)
.unwrap();
assert!(matches!(
unconfigured_backend.worker_session(WorkerSessionObservationRequest {
worker_ref: worker_ref.clone(),
}),
runtime_api::WorkerSessionAvailability::Unavailable {
reason: runtime_api::WorkerSessionUnavailableReason::RetentionMissing,
..
}
));
assert!(!root.path().join("unconfigured-profiles").exists());
assert!(unconfigured_backend.workers.lock().unwrap().is_empty());
let canonical_dir = runtime_store_dir
.join("workers")
.join(worker_ref.worker_id.to_string());
let backend = WorkerRuntimeExecutionBackend::new(
ProfileRuntimeWorkerFactory::new(root.path().join("profiles"))
.with_runtime_store_dir(&runtime_store_dir),
)
.unwrap();
let unavailable = backend.worker_session(WorkerSessionObservationRequest {
worker_ref: worker_ref.clone(),
});
assert!(matches!(
unavailable,
runtime_api::WorkerSessionAvailability::Unavailable {
reason: runtime_api::WorkerSessionUnavailableReason::RetentionMissing,
..
}
));
assert!(!canonical_dir.exists());
assert!(!runtime_store_dir.exists());
assert!(backend.workers.lock().unwrap().is_empty());
}
#[test]
fn profile_factory_retained_read_validates_metadata_worker_identity() {
let root = tempfile::tempdir().unwrap();
let runtime_store_dir = root.path().join("runtime");
let worker_ref = WorkerRef::new(WorkerId::now_v7());
let aggregate_dir = runtime_store_dir
.join("workers")
.join(worker_ref.worker_id.to_string());
fs::create_dir_all(&aggregate_dir).unwrap();
fs::write(
aggregate_dir.join("metadata.json"),
serde_json::to_vec(&WorkerMetadata::new("different-worker", None)).unwrap(),
)
.unwrap();
let files_before = persisted_files(&runtime_store_dir);
let backend = WorkerRuntimeExecutionBackend::new(
ProfileRuntimeWorkerFactory::new(root.path().join("profiles"))
.with_runtime_store_dir(&runtime_store_dir),
)
.unwrap();
let unavailable = backend.worker_session(WorkerSessionObservationRequest { worker_ref });
assert!(matches!(
unavailable,
runtime_api::WorkerSessionAvailability::Unavailable {
reason: runtime_api::WorkerSessionUnavailableReason::CorruptLog,
ref message,
} if message == "retained session log is corrupt"
));
assert_eq!(persisted_files(&runtime_store_dir), files_before);
assert!(backend.workers.lock().unwrap().is_empty());
}
#[test] #[test]
fn restored_router_uses_workspace_proxy_for_logical_only_attachment() { fn restored_router_uses_workspace_proxy_for_logical_only_attachment() {
let root = tempfile::tempdir().unwrap(); let root = tempfile::tempdir().unwrap();