chore: merge develop into hare/develop
This commit is contained in:
@@ -298,6 +298,10 @@ pub struct CreateWorkerRequest {
|
||||
/// Backend-authored immutable Workspace Memory settings snapshot.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub memory_settings: Option<manifest::WorkspaceMemorySettingsSnapshot>,
|
||||
/// Backend-attested subject or consolidation attachment. A reusable Profile
|
||||
/// may enable subjektiv policy, but only this host-owned bit activates it.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub subjektiv_attached: bool,
|
||||
}
|
||||
|
||||
/// Last persisted Worker lifecycle status.
|
||||
|
||||
@@ -1334,7 +1334,6 @@ async fn runtime_protocol_ws_session(
|
||||
request_id,
|
||||
subscription_id: subscription_id.clone(),
|
||||
selector: selector.clone(),
|
||||
snapshot_revision: subscription.snapshot_revision(),
|
||||
snapshot: subscription.snapshot().clone(),
|
||||
},
|
||||
));
|
||||
@@ -1354,7 +1353,6 @@ async fn runtime_protocol_ws_session(
|
||||
SubscriptionFramePayload::Event(
|
||||
SubscriptionEvent::Event {
|
||||
subscription_id: event_subscription_id.clone(),
|
||||
subject_revision: update.subject_revision,
|
||||
payload: update.payload,
|
||||
},
|
||||
),
|
||||
@@ -2314,6 +2312,9 @@ fn required_runtime_permission(method: &Method, path: &str) -> Option<&'static s
|
||||
if path.starts_with("/v1/workers/") && *method == Method::DELETE {
|
||||
return Some("workers:delete");
|
||||
}
|
||||
if path.starts_with("/v1/session-public/") && *method == Method::POST {
|
||||
return Some("workers:read");
|
||||
}
|
||||
if path.starts_with("/v1/workers/") && *method == Method::GET {
|
||||
return Some("workers:read");
|
||||
}
|
||||
@@ -3101,6 +3102,7 @@ mod tests {
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
}),
|
||||
subjektiv_attached: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3994,6 +3996,8 @@ mod ws_tests {
|
||||
tools: Vec::new(),
|
||||
context_window: 0,
|
||||
context_tokens: 0,
|
||||
reasoning: None,
|
||||
context_usage: None,
|
||||
},
|
||||
state: protocol::WorkerStateSnapshot::initial(),
|
||||
in_flight: protocol::InFlightSnapshot {
|
||||
@@ -4078,6 +4082,7 @@ mod ws_tests {
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
}),
|
||||
subjektiv_attached: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -289,6 +289,80 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
|
||||
.map_err(api_error)
|
||||
}
|
||||
|
||||
async fn session_public_search(
|
||||
&self,
|
||||
request: runtime_api::SessionPublicSearchRequest,
|
||||
) -> Result<runtime_api::SessionPublicSearchAvailability, runtime_api::RuntimeApiError> {
|
||||
let scope = auth_workspace_scope(&self.state, auth_extension().as_ref())
|
||||
.map_err(api_error)?
|
||||
.ok_or_else(|| {
|
||||
runtime_api::RuntimeApiError::new(
|
||||
StatusCode::FORBIDDEN.as_u16(),
|
||||
"runtime_session_public_scope_required",
|
||||
"Session public search requires a Workspace-scoped capability",
|
||||
)
|
||||
})?;
|
||||
if scope.workspace_id != request.workspace_id {
|
||||
return Err(runtime_api::RuntimeApiError::new(
|
||||
StatusCode::FORBIDDEN.as_u16(),
|
||||
"runtime_session_public_workspace_scope_mismatch",
|
||||
"Session public search Workspace scope does not match the authenticated capability",
|
||||
));
|
||||
}
|
||||
#[cfg(feature = "fs-store")]
|
||||
{
|
||||
self.state
|
||||
.runtime
|
||||
.session_public_search_scoped(&scope, &request)
|
||||
.map_err(RuntimeHttpRestError::runtime)
|
||||
.map_err(api_error)
|
||||
}
|
||||
#[cfg(not(feature = "fs-store"))]
|
||||
{
|
||||
Ok(runtime_api::SessionPublicSearchAvailability::Unavailable {
|
||||
reason: runtime_api::SessionPublicUnavailableReason::StorageUnavailable,
|
||||
message: "Session public storage is unavailable".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn session_public_read(
|
||||
&self,
|
||||
request: runtime_api::SessionPublicReadRequest,
|
||||
) -> Result<runtime_api::SessionPublicReadAvailability, runtime_api::RuntimeApiError> {
|
||||
let scope = auth_workspace_scope(&self.state, auth_extension().as_ref())
|
||||
.map_err(api_error)?
|
||||
.ok_or_else(|| {
|
||||
runtime_api::RuntimeApiError::new(
|
||||
StatusCode::FORBIDDEN.as_u16(),
|
||||
"runtime_session_public_scope_required",
|
||||
"Session public read requires a Workspace-scoped capability",
|
||||
)
|
||||
})?;
|
||||
if scope.workspace_id != request.workspace_id {
|
||||
return Err(runtime_api::RuntimeApiError::new(
|
||||
StatusCode::FORBIDDEN.as_u16(),
|
||||
"runtime_session_public_workspace_scope_mismatch",
|
||||
"Session public read Workspace scope does not match the authenticated capability",
|
||||
));
|
||||
}
|
||||
#[cfg(feature = "fs-store")]
|
||||
{
|
||||
self.state
|
||||
.runtime
|
||||
.session_public_read_scoped(&scope, &request)
|
||||
.map_err(RuntimeHttpRestError::runtime)
|
||||
.map_err(api_error)
|
||||
}
|
||||
#[cfg(not(feature = "fs-store"))]
|
||||
{
|
||||
Ok(runtime_api::SessionPublicReadAvailability::Unavailable {
|
||||
reason: runtime_api::SessionPublicUnavailableReason::StorageUnavailable,
|
||||
message: "Session public storage is unavailable".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_worker(
|
||||
&self,
|
||||
value: runtime_api::CreateWorkerRequest,
|
||||
|
||||
@@ -25,6 +25,8 @@ pub mod resource;
|
||||
#[cfg(feature = "fs-store")]
|
||||
pub mod retention;
|
||||
mod runtime;
|
||||
#[cfg(feature = "fs-store")]
|
||||
mod session_public;
|
||||
pub mod ssh_host_key_probe;
|
||||
pub mod worker_backend;
|
||||
pub mod worker_source;
|
||||
@@ -36,5 +38,8 @@ pub mod workspace_request;
|
||||
#[cfg(feature = "fs-store")]
|
||||
pub use fs_store::{FsRuntimeStore, FsRuntimeStoreOptions};
|
||||
pub use management::RuntimeOptions;
|
||||
pub use runtime::{Runtime, RuntimeWorkspaceScope};
|
||||
pub use runtime::{
|
||||
Runtime, RuntimeEventSelectorSubscription, RuntimeSubscriptionRecvError,
|
||||
RuntimeSubscriptionUpdate, RuntimeWorkspaceScope,
|
||||
};
|
||||
pub use session_store::UploadedFileUploadContext;
|
||||
|
||||
@@ -356,6 +356,26 @@ impl FsWorkerRetentionProvider {
|
||||
.join(archive_id))
|
||||
}
|
||||
|
||||
pub(crate) fn session_archive(
|
||||
&self,
|
||||
archive_id: &str,
|
||||
) -> Result<(WorkerSessionArchiveManifest, PathBuf), RuntimeError> {
|
||||
let archive_dir = self.archive_dir(archive_id)?;
|
||||
let manifest: WorkerSessionArchiveManifest = read_json(
|
||||
&archive_dir.join("manifest.json"),
|
||||
"read Worker Session archive",
|
||||
)?;
|
||||
if manifest.schema_version != ARCHIVE_SCHEMA_VERSION || manifest.archive_id != archive_id {
|
||||
return Err(RuntimeError::StoreCorrupt {
|
||||
operation: "read Worker Session archive",
|
||||
path: archive_dir.join("manifest.json"),
|
||||
message: "archive manifest identity mismatch".to_string(),
|
||||
});
|
||||
}
|
||||
let manifest = validate_existing_archive(&archive_dir, &manifest)?;
|
||||
Ok((manifest, archive_dir.join("session")))
|
||||
}
|
||||
|
||||
fn diagnostics_dir(&self, operation_id: &str) -> Result<PathBuf, RuntimeError> {
|
||||
validate_id("operation_id", operation_id)?;
|
||||
Ok(self
|
||||
|
||||
@@ -77,6 +77,41 @@ impl RuntimeWorkspaceScope {
|
||||
}
|
||||
|
||||
const SUBSCRIPTION_QUEUE_CAPACITY: usize = 256;
|
||||
|
||||
#[cfg(feature = "fs-store")]
|
||||
fn session_public_source_unavailable(
|
||||
source: &runtime_api::SessionPublicSource,
|
||||
error: &RuntimeError,
|
||||
) -> Option<(runtime_api::SessionPublicUnavailableReason, &'static str)> {
|
||||
use runtime_api::SessionPublicUnavailableReason as Reason;
|
||||
|
||||
match (source, error) {
|
||||
(
|
||||
runtime_api::SessionPublicSource::Retained { .. },
|
||||
RuntimeError::WorkerNotFound { .. },
|
||||
) => Some((
|
||||
Reason::RetentionMissing,
|
||||
"retained Session storage is missing",
|
||||
)),
|
||||
(runtime_api::SessionPublicSource::Archived { .. }, RuntimeError::StoreMissing { .. }) => {
|
||||
Some((
|
||||
Reason::ArchiveIncomplete,
|
||||
"committed Session archive is incomplete",
|
||||
))
|
||||
}
|
||||
(runtime_api::SessionPublicSource::Archived { .. }, RuntimeError::StoreCorrupt { .. }) => {
|
||||
Some((
|
||||
Reason::ArchiveIncomplete,
|
||||
"committed Session archive failed integrity validation",
|
||||
))
|
||||
}
|
||||
(_, RuntimeError::StoreIo { .. } | RuntimeError::StoreCommitOutcomeUnknown { .. }) => {
|
||||
Some((Reason::StorageUnavailable, "Session storage is unavailable"))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const WORKER_DELETE_FAILURE_DIAGNOSTIC_CODE: &str = "worker_delete_persistence_failed";
|
||||
const WORKER_DELETE_FAILURE_DIAGNOSTIC_MESSAGE: &str =
|
||||
"Worker metadata deletion failed; the persisted Worker identity was retained for retry";
|
||||
@@ -151,7 +186,6 @@ impl RuntimeWorkerRestoreResult {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RuntimeSubscriptionUpdate {
|
||||
pub subject_revision: u64,
|
||||
pub payload: SubscriptionEventPayload,
|
||||
}
|
||||
|
||||
@@ -168,7 +202,6 @@ pub enum RuntimeSubscriptionRecvError {
|
||||
pub struct RuntimeEventSelectorSubscription {
|
||||
subscription_id: u64,
|
||||
selector: EventSubscriptionSelector,
|
||||
snapshot_revision: u64,
|
||||
snapshot: SubscriptionSnapshot,
|
||||
receiver: mpsc::Receiver<RuntimeSubscriptionUpdate>,
|
||||
lagged: Arc<AtomicBool>,
|
||||
@@ -180,10 +213,6 @@ impl RuntimeEventSelectorSubscription {
|
||||
&self.selector
|
||||
}
|
||||
|
||||
pub fn snapshot_revision(&self) -> u64 {
|
||||
self.snapshot_revision
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> &SubscriptionSnapshot {
|
||||
&self.snapshot
|
||||
}
|
||||
@@ -1280,7 +1309,6 @@ impl Runtime {
|
||||
state.persist_runtime_snapshot()?;
|
||||
}
|
||||
let snapshot = state.subscription_snapshot(scope, &selector)?;
|
||||
let snapshot_revision = state.subscription_revision;
|
||||
let subscription_id = state.next_event_subscription_id;
|
||||
state.next_event_subscription_id = state.next_event_subscription_id.saturating_add(1);
|
||||
let (sender, receiver) = mpsc::channel(SUBSCRIPTION_QUEUE_CAPACITY);
|
||||
@@ -1298,7 +1326,6 @@ impl Runtime {
|
||||
Ok(RuntimeEventSelectorSubscription {
|
||||
subscription_id,
|
||||
selector,
|
||||
snapshot_revision,
|
||||
snapshot,
|
||||
receiver,
|
||||
lagged,
|
||||
@@ -2217,9 +2244,9 @@ impl Runtime {
|
||||
if let Some(payload) = input_protocol_event(&input) {
|
||||
state.push_worker_observation_event(worker_ref.clone(), payload);
|
||||
}
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
|
||||
Ok(WorkerInteractionAck {
|
||||
worker_ref: worker_ref.clone(),
|
||||
@@ -2500,9 +2527,9 @@ impl Runtime {
|
||||
worker.workdir_attachments = workdir_attachments;
|
||||
worker.detail()
|
||||
};
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
Ok(detail)
|
||||
}
|
||||
|
||||
@@ -2541,8 +2568,8 @@ impl Runtime {
|
||||
state
|
||||
.observation_events
|
||||
.retain(|event| event.worker_ref != *worker_ref);
|
||||
state.publish_worker_removed(worker_ref.worker_id, workspace_id.as_deref())?;
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.publish_worker_removed(worker_ref.worker_id, workspace_id.as_deref())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2564,9 +2591,9 @@ impl Runtime {
|
||||
let mut state = self.lock()?;
|
||||
let worker = state.worker_mut(worker_ref)?;
|
||||
worker.apply_worker_state(&snapshot);
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2870,8 +2897,8 @@ impl Runtime {
|
||||
state
|
||||
.observation_events
|
||||
.retain(|event| event.worker_ref != *worker_ref);
|
||||
state.publish_worker_removed(worker_ref.worker_id, removed_workspace_id.as_deref())?;
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.publish_worker_removed(worker_ref.worker_id, removed_workspace_id.as_deref())?;
|
||||
Ok(WorkerDeleteResult {
|
||||
worker_id: removed.worker_id,
|
||||
deleted: true,
|
||||
@@ -2959,13 +2986,13 @@ impl Runtime {
|
||||
let worker_state_changed =
|
||||
state.project_protocol_event_to_worker_state(worker_ref, &payload);
|
||||
let activity_changed = state.project_internal_worker_activity(worker_ref, &payload);
|
||||
if worker_state_changed || activity_changed {
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
}
|
||||
if worker_state_changed {
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
}
|
||||
if worker_state_changed || activity_changed {
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
}
|
||||
let event = state.push_worker_observation_event(worker_ref.clone(), payload);
|
||||
Ok(event)
|
||||
}
|
||||
@@ -3021,13 +3048,13 @@ impl Runtime {
|
||||
state.project_protocol_event_to_worker_state(&worker_ref, &payload);
|
||||
let activity_changed =
|
||||
state.project_internal_worker_activity(&worker_ref, &payload);
|
||||
if worker_state_changed || activity_changed {
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
}
|
||||
if worker_state_changed {
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
}
|
||||
if worker_state_changed || activity_changed {
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
}
|
||||
Ok(state.push_worker_observation_event(worker_ref, payload))
|
||||
}),
|
||||
)
|
||||
@@ -3054,13 +3081,13 @@ impl Runtime {
|
||||
state.project_protocol_event_to_worker_state(&worker_ref, &payload);
|
||||
let activity_changed =
|
||||
state.project_internal_worker_activity(&worker_ref, &payload);
|
||||
if worker_state_changed || activity_changed {
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
}
|
||||
if worker_state_changed {
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
}
|
||||
if worker_state_changed || activity_changed {
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
}
|
||||
Ok(state.push_worker_observation_event(worker_ref, payload))
|
||||
}),
|
||||
)
|
||||
@@ -3454,8 +3481,8 @@ impl Runtime {
|
||||
logical_workdir_attachments_from_statuses(&workdir_attachments, claims);
|
||||
candidate.workdir_attachments = workdir_attachments;
|
||||
state.workers.insert(worker_ref.worker_id, candidate);
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
state.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3551,6 +3578,123 @@ impl Runtime {
|
||||
)
|
||||
}
|
||||
|
||||
/// Search the committed public projection of one Backend-selected retained
|
||||
/// Session or verified archive without restoring a Worker.
|
||||
#[cfg(feature = "fs-store")]
|
||||
pub fn session_public_search_scoped(
|
||||
&self,
|
||||
scope: &RuntimeWorkspaceScope,
|
||||
request: &runtime_api::SessionPublicSearchRequest,
|
||||
) -> Result<runtime_api::SessionPublicSearchAvailability, RuntimeError> {
|
||||
let (session_root, archive) =
|
||||
match self.resolve_session_public_source(scope, &request.source) {
|
||||
Ok(source) => source,
|
||||
Err(error) => {
|
||||
if let Some((reason, message)) =
|
||||
session_public_source_unavailable(&request.source, &error)
|
||||
{
|
||||
return Ok(runtime_api::SessionPublicSearchAvailability::Unavailable {
|
||||
reason,
|
||||
message: message.to_string(),
|
||||
});
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
Ok(crate::session_public::search(
|
||||
&session_root,
|
||||
request,
|
||||
archive.as_ref(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Read one exact public entry from a Backend-selected retained Session or
|
||||
/// verified archive. No fallback Worker, Segment, or entry is selected.
|
||||
#[cfg(feature = "fs-store")]
|
||||
pub fn session_public_read_scoped(
|
||||
&self,
|
||||
scope: &RuntimeWorkspaceScope,
|
||||
request: &runtime_api::SessionPublicReadRequest,
|
||||
) -> Result<runtime_api::SessionPublicReadAvailability, RuntimeError> {
|
||||
let (session_root, archive) =
|
||||
match self.resolve_session_public_source(scope, &request.source) {
|
||||
Ok(source) => source,
|
||||
Err(error) => {
|
||||
if let Some((reason, message)) =
|
||||
session_public_source_unavailable(&request.source, &error)
|
||||
{
|
||||
return Ok(runtime_api::SessionPublicReadAvailability::Unavailable {
|
||||
reason,
|
||||
message: message.to_string(),
|
||||
});
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
Ok(crate::session_public::read(
|
||||
&session_root,
|
||||
request,
|
||||
archive.as_ref(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "fs-store")]
|
||||
fn resolve_session_public_source(
|
||||
&self,
|
||||
scope: &RuntimeWorkspaceScope,
|
||||
source: &runtime_api::SessionPublicSource,
|
||||
) -> Result<
|
||||
(
|
||||
std::path::PathBuf,
|
||||
Option<crate::retention::WorkerSessionArchiveManifest>,
|
||||
),
|
||||
RuntimeError,
|
||||
> {
|
||||
let state = self.lock()?;
|
||||
let store = state.fs_store().ok_or_else(|| {
|
||||
RuntimeError::InvalidRequest(
|
||||
"Session public reads require an fs-backed Runtime".to_string(),
|
||||
)
|
||||
})?;
|
||||
match source {
|
||||
runtime_api::SessionPublicSource::Retained { worker_id } => {
|
||||
let worker_ref = WorkerRef::new(*worker_id);
|
||||
let worker = state.worker(&worker_ref)?;
|
||||
if worker.workspace_id.as_deref() != Some(scope.workspace_id.as_str()) {
|
||||
return Err(RuntimeError::WorkerNotFound {
|
||||
worker_id: *worker_id,
|
||||
});
|
||||
}
|
||||
Ok((
|
||||
store
|
||||
.runtime_dir()
|
||||
.join("workers")
|
||||
.join(worker_id.to_string())
|
||||
.join("session"),
|
||||
None,
|
||||
))
|
||||
}
|
||||
runtime_api::SessionPublicSource::Archived { archive_id } => {
|
||||
let runtime_id = state.runtime_identity.as_deref().ok_or_else(|| {
|
||||
RuntimeError::InvalidRequest(
|
||||
"Runtime identity is not bound for Session archive reads".to_string(),
|
||||
)
|
||||
})?;
|
||||
let provider = FsWorkerRetentionProvider::new(store.runtime_dir());
|
||||
let (manifest, session_root) = provider.session_archive(archive_id)?;
|
||||
if manifest.workspace_id != scope.workspace_id
|
||||
|| manifest.source_runtime_id != runtime_id
|
||||
{
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"Session archive scope does not match the authenticated Runtime capability"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Ok((session_root, Some(manifest)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerate host-authoritative Runtime inventory for Backend orphan
|
||||
/// reconciliation. Runtime identity and Workspace scope are derived here,
|
||||
/// not accepted in a diagnostic payload.
|
||||
@@ -3702,8 +3846,6 @@ struct RuntimeState {
|
||||
workspace_config_latest: BTreeMap<String, ConfigBundleRef>,
|
||||
workspace_config_fetch_gates: BTreeMap<String, Arc<Mutex<()>>>,
|
||||
diagnostics: Vec<RuntimeDiagnostic>,
|
||||
subscription_revision: u64,
|
||||
worker_subject_revisions: BTreeMap<WorkerId, u64>,
|
||||
next_event_subscription_id: u64,
|
||||
subscriptions: BTreeMap<u64, SubscriptionSink>,
|
||||
#[cfg(feature = "ws-server")]
|
||||
@@ -3733,8 +3875,6 @@ impl RuntimeState {
|
||||
workspace_config_latest: BTreeMap::new(),
|
||||
workspace_config_fetch_gates: BTreeMap::new(),
|
||||
diagnostics: Vec::new(),
|
||||
subscription_revision: 0,
|
||||
worker_subject_revisions: BTreeMap::new(),
|
||||
next_event_subscription_id: 1,
|
||||
subscriptions: BTreeMap::new(),
|
||||
#[cfg(feature = "ws-server")]
|
||||
@@ -3765,8 +3905,6 @@ impl RuntimeState {
|
||||
workspace_config_latest: BTreeMap::new(),
|
||||
workspace_config_fetch_gates: BTreeMap::new(),
|
||||
diagnostics: Vec::new(),
|
||||
subscription_revision: 0,
|
||||
worker_subject_revisions: BTreeMap::new(),
|
||||
next_event_subscription_id: 1,
|
||||
subscriptions: BTreeMap::new(),
|
||||
#[cfg(feature = "ws-server")]
|
||||
@@ -3895,8 +4033,6 @@ impl RuntimeState {
|
||||
workspace_config_fetch_gates: BTreeMap::new(),
|
||||
workspace_owners: persisted.workspace_owners,
|
||||
diagnostics,
|
||||
subscription_revision: 0,
|
||||
worker_subject_revisions: BTreeMap::new(),
|
||||
next_event_subscription_id: 1,
|
||||
subscriptions: BTreeMap::new(),
|
||||
#[cfg(feature = "ws-server")]
|
||||
@@ -4215,11 +4351,6 @@ impl RuntimeState {
|
||||
runtime_id: None,
|
||||
resource_key: None,
|
||||
availability: protocol::subscription::SubscriptionWorkerAvailability::Observed,
|
||||
subject_revision: self
|
||||
.worker_subject_revisions
|
||||
.get(&worker.worker_id)
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
worker_state: worker.worker_state.clone(),
|
||||
state: subscription_worker_state(worker.status),
|
||||
has_running_internal_workers: worker.internal_workers.has_running_worker(),
|
||||
@@ -4232,25 +4363,17 @@ impl RuntimeState {
|
||||
}
|
||||
|
||||
fn publish_worker_upsert(&mut self, worker_id: WorkerId) -> Result<(), RuntimeError> {
|
||||
self.subscription_revision = self.subscription_revision.saturating_add(1);
|
||||
let subject_revision = {
|
||||
let revision = self.worker_subject_revisions.entry(worker_id).or_insert(0);
|
||||
*revision = revision.saturating_add(1);
|
||||
*revision
|
||||
};
|
||||
let worker = self
|
||||
.workers
|
||||
.get(&worker_id)
|
||||
.ok_or(RuntimeError::WorkerNotFound { worker_id })?;
|
||||
let workspace_id = worker.workspace_id.clone();
|
||||
let mut projected = self.subscription_worker(worker)?;
|
||||
projected.subject_revision = subject_revision;
|
||||
let projected = self.subscription_worker(worker)?;
|
||||
let projected_worker_id = projected.worker_id.clone();
|
||||
self.deliver_worker_subscription_update(
|
||||
&projected_worker_id,
|
||||
workspace_id.as_deref(),
|
||||
RuntimeSubscriptionUpdate {
|
||||
subject_revision,
|
||||
payload: SubscriptionEventPayload::WorkerUpserted { worker: projected },
|
||||
},
|
||||
);
|
||||
@@ -4262,19 +4385,12 @@ impl RuntimeState {
|
||||
worker_id: WorkerId,
|
||||
workspace_id: Option<&str>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
self.subscription_revision = self.subscription_revision.saturating_add(1);
|
||||
let subject_revision = {
|
||||
let revision = self.worker_subject_revisions.entry(worker_id).or_insert(0);
|
||||
*revision = revision.saturating_add(1);
|
||||
*revision
|
||||
};
|
||||
let worker_id = SubscriptionWorkerId::new(worker_id.to_string())
|
||||
.map_err(subscription_validation_error)?;
|
||||
self.deliver_worker_subscription_update(
|
||||
&worker_id,
|
||||
workspace_id,
|
||||
RuntimeSubscriptionUpdate {
|
||||
subject_revision,
|
||||
payload: SubscriptionEventPayload::WorkerRemoved {
|
||||
worker_id: worker_id.clone(),
|
||||
runtime_id: None,
|
||||
@@ -4417,8 +4533,8 @@ impl RuntimeState {
|
||||
worker_ref: Some(worker_ref.clone()),
|
||||
});
|
||||
self.workers.insert(worker_ref.worker_id, candidate);
|
||||
self.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
self.persist_runtime_snapshot()?;
|
||||
self.publish_worker_upsert(worker_ref.worker_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5520,6 +5636,7 @@ mod tests {
|
||||
worker,
|
||||
revision,
|
||||
status,
|
||||
greeting: None,
|
||||
session: protocol::SessionSnapshot {
|
||||
pending_submissions: protocol::PendingSubmissionsSnapshot::default(),
|
||||
entries: Vec::new(),
|
||||
@@ -5545,6 +5662,8 @@ mod tests {
|
||||
tools: Vec::new(),
|
||||
context_window: 0,
|
||||
context_tokens: 0,
|
||||
reasoning: None,
|
||||
context_usage: None,
|
||||
},
|
||||
state: protocol::WorkerStatus::Idle.into(),
|
||||
in_flight: protocol::InFlightSnapshot::default(),
|
||||
@@ -5883,6 +6002,7 @@ mod tests {
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
}),
|
||||
subjektiv_attached: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6647,7 +6767,6 @@ mod tests {
|
||||
let mut subscription = runtime
|
||||
.subscribe_event_selector(EventSubscriptionSelector::RuntimeWorkers)
|
||||
.unwrap();
|
||||
assert_eq!(subscription.snapshot_revision(), 0);
|
||||
let SubscriptionSnapshot::Workers { workers } = subscription.snapshot() else {
|
||||
panic!("runtime_workers must return a Worker snapshot");
|
||||
};
|
||||
@@ -6655,11 +6774,9 @@ mod tests {
|
||||
|
||||
let created = runtime.create_worker(task_request("live")).unwrap();
|
||||
let update = receive_subscription_update(&mut subscription).unwrap();
|
||||
assert_eq!(update.subject_revision, 1);
|
||||
match update.payload {
|
||||
SubscriptionEventPayload::WorkerUpserted { worker } => {
|
||||
assert_eq!(worker.worker_id.as_str(), created.worker_id.to_string());
|
||||
assert_eq!(worker.subject_revision, 1);
|
||||
assert_eq!(worker.state, SubscriptionWorkerState::Idle);
|
||||
}
|
||||
payload => panic!("unexpected subscription payload: {payload:?}"),
|
||||
@@ -6676,7 +6793,6 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let update = receive_subscription_update(&mut subscription).unwrap();
|
||||
assert_eq!(update.subject_revision, 2);
|
||||
match update.payload {
|
||||
SubscriptionEventPayload::WorkerUpserted { worker } => {
|
||||
assert_eq!(worker.state, SubscriptionWorkerState::Idle);
|
||||
@@ -6695,7 +6811,6 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let update = receive_subscription_update(&mut subscription).unwrap();
|
||||
assert_eq!(update.subject_revision, 3);
|
||||
match update.payload {
|
||||
SubscriptionEventPayload::WorkerUpserted { worker } => {
|
||||
assert_eq!(worker.state, SubscriptionWorkerState::Idle);
|
||||
@@ -6737,7 +6852,6 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let update = receive_subscription_update(&mut subscription).unwrap();
|
||||
assert_eq!(update.subject_revision, 4);
|
||||
match update.payload {
|
||||
SubscriptionEventPayload::WorkerUpserted { worker } => {
|
||||
assert_eq!(worker.state, SubscriptionWorkerState::Idle);
|
||||
@@ -6748,7 +6862,6 @@ mod tests {
|
||||
|
||||
runtime.stop_worker(&created.worker_ref, None).unwrap();
|
||||
let update = receive_subscription_update(&mut subscription).unwrap();
|
||||
assert_eq!(update.subject_revision, 5);
|
||||
match update.payload {
|
||||
SubscriptionEventPayload::WorkerUpserted { worker } => {
|
||||
assert_eq!(worker.worker_id.as_str(), created.worker_id.to_string());
|
||||
@@ -6760,6 +6873,87 @@ mod tests {
|
||||
runtime.stop_runtime().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_snapshot_and_worker_creation_cross_the_boundary_exactly_once() {
|
||||
let runtime = runtime_with_backend();
|
||||
let barrier = Arc::new(Barrier::new(2));
|
||||
let subscribing_runtime = runtime.clone();
|
||||
let subscribing_barrier = barrier.clone();
|
||||
let subscriber = std::thread::spawn(move || {
|
||||
subscribing_barrier.wait();
|
||||
subscribing_runtime
|
||||
.subscribe_event_selector(EventSubscriptionSelector::RuntimeWorkers)
|
||||
.unwrap()
|
||||
});
|
||||
let creating_runtime = runtime.clone();
|
||||
let creating_barrier = barrier.clone();
|
||||
let creator = std::thread::spawn(move || {
|
||||
creating_barrier.wait();
|
||||
creating_runtime
|
||||
.create_worker(task_request("concurrent-boundary"))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let mut subscription = subscriber.join().unwrap();
|
||||
let created = creator.join().unwrap();
|
||||
let SubscriptionSnapshot::Workers { workers } = subscription.snapshot() else {
|
||||
panic!("runtime_workers must return a Worker snapshot");
|
||||
};
|
||||
let in_snapshot = workers
|
||||
.iter()
|
||||
.any(|worker| worker.worker_id.as_str() == created.worker_id.to_string());
|
||||
let in_event = match subscription.receiver.try_recv() {
|
||||
Ok(RuntimeSubscriptionUpdate {
|
||||
payload: SubscriptionEventPayload::WorkerUpserted { worker },
|
||||
}) => worker.worker_id.as_str() == created.worker_id.to_string(),
|
||||
Ok(update) => panic!("unexpected subscription update: {update:?}"),
|
||||
Err(mpsc::error::TryRecvError::Empty) => false,
|
||||
Err(error) => panic!("unexpected subscription receive error: {error}"),
|
||||
};
|
||||
assert_ne!(
|
||||
in_snapshot, in_event,
|
||||
"the concurrent creation must appear in the snapshot or its ordered suffix, never both"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "fs-store")]
|
||||
#[test]
|
||||
fn restarted_runtime_subscription_starts_from_persisted_authoritative_snapshot() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let options = crate::fs_store::FsRuntimeStoreOptions {
|
||||
root: root.path().join("runtime"),
|
||||
runtime_id: "subscription-restart".to_string(),
|
||||
display_name: None,
|
||||
};
|
||||
let runtime = Runtime::with_fs_store_and_execution_backend(
|
||||
options.clone(),
|
||||
Arc::new(TestExecutionBackend::default()),
|
||||
)
|
||||
.unwrap();
|
||||
runtime.store_config_bundle(test_bundle()).unwrap();
|
||||
let worker = runtime
|
||||
.create_worker(task_request("persisted-subscription-worker"))
|
||||
.unwrap();
|
||||
drop(runtime);
|
||||
|
||||
let restarted = Runtime::with_fs_store_and_execution_backend(
|
||||
options,
|
||||
Arc::new(TestExecutionBackend::default()),
|
||||
)
|
||||
.unwrap();
|
||||
let subscription = restarted
|
||||
.subscribe_event_selector(EventSubscriptionSelector::RuntimeWorkers)
|
||||
.unwrap();
|
||||
let SubscriptionSnapshot::Workers { workers } = subscription.snapshot() else {
|
||||
panic!("runtime_workers must return a Worker snapshot");
|
||||
};
|
||||
assert!(workers.iter().any(|candidate| {
|
||||
candidate.worker_id.as_str() == worker.worker_id.to_string()
|
||||
&& candidate.availability
|
||||
== protocol::subscription::SubscriptionWorkerAvailability::Observed
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_lifecycle_subscription_delivers_only_selected_workers() {
|
||||
let runtime = runtime_with_backend();
|
||||
@@ -8108,6 +8302,8 @@ mod tests {
|
||||
tools: Vec::new(),
|
||||
context_window: 128,
|
||||
context_tokens: 64,
|
||||
reasoning: None,
|
||||
context_usage: None,
|
||||
},
|
||||
state: protocol::WorkerStatus::Running.into(),
|
||||
in_flight: protocol::InFlightSnapshot {
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
use std::path::Path;
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use runtime_api::{
|
||||
SessionPublicEntryKind, SessionPublicLineage, SessionPublicLineageKind,
|
||||
SessionPublicReadAvailability, SessionPublicReadMode, SessionPublicReadPage,
|
||||
SessionPublicReadRequest, SessionPublicSearchAvailability, SessionPublicSearchItem,
|
||||
SessionPublicSearchPage, SessionPublicSearchRequest, SessionPublicToolPart,
|
||||
SessionPublicUnavailableReason, WorkerSessionArchiveManifest,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session_store::{
|
||||
SessionPublicIndexEntry, SessionPublicIndexEntryKind, SessionPublicIndexLimits,
|
||||
SessionPublicIndexLineage, SessionPublicIndexOriginKind, SessionPublicIndexReadError,
|
||||
SessionPublicIndexScanPosition, SessionPublicIndexToolPart, read_session_public_index_entry,
|
||||
read_session_public_index_page,
|
||||
};
|
||||
|
||||
use crate::retention::WorkerSessionArchiveManifest as DomainArchiveManifest;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct SessionPublicRuntimeCursor {
|
||||
generation: String,
|
||||
position: SessionPublicIndexScanPosition,
|
||||
}
|
||||
|
||||
pub(crate) fn search(
|
||||
session_root: &Path,
|
||||
request: &SessionPublicSearchRequest,
|
||||
archive_manifest: Option<&DomainArchiveManifest>,
|
||||
) -> SessionPublicSearchAvailability {
|
||||
if request.limit == 0 || request.limit > 100 {
|
||||
return unavailable(
|
||||
SessionPublicUnavailableReason::ResourceLimit,
|
||||
"Session public search limit must be between 1 and 100",
|
||||
);
|
||||
}
|
||||
let cursor = match request.scan_cursor.as_deref().map(decode_scan_cursor) {
|
||||
Some(Ok(cursor)) => Some(cursor),
|
||||
Some(Err(())) => {
|
||||
return unavailable(
|
||||
SessionPublicUnavailableReason::InvalidCursor,
|
||||
"Session public search cursor is invalid",
|
||||
);
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if let (Some(expected), Some(cursor)) =
|
||||
(request.expected_generation.as_deref(), cursor.as_ref())
|
||||
&& expected != cursor.generation
|
||||
{
|
||||
return unavailable(
|
||||
SessionPublicUnavailableReason::InvalidCursor,
|
||||
"Session public search cursor generation changed",
|
||||
);
|
||||
}
|
||||
|
||||
let query = request
|
||||
.query
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_lowercase);
|
||||
let expected_generation = request
|
||||
.expected_generation
|
||||
.as_deref()
|
||||
.or_else(|| cursor.as_ref().map(|cursor| cursor.generation.as_str()));
|
||||
let index = match read_session_public_index_page(
|
||||
session_root,
|
||||
SessionPublicIndexLimits {
|
||||
max_bytes: request.max_scan_bytes,
|
||||
max_segments: request.max_segments,
|
||||
max_entries: request.max_entries,
|
||||
},
|
||||
expected_generation,
|
||||
cursor.as_ref().map(|cursor| cursor.position.clone()),
|
||||
request.limit,
|
||||
|entry| matches_entry(entry, request, query.as_deref()),
|
||||
) {
|
||||
Ok(index) => index,
|
||||
Err(error) => {
|
||||
let (reason, message) = map_index_error(error);
|
||||
return unavailable(reason, message);
|
||||
}
|
||||
};
|
||||
if index.session_id != request.expected_session_id {
|
||||
return unavailable(
|
||||
SessionPublicUnavailableReason::NotFound,
|
||||
"Session identity does not match the authorized source",
|
||||
);
|
||||
}
|
||||
|
||||
let mut items = Vec::with_capacity(index.entries.len());
|
||||
for item in index.entries {
|
||||
let scan_cursor = match encode_scan_cursor(&SessionPublicRuntimeCursor {
|
||||
generation: index.generation.clone(),
|
||||
position: item.scan_from,
|
||||
}) {
|
||||
Ok(cursor) => cursor,
|
||||
Err(()) => {
|
||||
return unavailable(
|
||||
SessionPublicUnavailableReason::StorageUnavailable,
|
||||
"Session public search cursor could not be encoded",
|
||||
);
|
||||
}
|
||||
};
|
||||
let entry = item.entry;
|
||||
items.push(SessionPublicSearchItem {
|
||||
segment_id: item.segment_id,
|
||||
entry_ref: entry.entry_ref,
|
||||
kind: entry_kind(entry.kind),
|
||||
origin: entry.provenance,
|
||||
tool_name: entry.tool_name,
|
||||
tool_part: entry.tool_part.map(tool_part),
|
||||
compact: entry.compact_text,
|
||||
compact_truncated: entry.compact_text_truncated,
|
||||
lineage: lineage(&item.lineage),
|
||||
scan_cursor,
|
||||
});
|
||||
}
|
||||
let next_scan_cursor = match index.next_position {
|
||||
Some(position) => match encode_scan_cursor(&SessionPublicRuntimeCursor {
|
||||
generation: index.generation.clone(),
|
||||
position,
|
||||
}) {
|
||||
Ok(cursor) => Some(cursor),
|
||||
Err(()) => {
|
||||
return unavailable(
|
||||
SessionPublicUnavailableReason::StorageUnavailable,
|
||||
"Session public search cursor could not be encoded",
|
||||
);
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
SessionPublicSearchAvailability::Page {
|
||||
page: SessionPublicSearchPage {
|
||||
session_id: index.session_id,
|
||||
generation: index.generation,
|
||||
items,
|
||||
next_scan_cursor,
|
||||
has_more: index.has_more,
|
||||
scanned_bytes: index.scanned_bytes,
|
||||
scanned_segments: index.scanned_segments,
|
||||
scanned_entries: index.scanned_entries,
|
||||
archive_manifest: archive_manifest.map(api_archive_manifest),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_scan_cursor(cursor: &SessionPublicRuntimeCursor) -> Result<String, ()> {
|
||||
serde_json::to_vec(cursor)
|
||||
.map(|bytes| URL_SAFE_NO_PAD.encode(bytes))
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
fn decode_scan_cursor(cursor: &str) -> Result<SessionPublicRuntimeCursor, ()> {
|
||||
let bytes = URL_SAFE_NO_PAD.decode(cursor).map_err(|_| ())?;
|
||||
serde_json::from_slice(&bytes).map_err(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn read(
|
||||
session_root: &Path,
|
||||
request: &SessionPublicReadRequest,
|
||||
archive_manifest: Option<&DomainArchiveManifest>,
|
||||
) -> SessionPublicReadAvailability {
|
||||
if request.max_content_bytes == 0 {
|
||||
return read_unavailable(
|
||||
SessionPublicUnavailableReason::ResourceLimit,
|
||||
"Session public read content limit must be positive",
|
||||
);
|
||||
}
|
||||
let selected = match read_session_public_index_entry(
|
||||
session_root,
|
||||
SessionPublicIndexLimits {
|
||||
max_bytes: request.max_scan_bytes,
|
||||
max_segments: request.max_segments,
|
||||
max_entries: request.max_entries,
|
||||
},
|
||||
request.expected_generation.as_deref(),
|
||||
&request.segment_id,
|
||||
&request.entry_ref,
|
||||
) {
|
||||
Ok(Some(selected)) => selected,
|
||||
Ok(None) => {
|
||||
return read_unavailable(
|
||||
SessionPublicUnavailableReason::NotFound,
|
||||
"Session entry was not found in the authorized source",
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
let (reason, message) = map_index_error(error);
|
||||
return read_unavailable(reason, message);
|
||||
}
|
||||
};
|
||||
if selected.session_id != request.expected_session_id {
|
||||
return read_unavailable(
|
||||
SessionPublicUnavailableReason::NotFound,
|
||||
"Session identity does not match the authorized source",
|
||||
);
|
||||
}
|
||||
let item = selected.item;
|
||||
let entry = item.entry;
|
||||
let content = match request.mode {
|
||||
SessionPublicReadMode::Compact => &entry.compact_text,
|
||||
SessionPublicReadMode::Full => &entry.full_text,
|
||||
};
|
||||
if request.byte_offset > content.len() || !content.is_char_boundary(request.byte_offset) {
|
||||
return read_unavailable(
|
||||
SessionPublicUnavailableReason::InvalidCursor,
|
||||
"Session public read position is stale",
|
||||
);
|
||||
}
|
||||
let mut end = request
|
||||
.byte_offset
|
||||
.saturating_add(request.max_content_bytes)
|
||||
.min(content.len());
|
||||
while end > request.byte_offset && !content.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
if end == request.byte_offset && request.byte_offset < content.len() {
|
||||
return read_unavailable(
|
||||
SessionPublicUnavailableReason::ResourceLimit,
|
||||
"Session public read cannot fit one UTF-8 scalar in the content limit",
|
||||
);
|
||||
}
|
||||
let has_more = end < content.len();
|
||||
SessionPublicReadAvailability::Page {
|
||||
page: SessionPublicReadPage {
|
||||
session_id: selected.session_id,
|
||||
generation: selected.generation,
|
||||
segment_id: item.segment_id,
|
||||
entry_ref: entry.entry_ref.clone(),
|
||||
kind: entry_kind(entry.kind),
|
||||
origin: entry.provenance.clone(),
|
||||
lineage: lineage(&item.lineage),
|
||||
mode: request.mode,
|
||||
content: content[request.byte_offset..end].to_string(),
|
||||
next_byte_offset: has_more.then_some(end),
|
||||
has_more,
|
||||
scanned_bytes: selected.scanned_bytes,
|
||||
scanned_segments: selected.scanned_segments,
|
||||
scanned_entries: selected.scanned_entries,
|
||||
archive_manifest: archive_manifest.map(api_archive_manifest),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_entry(
|
||||
entry: &SessionPublicIndexEntry,
|
||||
request: &SessionPublicSearchRequest,
|
||||
query: Option<&str>,
|
||||
) -> bool {
|
||||
if request
|
||||
.kind
|
||||
.is_some_and(|kind| kind != entry_kind(entry.kind))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if request.tool_name.is_some() || request.tool_part != SessionPublicToolPart::Both {
|
||||
if entry.kind != SessionPublicIndexEntryKind::Tool {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if request
|
||||
.tool_name
|
||||
.as_deref()
|
||||
.is_some_and(|name| entry.tool_name.as_deref() != Some(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if request.tool_part != SessionPublicToolPart::Both
|
||||
&& entry.tool_part.map(tool_part) != Some(request.tool_part)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
query.is_none_or(|query| entry.full_text.to_lowercase().contains(query))
|
||||
}
|
||||
|
||||
fn entry_kind(kind: SessionPublicIndexEntryKind) -> SessionPublicEntryKind {
|
||||
match kind {
|
||||
SessionPublicIndexEntryKind::User => SessionPublicEntryKind::User,
|
||||
SessionPublicIndexEntryKind::Assistant => SessionPublicEntryKind::Assistant,
|
||||
SessionPublicIndexEntryKind::Tool => SessionPublicEntryKind::Tool,
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_part(part: SessionPublicIndexToolPart) -> SessionPublicToolPart {
|
||||
match part {
|
||||
SessionPublicIndexToolPart::Input => SessionPublicToolPart::Input,
|
||||
SessionPublicIndexToolPart::Output => SessionPublicToolPart::Output,
|
||||
}
|
||||
}
|
||||
|
||||
fn lineage(value: &SessionPublicIndexLineage) -> SessionPublicLineage {
|
||||
SessionPublicLineage {
|
||||
kind: match value.origin_kind {
|
||||
SessionPublicIndexOriginKind::Root => SessionPublicLineageKind::Root,
|
||||
SessionPublicIndexOriginKind::Fork => SessionPublicLineageKind::Fork,
|
||||
SessionPublicIndexOriginKind::Compact => SessionPublicLineageKind::Compact,
|
||||
},
|
||||
parent_segment_id: value.parent_segment_id.clone(),
|
||||
at_turn_index: value.parent_turn_index,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_index_error(
|
||||
error: SessionPublicIndexReadError,
|
||||
) -> (SessionPublicUnavailableReason, &'static str) {
|
||||
match error {
|
||||
SessionPublicIndexReadError::MigrationRequired => (
|
||||
SessionPublicUnavailableReason::MigrationRequired,
|
||||
"Session storage requires migration",
|
||||
),
|
||||
SessionPublicIndexReadError::Missing => (
|
||||
SessionPublicUnavailableReason::RetentionMissing,
|
||||
"Session storage is missing",
|
||||
),
|
||||
SessionPublicIndexReadError::Corrupt => (
|
||||
SessionPublicUnavailableReason::CorruptLog,
|
||||
"Session log is corrupt",
|
||||
),
|
||||
SessionPublicIndexReadError::Storage => (
|
||||
SessionPublicUnavailableReason::StorageUnavailable,
|
||||
"Session storage is unavailable",
|
||||
),
|
||||
SessionPublicIndexReadError::ResourceLimit => (
|
||||
SessionPublicUnavailableReason::ResourceLimit,
|
||||
"Session scan exceeds a resource limit",
|
||||
),
|
||||
SessionPublicIndexReadError::StaleCursor => (
|
||||
SessionPublicUnavailableReason::InvalidCursor,
|
||||
"Session public search cursor is stale",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn unavailable(
|
||||
reason: SessionPublicUnavailableReason,
|
||||
message: impl Into<String>,
|
||||
) -> SessionPublicSearchAvailability {
|
||||
SessionPublicSearchAvailability::Unavailable {
|
||||
reason,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_unavailable(
|
||||
reason: SessionPublicUnavailableReason,
|
||||
message: impl Into<String>,
|
||||
) -> SessionPublicReadAvailability {
|
||||
SessionPublicReadAvailability::Unavailable {
|
||||
reason,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn api_archive_manifest(manifest: &DomainArchiveManifest) -> WorkerSessionArchiveManifest {
|
||||
WorkerSessionArchiveManifest {
|
||||
schema_version: manifest.schema_version,
|
||||
archive_id: manifest.archive_id.clone(),
|
||||
workspace_id: manifest.workspace_id.clone(),
|
||||
source_runtime_id: manifest.source_runtime_id.clone(),
|
||||
source_worker_id: manifest.source_worker_id,
|
||||
source_session_id: manifest.source_session_id.clone(),
|
||||
segment_ids: manifest.segment_ids.clone(),
|
||||
source_created_at: manifest.source_created_at.clone(),
|
||||
removed_at: manifest.removed_at.clone(),
|
||||
archived_at_unix_seconds: manifest.archived_at_unix_seconds,
|
||||
effective_profile: manifest.effective_profile.clone(),
|
||||
retention_class: manifest.retention_class.clone(),
|
||||
content_checksum_sha256: manifest.content_checksum_sha256.clone(),
|
||||
content_bytes: manifest.content_bytes,
|
||||
content_file_count: manifest.content_file_count,
|
||||
policy_id: manifest.policy_id.clone(),
|
||||
policy_revision: manifest.policy_revision,
|
||||
operation_id: manifest.operation_id.clone(),
|
||||
input_fingerprint: manifest.input_fingerprint.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use agen::llm_client::RequestConfig;
|
||||
use session_store::{
|
||||
LogEntry, LoggedContentPart, LoggedHistoryEntry, LoggedItem, LoggedRole,
|
||||
LoggedSessionHistoryEntryId, LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin,
|
||||
Store, WorkerSessionStore,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn message(id: &str, text: &str) -> LoggedHistoryEntry {
|
||||
LoggedHistoryEntry {
|
||||
item: LoggedItem::Message {
|
||||
role: LoggedRole::User,
|
||||
content: vec![LoggedContentPart::Text { text: text.into() }],
|
||||
},
|
||||
metadata: LoggedSessionHistoryMetadata {
|
||||
entry_id: LoggedSessionHistoryEntryId(id.to_string()),
|
||||
origin: LoggedSessionHistoryOrigin::HumanInput {
|
||||
account_id: "account-1".to_string(),
|
||||
},
|
||||
derivation: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn request(
|
||||
session_id: session_store::SessionId,
|
||||
generation: Option<String>,
|
||||
scan_cursor: Option<String>,
|
||||
) -> SessionPublicSearchRequest {
|
||||
SessionPublicSearchRequest {
|
||||
workspace_id: "workspace-1".to_string(),
|
||||
source: runtime_api::SessionPublicSource::Retained {
|
||||
worker_id: crate::identity::WorkerId::now_v7(),
|
||||
},
|
||||
expected_session_id: session_id.to_string(),
|
||||
expected_generation: generation,
|
||||
query: Some("needle".to_string()),
|
||||
kind: None,
|
||||
tool_name: None,
|
||||
tool_part: SessionPublicToolPart::Both,
|
||||
scan_cursor,
|
||||
limit: 20,
|
||||
max_scan_bytes: 64 * 1024 * 1024,
|
||||
max_segments: 64,
|
||||
max_entries: 100_000,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_continues_beyond_segment_budget_and_stales_after_mutation() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let root = temp.path().join("session");
|
||||
let store = WorkerSessionStore::new(&root).unwrap();
|
||||
let session_id = session_store::new_session_id();
|
||||
let mut last_segment = None;
|
||||
for index in 0..65_u128 {
|
||||
let segment_id = uuid::Uuid::from_u128(10_000 + index);
|
||||
last_segment = Some(segment_id);
|
||||
store
|
||||
.create_segment(
|
||||
session_id,
|
||||
segment_id,
|
||||
&[LogEntry::AnnotatedSegmentStart {
|
||||
ts: 1,
|
||||
session_id,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
history: vec![message(
|
||||
&format!("entry-{index}"),
|
||||
if index == 64 { "needle" } else { "ordinary" },
|
||||
)],
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let first = match search(&root, &request(session_id, None, None), None) {
|
||||
SessionPublicSearchAvailability::Page { page } => page,
|
||||
other => panic!("unexpected first search result: {other:?}"),
|
||||
};
|
||||
assert!(first.items.is_empty());
|
||||
assert!(first.has_more);
|
||||
let cursor = first.next_scan_cursor.clone().unwrap();
|
||||
|
||||
let second = match search(
|
||||
&root,
|
||||
&request(
|
||||
session_id,
|
||||
Some(first.generation.clone()),
|
||||
Some(cursor.clone()),
|
||||
),
|
||||
None,
|
||||
) {
|
||||
SessionPublicSearchAvailability::Page { page } => page,
|
||||
other => panic!("unexpected continued search result: {other:?}"),
|
||||
};
|
||||
assert_eq!(second.items.len(), 1);
|
||||
assert_eq!(second.items[0].compact, "needle");
|
||||
assert!(!second.has_more);
|
||||
|
||||
let read_page = match read(
|
||||
&root,
|
||||
&SessionPublicReadRequest {
|
||||
workspace_id: "workspace-1".into(),
|
||||
source: runtime_api::SessionPublicSource::Retained {
|
||||
worker_id: crate::identity::WorkerId::now_v7(),
|
||||
},
|
||||
expected_session_id: session_id.to_string(),
|
||||
expected_generation: Some(second.generation.clone()),
|
||||
segment_id: last_segment.unwrap().to_string(),
|
||||
entry_ref: "Eentry-64".into(),
|
||||
mode: SessionPublicReadMode::Full,
|
||||
byte_offset: 0,
|
||||
max_content_bytes: 16 * 1024,
|
||||
max_scan_bytes: 64 * 1024 * 1024,
|
||||
max_segments: 64,
|
||||
max_entries: 100_000,
|
||||
},
|
||||
None,
|
||||
) {
|
||||
SessionPublicReadAvailability::Page { page } => page,
|
||||
other => panic!("unexpected direct read result: {other:?}"),
|
||||
};
|
||||
assert_eq!(read_page.content, "needle");
|
||||
assert_eq!(read_page.segment_id, last_segment.unwrap().to_string());
|
||||
assert!(!read_page.has_more);
|
||||
|
||||
store
|
||||
.append(
|
||||
session_id,
|
||||
last_segment.unwrap(),
|
||||
&LogEntry::AnnotatedAssistantItem {
|
||||
ts: 2,
|
||||
entry: message("mutation", "changed"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
search(
|
||||
&root,
|
||||
&request(session_id, Some(first.generation), Some(cursor)),
|
||||
None,
|
||||
),
|
||||
SessionPublicSearchAvailability::Unavailable {
|
||||
reason: SessionPublicUnavailableReason::InvalidCursor,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_then_read_crosses_entry_budget_within_one_segment() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let root = temp.path().join("session");
|
||||
let store = WorkerSessionStore::new(&root).unwrap();
|
||||
let session_id = session_store::new_session_id();
|
||||
let segment_id = uuid::Uuid::from_u128(20_000);
|
||||
store
|
||||
.create_segment(
|
||||
session_id,
|
||||
segment_id,
|
||||
&[LogEntry::AnnotatedSegmentStart {
|
||||
ts: 1,
|
||||
session_id,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
history: vec![message("seed", "ordinary")],
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
for index in 0..4 {
|
||||
let id = if index == 3 {
|
||||
"target".to_string()
|
||||
} else {
|
||||
format!("ordinary-{index}")
|
||||
};
|
||||
store
|
||||
.append(
|
||||
session_id,
|
||||
segment_id,
|
||||
&LogEntry::AnnotatedAssistantItem {
|
||||
ts: 2 + index,
|
||||
entry: message(&id, if index == 3 { "needle" } else { "ordinary" }),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut generation = None;
|
||||
let mut scan_cursor = None;
|
||||
let found = loop {
|
||||
let mut search_request = request(session_id, generation.clone(), scan_cursor.clone());
|
||||
search_request.max_entries = 2;
|
||||
let page = match search(&root, &search_request, None) {
|
||||
SessionPublicSearchAvailability::Page { page } => page,
|
||||
other => panic!("unexpected search result: {other:?}"),
|
||||
};
|
||||
if let Some(item) = page.items.into_iter().next() {
|
||||
break (page.generation, item);
|
||||
}
|
||||
assert!(page.has_more);
|
||||
generation = Some(page.generation);
|
||||
scan_cursor = page.next_scan_cursor;
|
||||
};
|
||||
assert_eq!(found.1.entry_ref, "Etarget");
|
||||
|
||||
let read_page = match read(
|
||||
&root,
|
||||
&SessionPublicReadRequest {
|
||||
workspace_id: "workspace-1".into(),
|
||||
source: runtime_api::SessionPublicSource::Retained {
|
||||
worker_id: crate::identity::WorkerId::now_v7(),
|
||||
},
|
||||
expected_session_id: session_id.to_string(),
|
||||
expected_generation: Some(found.0),
|
||||
segment_id: segment_id.to_string(),
|
||||
entry_ref: found.1.entry_ref,
|
||||
mode: SessionPublicReadMode::Full,
|
||||
byte_offset: 0,
|
||||
max_content_bytes: 16 * 1024,
|
||||
max_scan_bytes: 64 * 1024 * 1024,
|
||||
max_segments: 64,
|
||||
max_entries: 2,
|
||||
},
|
||||
None,
|
||||
) {
|
||||
SessionPublicReadAvailability::Page { page } => page,
|
||||
other => panic!("unexpected direct read result: {other:?}"),
|
||||
};
|
||||
assert_eq!(read_page.content, "needle");
|
||||
assert!(read_page.scanned_entries > 2);
|
||||
}
|
||||
}
|
||||
@@ -944,7 +944,7 @@ fn bind_workspace_memory_settings(
|
||||
request: &CreateWorkerRequest,
|
||||
) -> Result<(), String> {
|
||||
let Some(snapshot) = request.memory_settings.as_ref() else {
|
||||
if request.workspace_api.is_some() {
|
||||
if request.workspace_api.is_some() || request.subjektiv_attached {
|
||||
return Err(
|
||||
"Workspace Worker request is missing its bound Memory settings snapshot"
|
||||
.to_string(),
|
||||
@@ -960,14 +960,33 @@ fn bind_workspace_memory_settings(
|
||||
snapshot.workspace_id, workspace_api.workspace_id
|
||||
));
|
||||
}
|
||||
if manifest.feature.memory.profile.enabled {
|
||||
manifest
|
||||
.feature
|
||||
.memory
|
||||
.bind_workspace_settings(snapshot.clone())
|
||||
.map_err(str::to_string)?;
|
||||
}
|
||||
if request.subjektiv_attached {
|
||||
if !manifest.feature.subjektiv.profile.enabled {
|
||||
return Err(
|
||||
"subject-attached Worker profile does not enable subjektiv policy".to_string(),
|
||||
);
|
||||
}
|
||||
manifest
|
||||
.feature
|
||||
.subjektiv
|
||||
.bind_workspace_settings(snapshot.clone())
|
||||
.map_err(str::to_string)?;
|
||||
}
|
||||
manifest
|
||||
.feature
|
||||
.memory
|
||||
.bind_workspace_settings(snapshot.clone())
|
||||
.validate_execution()
|
||||
.map_err(str::to_string)?;
|
||||
manifest
|
||||
.feature
|
||||
.memory
|
||||
.subjektiv
|
||||
.validate_execution()
|
||||
.map_err(str::to_string)?;
|
||||
Ok(())
|
||||
@@ -978,6 +997,12 @@ fn validate_worker_memory_settings(
|
||||
request: &CreateWorkerRequest,
|
||||
) -> Result<(), String> {
|
||||
let Some(expected) = request.memory_settings.as_ref() else {
|
||||
if request.subjektiv_attached {
|
||||
return Err(
|
||||
"subject-attached Worker restore is missing its trusted settings snapshot"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
manifest
|
||||
@@ -985,17 +1010,21 @@ fn validate_worker_memory_settings(
|
||||
.memory
|
||||
.validate_execution()
|
||||
.map_err(str::to_string)?;
|
||||
if !manifest.feature.memory.profile.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
let actual = manifest
|
||||
manifest
|
||||
.feature
|
||||
.memory
|
||||
.workspace_settings()
|
||||
.ok_or_else(|| {
|
||||
"Workspace Worker restored without its bound Memory settings snapshot".to_string()
|
||||
})?;
|
||||
if &actual != expected {
|
||||
.subjektiv
|
||||
.validate_execution()
|
||||
.map_err(str::to_string)?;
|
||||
if manifest.feature.memory.profile.enabled
|
||||
&& manifest.feature.memory.workspace_settings().as_ref() != Some(expected)
|
||||
{
|
||||
let actual = manifest
|
||||
.feature
|
||||
.memory
|
||||
.workspace_settings()
|
||||
.ok_or_else(|| {
|
||||
"Workspace Worker restored without its bound Memory settings snapshot".to_string()
|
||||
})?;
|
||||
return Err(format!(
|
||||
"Workspace Worker Memory settings snapshot mismatch: expected {} revision {}, restored {} revision {}",
|
||||
expected.workspace_id,
|
||||
@@ -1004,6 +1033,33 @@ fn validate_worker_memory_settings(
|
||||
actual.settings_revision
|
||||
));
|
||||
}
|
||||
let subjektiv_settings = manifest.feature.subjektiv.workspace_settings();
|
||||
if request.subjektiv_attached {
|
||||
if !manifest.feature.subjektiv.profile.enabled {
|
||||
return Err(
|
||||
"subject-attached Worker restored with a Profile that disables subjektiv policy"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if subjektiv_settings.as_ref() != Some(expected) {
|
||||
let actual = subjektiv_settings.ok_or_else(|| {
|
||||
"subject-attached Worker restored without its bound subjektiv settings snapshot"
|
||||
.to_string()
|
||||
})?;
|
||||
return Err(format!(
|
||||
"Workspace Worker subjektiv settings snapshot mismatch: expected {} revision {}, restored {} revision {}",
|
||||
expected.workspace_id,
|
||||
expected.settings_revision,
|
||||
actual.workspace_id,
|
||||
actual.settings_revision
|
||||
));
|
||||
}
|
||||
} else if subjektiv_settings.is_some() {
|
||||
return Err(
|
||||
"ordinary Workspace Worker restored with an unauthorized subjektiv attachment"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1241,10 +1297,6 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
}
|
||||
}
|
||||
|
||||
worker
|
||||
.materialize_durable_session_head()
|
||||
.await
|
||||
.map_err(|error| format!("materialize durable Worker session head: {error}"))?;
|
||||
let workspace_client = worker.workspace_client_handle();
|
||||
let started = prepared.start().await.map_err(|error| match error {
|
||||
WorkerBootstrapError::Worker(source) => {
|
||||
@@ -4391,9 +4443,70 @@ mod tests {
|
||||
worker_observation_grants: Vec::new(),
|
||||
workspace_api: None,
|
||||
memory_settings: None,
|
||||
subjektiv_attached: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_attachment_alone_activates_subjektiv_profile_policy() {
|
||||
let manifest = || {
|
||||
let mut manifest = WorkerManifest::from_toml(
|
||||
r#"
|
||||
[worker]
|
||||
name = "subjektiv-attachment-test"
|
||||
pwd = "./"
|
||||
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "test-model"
|
||||
auth = { kind = "none" }
|
||||
|
||||
[engine]
|
||||
max_tokens = 100
|
||||
|
||||
[[scope.allow]]
|
||||
target = "./"
|
||||
permission = "read"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
manifest.feature.subjektiv.profile.enabled = true;
|
||||
manifest.feature.subjektiv.profile.extraction.enabled = true;
|
||||
manifest
|
||||
};
|
||||
let settings = manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: "workspace-subject".to_string(),
|
||||
settings_revision: 3,
|
||||
language: "English".to_string(),
|
||||
};
|
||||
let mut ordinary_request = create_request("ordinary");
|
||||
ordinary_request.workspace_api = Some(WorkspaceApiRef {
|
||||
workspace_id: settings.workspace_id.clone(),
|
||||
base_url: "http://workspace.invalid".to_string(),
|
||||
});
|
||||
ordinary_request.memory_settings = Some(settings.clone());
|
||||
let mut ordinary = manifest();
|
||||
bind_workspace_memory_settings(&mut ordinary, &ordinary_request).unwrap();
|
||||
assert!(ordinary.feature.subjektiv.profile.enabled);
|
||||
assert!(!ordinary.feature.subjektiv.execution_enabled());
|
||||
validate_worker_memory_settings(&ordinary, &ordinary_request).unwrap();
|
||||
|
||||
let mut attached_request = ordinary_request.clone();
|
||||
attached_request.subjektiv_attached = true;
|
||||
let mut attached = manifest();
|
||||
bind_workspace_memory_settings(&mut attached, &attached_request).unwrap();
|
||||
assert!(attached.feature.subjektiv.execution_enabled());
|
||||
assert_eq!(
|
||||
attached.feature.subjektiv.workspace_settings(),
|
||||
Some(settings.clone())
|
||||
);
|
||||
validate_worker_memory_settings(&attached, &attached_request).unwrap();
|
||||
assert!(validate_worker_memory_settings(&attached, &ordinary_request).is_err());
|
||||
|
||||
attached_request.memory_settings = None;
|
||||
assert!(bind_workspace_memory_settings(&mut manifest(), &attached_request).is_err());
|
||||
}
|
||||
|
||||
fn git(path: &std::path::Path, args: &[&str]) {
|
||||
let status = Command::new("git")
|
||||
.arg("-C")
|
||||
@@ -4499,6 +4612,8 @@ mod tests {
|
||||
tools: Vec::new(),
|
||||
context_window: 1_000,
|
||||
context_tokens: 0,
|
||||
reasoning: None,
|
||||
context_usage: None,
|
||||
},
|
||||
));
|
||||
hub.workers.lock().unwrap().insert(
|
||||
|
||||
@@ -564,6 +564,30 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
|
||||
.await,
|
||||
)
|
||||
}
|
||||
WorkspaceServerOperation::SubjektivStageCandidate(request) => {
|
||||
generated_workspace_response(
|
||||
client
|
||||
.subjektiv_stage_candidate(workspace_id, request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
WorkspaceServerOperation::SubjektivRecordSession(request) => {
|
||||
generated_workspace_response(
|
||||
client.subjektiv_record_session(workspace_id, request).await,
|
||||
)
|
||||
}
|
||||
WorkspaceServerOperation::SubjektivMemory(request) => {
|
||||
generated_workspace_response(
|
||||
client.subjektiv_memory_backend(workspace_id, request).await,
|
||||
)
|
||||
}
|
||||
WorkspaceServerOperation::SubjektivSession(request) => {
|
||||
generated_workspace_response(
|
||||
client
|
||||
.subjektiv_session_backend(workspace_id, request)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user