feat: integrate bounded subject memory surfaces

This commit is contained in:
2026-10-02 15:11:12 +09:00
20 changed files with 3314 additions and 112 deletions
+11 -8
View File
@@ -1,8 +1,9 @@
use memory::backend::{
MemoryBackendAckOutput, MemoryBackendOperation, MemoryBackendOperationResult,
MemoryDocumentReadOperation, MemoryDocumentUpdateOperation, MemoryQueryOperation,
MemoryStagingCloseAction, MemoryStagingCloseOperation, MemoryStagingListOperation,
MemoryStagingReadOperation, MemoryStagingWriteOutput, MemoryToolOutput,
MemoryResidentSummaryAvailability, MemoryResidentSummaryOutput, MemoryStagingCloseAction,
MemoryStagingCloseOperation, MemoryStagingListOperation, MemoryStagingReadOperation,
MemoryStagingWriteOutput, MemoryToolOutput,
};
use memory::extract::{ExtractedCandidate, StagingEvidence, StagingRecord};
use memory::schema::{SourceEvidenceRef, SourceRef};
@@ -32,7 +33,7 @@ pub fn execute_memory_backend_operation_with_authority<A: MemoryAuthority>(
.map(MemoryBackendOperationResult::ToolOutput)
}
MemoryBackendOperation::ResidentSummary(_operation) => Ok(
MemoryBackendOperationResult::ToolOutput(execute_resident_summary(authority)?),
MemoryBackendOperationResult::ResidentSummary(execute_resident_summary(authority)?),
),
MemoryBackendOperation::AppendAudit(_operation) => Ok(
MemoryBackendOperationResult::Acknowledged(MemoryBackendAckOutput {
@@ -88,16 +89,18 @@ pub fn execute_memory_backend_operation_with_authority<A: MemoryAuthority>(
}
}
fn execute_resident_summary<A: MemoryAuthority>(authority: &A) -> Result<MemoryToolOutput> {
fn execute_resident_summary<A: MemoryAuthority>(
authority: &A,
) -> Result<MemoryResidentSummaryOutput> {
let document = authority.ensure_memory_document()?;
if document.body_md.trim().is_empty() {
Ok(MemoryToolOutput {
summary: "resident memory summary unavailable".to_string(),
Ok(MemoryResidentSummaryOutput {
availability: MemoryResidentSummaryAvailability::Unavailable,
content: None,
})
} else {
Ok(MemoryToolOutput {
summary: "resident memory document collected".to_string(),
Ok(MemoryResidentSummaryOutput {
availability: MemoryResidentSummaryAvailability::Ready,
content: Some(document.body_md),
})
}
+297 -21
View File
@@ -20,8 +20,8 @@ use config_source::ConfigTreeSnapshot;
use flow::{FlowSourceKind, FlowSourceResolveRequest, ResolvedFlowSource};
use futures::{SinkExt, StreamExt};
use memory::backend::{
MemoryBackendHttpResponse, MemoryBackendOperation, MemoryConsolidateStagingOperation,
MemoryConsolidationOutput,
MemoryBackendHttpResponse, MemoryBackendOperation, MemoryBackendOperationResult,
MemoryConsolidateStagingOperation, MemoryConsolidationOutput,
};
use protocol::Segment;
use protocol::stream::{decode_method, encode_event};
@@ -6998,6 +6998,7 @@ impl server_api::ServerApi for ServerApiContractService {
async fn memory_backend(
&self,
context: server_api::ServerRequestContext,
workspace_id: String,
request: server_api::MemoryBackendRequest,
) -> std::result::Result<server_api::MemoryBackendResponse, server_api::RepositoryApiError>
@@ -7005,6 +7006,7 @@ impl server_api::ServerApi for ServerApiContractService {
scoped_memory_backend_operation(
State(self.workspace_api()?.clone()),
AxumPath(ScopedWorkspacePath { workspace_id }),
context,
Json(request.0),
)
.await
@@ -16532,11 +16534,17 @@ async fn scoped_list_memory_staging(
async fn scoped_memory_backend_operation(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
context: server_api::ServerRequestContext,
Json(operation): Json<MemoryBackendOperation>,
) -> ApiResult<Json<MemoryBackendHttpResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let response = match execute_memory_backend_operation_with_authority(&api.authority, operation)
{
let result = match operation {
MemoryBackendOperation::ResidentSummary(_) => {
execute_scoped_resident_summary(&api, &context)
}
operation => execute_memory_backend_operation_with_authority(&api.authority, operation),
};
let response = match result {
Ok(result) => MemoryBackendHttpResponse::Ok { result },
Err(error) => MemoryBackendHttpResponse::Error {
message: sanitize_backend_error(&error.to_string()),
@@ -16545,6 +16553,78 @@ async fn scoped_memory_backend_operation(
Ok(Json(response))
}
fn execute_scoped_resident_summary(
api: &WorkspaceApi,
context: &server_api::ServerRequestContext,
) -> crate::Result<MemoryBackendOperationResult> {
let Some(source) = context.runtime_source.as_ref() else {
return execute_memory_backend_operation_with_authority(
&api.authority,
MemoryBackendOperation::ResidentSummary(Default::default()),
);
};
let Some(worker_id) = source.worker_id.as_deref() else {
return execute_memory_backend_operation_with_authority(
&api.authority,
MemoryBackendOperation::ResidentSummary(Default::default()),
);
};
let worker = RuntimeWorkerRef::new(&source.runtime_id, worker_id);
let Some(lease) = api
.store
.require_current_worker_singleton_owner(&api.config.workspace_id, &worker)?
else {
return execute_memory_backend_operation_with_authority(
&api.authority,
MemoryBackendOperation::ResidentSummary(Default::default()),
);
};
let Some(subject_id) = lease.key.strip_prefix(SUBJEKTIV_SINGLETON_PREFIX) else {
return execute_memory_backend_operation_with_authority(
&api.authority,
MemoryBackendOperation::ResidentSummary(Default::default()),
);
};
let resident =
crate::subjektiv::SubjektivStore::open(&api.feature_storage, &api.subjektiv_registration)
.map_err(|error| Error::Store(error.to_string()))?
.resident_surface(subject_id)
.map_err(|error| Error::Store(error.to_string()))?;
Ok(MemoryBackendOperationResult::ResidentSummary(
resident_summary_output(resident),
))
}
fn resident_summary_output(
resident: crate::subjektiv::ResidentSurface,
) -> memory::backend::MemoryResidentSummaryOutput {
let (availability, content) = match resident.availability {
crate::subjektiv::SurfaceAvailability::Ready => {
let snapshot = resident.snapshot.expect("ready surface has a snapshot");
(
memory::backend::MemoryResidentSummaryAvailability::Ready,
(!snapshot.body_md.is_empty()).then_some(snapshot.body_md),
)
}
crate::subjektiv::SurfaceAvailability::Ungenerated => (
memory::backend::MemoryResidentSummaryAvailability::Ungenerated,
None,
),
crate::subjektiv::SurfaceAvailability::Stale => (
memory::backend::MemoryResidentSummaryAvailability::Stale,
None,
),
crate::subjektiv::SurfaceAvailability::Failed => (
memory::backend::MemoryResidentSummaryAvailability::Failed,
None,
),
};
memory::backend::MemoryResidentSummaryOutput {
availability,
content,
}
}
const SUBJEKTIV_SINGLETON_PREFIX: &str = "subjektiv:";
const SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX: &str =
crate::hosts::SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX;
@@ -17069,10 +17149,97 @@ async fn scoped_subjektiv_memory_backend(
subjektiv_candidate_decision_response(receipt),
)
}
server_api::SubjektivMemoryBackendOperation::PrepareSurface(_) => {
require_subjektiv_worker_authority(
authority,
SubjektivWorkerAuthority::Consolidation,
"surface generation preparation",
)?;
let generation = store
.prepare_surface_generation(&subject_id)
.map_err(subjektiv_store_error)?;
server_api::SubjektivMemoryBackendResponse::SurfacePrepared(
subjektiv_surface_prepare_response(generation),
)
}
server_api::SubjektivMemoryBackendOperation::PublishSurface(input) => {
require_subjektiv_worker_authority(
authority,
SubjektivWorkerAuthority::Consolidation,
"surface publication",
)?;
let points = input
.points
.into_iter()
.map(|point| crate::subjektiv::SurfacePoint {
body_md: point.body_md,
memory_refs: point
.memory_refs
.into_iter()
.map(|reference| crate::subjektiv::MemoryRevisionRef {
memory_id: reference.memory_id,
revision: reference.revision,
})
.collect(),
})
.collect();
let snapshot = store
.publish_surface_generation(&subject_id, &input.generation_id, points)
.map_err(subjektiv_store_error)?;
server_api::SubjektivMemoryBackendResponse::SurfacePublished(
server_api::SubjektivSurfacePublishResponse {
snapshot_id: snapshot.id,
built_from_store_revision: snapshot.built_from_store_revision,
empty: snapshot.body_md.is_empty(),
},
)
}
server_api::SubjektivMemoryBackendOperation::FailSurface(input) => {
require_subjektiv_worker_authority(
authority,
SubjektivWorkerAuthority::Consolidation,
"surface generation failure",
)?;
let revision = store
.fail_surface_generation(&subject_id, &input.generation_id, &input.reason_code)
.map_err(subjektiv_store_error)?;
server_api::SubjektivMemoryBackendResponse::SurfaceFailed(
server_api::SubjektivSurfaceFailureResponse {
store_revision: revision,
status: "failed".into(),
},
)
}
};
Ok(Json(response))
}
fn subjektiv_surface_prepare_response(
generation: crate::subjektiv::SurfaceGeneration,
) -> server_api::SubjektivSurfacePrepareResponse {
server_api::SubjektivSurfacePrepareResponse {
generation_id: generation.id,
store_revision: generation.store_revision,
active_memory_count: generation.active_memory_count,
materials: generation
.materials
.into_iter()
.map(|material| server_api::SubjektivSurfaceMaterial {
memory_id: material.memory_id,
revision: material.revision,
kind: material.kind,
body_md: material.body_md,
why_useful: material.why_useful,
staleness: material.staleness,
})
.collect(),
body_token_budget: crate::subjektiv::SURFACE_BODY_TOKEN_BUDGET,
input_token_budget: crate::subjektiv::SURFACE_INPUT_TOKEN_BUDGET,
per_kind_limit: crate::subjektiv::SURFACE_PER_KIND_LIMIT,
total_material_limit: crate::subjektiv::SURFACE_TOTAL_MATERIAL_LIMIT,
}
}
fn subjektiv_candidate_summary(
candidate: crate::subjektiv::SubjectStagingRecord,
) -> server_api::SubjektivMemoryCandidateSummary {
@@ -18215,7 +18382,8 @@ fn subjektiv_store_error(error: crate::subjektiv::SubjektivError) -> Error {
crate::subjektiv::SubjektivError::SubjectScopeMismatch { .. } => {
Error::WorkspacePermissionDenied(format!("subject_scope_mismatch: {error}"))
}
crate::subjektiv::SubjektivError::RevisionConflict { .. } => {
crate::subjektiv::SubjektivError::RevisionConflict { .. }
| crate::subjektiv::SubjektivError::SurfaceGenerationConflict(_) => {
Error::RepositoryConflict(format!("revision_conflict: {error}"))
}
crate::subjektiv::SubjektivError::CandidateResolved(_)
@@ -18253,28 +18421,91 @@ async fn scoped_subjektiv_memory_consolidation(
)?))
}
const SUBJECT_CONSOLIDATION_THRESHOLD_FILES: usize = 5;
const SUBJECT_CONSOLIDATION_THRESHOLD_BYTES: u64 = 50_000;
fn subject_consolidation_required(
candidate_count: usize,
total_bytes: u64,
force: bool,
surface_availability: crate::subjektiv::SurfaceAvailability,
) -> bool {
let surface_needs_generation =
surface_availability != crate::subjektiv::SurfaceAvailability::Ready;
if candidate_count == 0 {
return surface_needs_generation;
}
force
|| surface_needs_generation
|| candidate_count >= SUBJECT_CONSOLIDATION_THRESHOLD_FILES
|| total_bytes >= SUBJECT_CONSOLIDATION_THRESHOLD_BYTES
}
#[cfg(test)]
mod subject_surface_consolidation_policy_tests {
use super::*;
use crate::subjektiv::SurfaceAvailability;
#[test]
fn failed_stale_and_ungenerated_surfaces_bypass_candidate_thresholds() {
for availability in [
SurfaceAvailability::Failed,
SurfaceAvailability::Stale,
SurfaceAvailability::Ungenerated,
] {
assert!(subject_consolidation_required(0, 0, false, availability));
assert!(subject_consolidation_required(1, 1, false, availability));
}
assert!(!subject_consolidation_required(
0,
0,
true,
SurfaceAvailability::Ready
));
assert!(!subject_consolidation_required(
1,
1,
false,
SurfaceAvailability::Ready
));
assert!(subject_consolidation_required(
SUBJECT_CONSOLIDATION_THRESHOLD_FILES,
1,
false,
SurfaceAvailability::Ready
));
}
}
fn start_subjektiv_staging_consolidation(
api: WorkspaceApi,
subject_id: &str,
operation: MemoryConsolidateStagingOperation,
) -> ApiResult<MemoryConsolidationOutput> {
let (candidate_count, total_bytes) = open_subjektiv_store(&api)?
let store = open_subjektiv_store(&api)?;
let (candidate_count, total_bytes) = store
.pending_staging_backlog(subject_id)
.map_err(subjektiv_store_error)?;
if candidate_count == 0 {
return Ok(MemoryConsolidationOutput {
status: "skipped_empty".into(),
summary: "No subject Memory staging candidates are pending.".into(),
candidate_count,
total_bytes,
});
}
const CONSOLIDATION_THRESHOLD_FILES: usize = 5;
const CONSOLIDATION_THRESHOLD_BYTES: u64 = 50_000;
if !operation.force
&& candidate_count < CONSOLIDATION_THRESHOLD_FILES
&& total_bytes < CONSOLIDATION_THRESHOLD_BYTES
{
let surface_availability = store
.resident_surface(subject_id)
.map_err(subjektiv_store_error)?
.availability;
if !subject_consolidation_required(
candidate_count,
total_bytes,
operation.force,
surface_availability,
) {
if candidate_count == 0 {
return Ok(MemoryConsolidationOutput {
status: "skipped_empty".into(),
summary:
"No subject Memory staging candidates are pending and the surface is current."
.into(),
candidate_count,
total_bytes,
});
}
return Ok(MemoryConsolidationOutput {
status: "skipped_below_threshold".into(),
summary: format!(
@@ -18288,7 +18519,8 @@ fn start_subjektiv_staging_consolidation(
let runtime_id = select_memory_consolidation_runtime(&api)?;
let singleton_key = format!("{SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX}{subject_id}");
let input_content = format!(
"Process the delegated subject's {candidate_count} pending Memory candidate(s) ({total_bytes} bytes) with MemoryStagingList, MemoryStagingRead, subject Memory reads, and MemoryApplyCandidate."
"Process the delegated subject's {candidate_count} pending Memory candidate(s) ({total_bytes} bytes) with MemoryStagingList, MemoryStagingRead, subject Memory reads, and MemoryApplyCandidate. After this committed consolidation turn, the Host will rebuild the {:?} resident surface from confirmed Memory in a separate clean context.",
surface_availability
);
if let Some(output) = try_reuse_subject_consolidation_worker(
&api,
@@ -29919,6 +30151,50 @@ mod tests {
bytes
}
#[test]
fn resident_summary_transport_preserves_ready_empty_state() {
let output = resident_summary_output(crate::subjektiv::ResidentSurface {
availability: crate::subjektiv::SurfaceAvailability::Ready,
snapshot: Some(crate::subjektiv::SurfaceSnapshot {
schema_version: 1,
id: "surface-empty".into(),
subject_id: "subject-a".into(),
body_md: String::new(),
memory_refs: Vec::new(),
built_from_store_revision: 7,
created_at: "2026-10-02T00:00:00Z".into(),
}),
});
assert_eq!(
output.availability,
memory::backend::MemoryResidentSummaryAvailability::Ready
);
assert_eq!(output.content, None);
for (surface, expected) in [
(
crate::subjektiv::SurfaceAvailability::Ungenerated,
memory::backend::MemoryResidentSummaryAvailability::Ungenerated,
),
(
crate::subjektiv::SurfaceAvailability::Stale,
memory::backend::MemoryResidentSummaryAvailability::Stale,
),
(
crate::subjektiv::SurfaceAvailability::Failed,
memory::backend::MemoryResidentSummaryAvailability::Failed,
),
] {
let output = resident_summary_output(crate::subjektiv::ResidentSurface {
availability: surface,
snapshot: None,
});
assert_eq!(output.availability, expected);
assert_eq!(output.content, None);
}
}
#[test]
fn external_permission_levels_map_to_existing_bits_and_reject_legacy_partial_grants() {
let bits = |capabilities| {
File diff suppressed because it is too large Load Diff