feat: cut over Memory to subjektiv product surfaces

This commit is contained in:
2026-10-03 00:06:47 +09:00
parent 07fcc249fe
commit f4966b78ea
54 changed files with 7600 additions and 848 deletions
+93 -59
View File
@@ -784,10 +784,12 @@ impl WorkerController {
// the actual registered set instead of a hand-maintained mirror.
worker.engine().tool_server_handle().flush_pending();
// Runtime-owned fresh Workers persist their initial model-visible head
// before controller exposure. Feature installation above must happen first
// so the resident contribution is loaded into that durable prompt. Restored
// Workers retain their established first-turn refresh lifecycle.
// Runtime-owned Workers persist their current model-visible head before
// controller exposure. Feature installation above must happen first so a
// fresh subject Worker captures the current resident surface in its durable
// prompt. Restored Workers defer their one-shot append-only resident refresh
// until the next model-visible context materialization, after the Runtime has
// committed restoration of the existing durable head.
if runtime_managed && worker.needs_initial_session_head_materialization() {
worker
.materialize_durable_session_head()
@@ -1179,18 +1181,17 @@ pub(crate) fn wire_event_bridges_on_engine<C, St>(
// per-item commit channel is wired at the top of this function.
}
fn validate_automatic_memory_extraction_targets(
fn validate_memory_lifecycle_targets(
memory: &manifest::MemoryFeatureProfileConfig,
subjektiv: &manifest::SubjektivFeatureProfileConfig,
) -> std::io::Result<()> {
if memory.enabled
&& memory.extraction.enabled
&& subjektiv.enabled
&& subjektiv.extraction.enabled
{
let legacy_lifecycle =
memory.enabled && (memory.extraction.enabled || memory.consolidation.request_enabled);
let subjektiv_lifecycle = subjektiv.enabled && subjektiv.extraction.enabled;
if legacy_lifecycle && subjektiv_lifecycle {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Workspace Memory and subjektiv automatic extraction cannot both be enabled for one Worker",
"Workspace Memory and subjektiv lifecycles cannot both be enabled for one Worker",
));
}
Ok(())
@@ -1307,56 +1308,75 @@ where
}
let memory_profile = &worker.manifest().feature.memory.profile;
let subjektiv_profile = &worker.manifest().feature.subjektiv.profile;
validate_automatic_memory_extraction_targets(memory_profile, subjektiv_profile)?;
validate_memory_lifecycle_targets(memory_profile, subjektiv_profile)?;
let memory_install_plan = crate::feature::builtin::memory::MemoryFeatureInstallPlan::prepare(
worker.manifest(),
worker.workspace_client_handle(),
worker.prompts().load_full(),
)?;
let memory_prompt_contribution = memory_install_plan.as_ref().map(|plan| {
let mut feature_prompt_contribution = memory_install_plan.as_ref().map(|plan| {
(
plan.resident_summary_source.clone(),
plan.system_prompt_override.clone(),
)
});
let memory_lifecycle_config = memory_install_plan
.as_ref()
.map(|plan| plan.resolved_config.clone());
let memory_lifecycle_config = worker.manifest().feature.memory.clone();
if let Some(plan) = memory_install_plan {
feature_registry.add_module(plan.module);
}
if let Some(subjektiv_memory) =
crate::feature::builtin::subjektiv_memory::SubjektivMemoryFeature::from_resolved_config(
&worker.manifest().feature.subjektiv,
worker.committed_session_capture_handle(),
let subjektiv_consolidation_plan =
crate::feature::builtin::memory::SubjektivConsolidationFeatureInstallPlan::prepare(
worker.manifest(),
worker.workspace_client_handle(),
worker.prompts().load_full(),
)?;
if let Some(plan) = subjektiv_consolidation_plan {
feature_prompt_contribution = Some((None, Some(plan.system_prompt_override)));
feature_registry.add_module(plan.module);
}
let ordinary_subjektiv_features_enabled =
crate::feature::builtin::memory::ordinary_subjektiv_features_enabled(worker.manifest());
if let Some(resident_summary_source) =
crate::feature::builtin::memory::ordinary_subjektiv_resident_summary_source(
worker.manifest(),
worker.workspace_client_handle(),
)?
{
feature_prompt_contribution = Some((Some(resident_summary_source), None));
}
if ordinary_subjektiv_features_enabled
&& let Some(subjektiv_memory) =
crate::feature::builtin::subjektiv_memory::SubjektivMemoryFeature::from_resolved_config(
&worker.manifest().feature.subjektiv,
worker.committed_session_capture_handle(),
worker.workspace_client_handle(),
)?
{
feature_registry.add_module(subjektiv_memory);
}
if let Some(subjektiv_sessions) =
crate::feature::builtin::subjektiv_session::SubjektivSessionFeature::from_resolved_config(
&worker.manifest().feature.subjektiv,
worker.committed_session_capture_handle(),
worker.workspace_client_handle(),
)?
if ordinary_subjektiv_features_enabled
&& let Some(subjektiv_sessions) =
crate::feature::builtin::subjektiv_session::SubjektivSessionFeature::from_resolved_config(
&worker.manifest().feature.subjektiv,
worker.committed_session_capture_handle(),
worker.workspace_client_handle(),
)?
{
feature_registry.add_module(subjektiv_sessions);
}
if let Some(memory_config) = memory_lifecycle_config
&& let Some(memory_lifecycle) =
crate::feature::builtin::memory_lifecycle::MemoryLifecycleFeature::from_resolved_config(
worker.manifest_lifecycle_features_enabled(),
memory_config,
worker.committed_session_capture_handle(),
worker.session_extension_handle(),
worker.workspace_client_handle(),
spawner_manifest.clone(),
worker.llm_client_handle(),
prompts.clone(),
spawner_workspace_context.clone(),
worker.working_event_sender(),
)?
if let Some(memory_lifecycle) =
crate::feature::builtin::memory_lifecycle::MemoryLifecycleFeature::from_resolved_config(
worker.manifest_lifecycle_features_enabled(),
memory_lifecycle_config,
worker.committed_session_capture_handle(),
worker.session_extension_handle(),
worker.workspace_client_handle(),
spawner_manifest.clone(),
worker.llm_client_handle(),
prompts.clone(),
spawner_workspace_context.clone(),
worker.working_event_sender(),
)?
{
feature_registry.add_module(memory_lifecycle);
}
@@ -1370,19 +1390,20 @@ where
)? {
feature_registry.add_module(surface_lifecycle);
}
if let Some(subjektiv_lifecycle) =
crate::feature::builtin::memory_lifecycle::SubjektivLifecycleFeature::from_resolved_config(
worker.manifest_lifecycle_features_enabled(),
worker.manifest().feature.subjektiv.clone(),
worker.committed_session_capture_handle(),
worker.session_extension_handle(),
worker.workspace_client_handle(),
spawner_manifest.clone(),
worker.llm_client_handle(),
prompts.clone(),
spawner_workspace_context.clone(),
worker.working_event_sender(),
)?
if ordinary_subjektiv_features_enabled
&& let Some(subjektiv_lifecycle) =
crate::feature::builtin::memory_lifecycle::SubjektivLifecycleFeature::from_resolved_config(
worker.manifest_lifecycle_features_enabled(),
worker.manifest().feature.subjektiv.clone(),
worker.committed_session_capture_handle(),
worker.session_extension_handle(),
worker.workspace_client_handle(),
spawner_manifest.clone(),
worker.llm_client_handle(),
prompts.clone(),
spawner_workspace_context.clone(),
worker.working_event_sender(),
)?
{
feature_registry.add_module(subjektiv_lifecycle);
}
@@ -1592,7 +1613,7 @@ where
),
));
}
if let Some((resident_summary, system_prompt_override)) = memory_prompt_contribution {
if let Some((resident_summary, system_prompt_override)) = feature_prompt_contribution {
worker.install_system_prompt_contribution(resident_summary, system_prompt_override);
}
worker.attach_tracker(tracker);
@@ -3570,9 +3591,17 @@ mod tests {
let exposure = startup
.find("let handle = WorkerHandle")
.expect("controller handle exposure");
let materialize_gate = startup[..materialize]
.rfind("if runtime_managed && worker.needs_initial_session_head_materialization() {")
.expect("fresh runtime-managed materialization gate");
assert!(feature_install < materialize);
assert!(feature_install < materialize_gate);
assert!(materialize_gate < materialize);
assert!(materialize < exposure);
assert!(
startup[materialize_gate..materialize]
.contains("needs_initial_session_head_materialization")
);
}
#[test]
@@ -3639,7 +3668,7 @@ mod tests {
}
#[test]
fn automatic_memory_extraction_destinations_are_mutually_exclusive() {
fn legacy_and_subjektiv_lifecycles_are_mutually_exclusive() {
let mut memory = manifest::MemoryFeatureProfileConfig::default();
memory.enabled = true;
memory.extraction.enabled = true;
@@ -3647,12 +3676,17 @@ mod tests {
subjektiv.enabled = true;
subjektiv.extraction.enabled = true;
let error = validate_automatic_memory_extraction_targets(&memory, &subjektiv).unwrap_err();
let error = validate_memory_lifecycle_targets(&memory, &subjektiv).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("cannot both be enabled"));
subjektiv.extraction.enabled = false;
validate_automatic_memory_extraction_targets(&memory, &subjektiv).unwrap();
memory.extraction.enabled = false;
assert!(
validate_memory_lifecycle_targets(&memory, &subjektiv).is_err(),
"legacy consolidation still installs the legacy lifecycle"
);
memory.consolidation.request_enabled = false;
validate_memory_lifecycle_targets(&memory, &subjektiv).unwrap();
}
#[test]
+470 -32
View File
@@ -650,6 +650,22 @@ struct WorkspaceResidentSummarySource {
client: Arc<dyn WorkspaceClient>,
}
struct WorkspaceSubjektivResidentSummarySource {
client: Arc<dyn WorkspaceClient>,
}
const READY_EMPTY_RESIDENT_SURFACE: &str =
"The current ready resident memory surface is intentionally empty.";
fn resident_summary_contribution(
output: memory::backend::MemoryResidentSummaryOutput,
) -> SystemPromptContribution {
if output.availability != memory::backend::MemoryResidentSummaryAvailability::Ready {
return SystemPromptContribution::Unavailable;
}
SystemPromptContribution::Ready(output.content.unwrap_or_default())
}
#[async_trait]
impl SystemPromptContributionSource for WorkspaceResidentSummarySource {
async fn load(&self) -> SystemPromptContribution {
@@ -662,14 +678,8 @@ impl SystemPromptContributionSource for WorkspaceResidentSummarySource {
)
.await
{
Ok(memory::backend::MemoryBackendOperationResult::ResidentSummary(output))
if output.availability
== memory::backend::MemoryResidentSummaryAvailability::Ready =>
{
SystemPromptContribution::Ready(output.content.unwrap_or_default())
}
Ok(memory::backend::MemoryBackendOperationResult::ResidentSummary(_)) => {
SystemPromptContribution::Unavailable
Ok(memory::backend::MemoryBackendOperationResult::ResidentSummary(output)) => {
resident_summary_contribution(output)
}
Ok(other) => {
tracing::debug!(?other, "unexpected resident Memory Backend result");
@@ -683,11 +693,39 @@ impl SystemPromptContributionSource for WorkspaceResidentSummarySource {
}
}
#[async_trait]
impl SystemPromptContributionSource for WorkspaceSubjektivResidentSummarySource {
async fn load(&self) -> SystemPromptContribution {
match execute_subjektiv_memory_operation(
self.client.as_ref(),
server_api::SubjektivMemoryBackendOperation::ResidentSummary(Default::default()),
)
.await
{
Ok(server_api::SubjektivMemoryBackendResponse::ResidentSummary(output)) => {
match resident_summary_contribution(output) {
SystemPromptContribution::Ready(content) if content.trim().is_empty() => {
SystemPromptContribution::Ready(READY_EMPTY_RESIDENT_SURFACE.to_string())
}
contribution => contribution,
}
}
Ok(other) => {
tracing::debug!(?other, "unexpected subject resident Memory response");
SystemPromptContribution::Unavailable
}
Err(error) => {
tracing::debug!(%error, "subject resident Memory unavailable");
SystemPromptContribution::Unavailable
}
}
}
}
pub(crate) struct MemoryFeatureInstallPlan {
pub(crate) module: MemoryToolsFeature,
pub(crate) resident_summary_source: Option<Arc<dyn SystemPromptContributionSource>>,
pub(crate) system_prompt_override: Option<String>,
pub(crate) resolved_config: manifest::ResolvedMemoryFeatureConfig,
}
impl MemoryFeatureInstallPlan {
@@ -720,11 +758,7 @@ impl MemoryFeatureInstallPlan {
} => Some(name.as_str()),
_ => None,
});
let memory_consolidation_worker = matches!(
profile_name,
Some("memory-consolidation" | "subjektiv-memory-consolidation")
);
let subjektiv_consolidation_worker = profile_name == Some("subjektiv-memory-consolidation");
let memory_consolidation_worker = profile_name == Some("memory-consolidation");
config
.validate_execution()
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
@@ -758,31 +792,170 @@ impl MemoryFeatureInstallPlan {
let system_prompt_override = if memory_consolidation_worker {
let language = settings.language;
Some(
if subjektiv_consolidation_worker {
prompts.subjektiv_memory_consolidation_system(&language)
} else {
prompts.memory_consolidation_system(&language)
}
.map_err(|error| std::io::Error::other(error.to_string()))?,
prompts
.memory_consolidation_system(&language)
.map_err(|error| std::io::Error::other(error.to_string()))?,
)
} else {
None
};
let module = if subjektiv_consolidation_worker {
MemoryToolsFeature::from_tools(workspace_http_subjektiv_consolidation_tools(client))
} else {
MemoryToolsFeature::new(client, config.profile.staging_tools)
};
let module = MemoryToolsFeature::new(client, config.profile.staging_tools);
Ok(Some(Self {
module,
resident_summary_source,
system_prompt_override,
resolved_config: config,
}))
}
}
pub(crate) fn is_builtin_subjektiv_consolidation_profile(
manifest: &manifest::WorkerManifest,
) -> bool {
manifest.profile.as_ref().is_some_and(|snapshot| {
matches!(
&snapshot.source,
manifest::ProfileSource::Registry {
source: manifest::ProfileRegistrySource::Builtin,
name,
..
} if name == "subjektiv-memory-consolidation"
)
})
}
pub(crate) fn ordinary_subjektiv_features_enabled(manifest: &manifest::WorkerManifest) -> bool {
manifest.feature.subjektiv.profile.enabled
&& !is_builtin_subjektiv_consolidation_profile(manifest)
}
pub(crate) fn ordinary_subjektiv_resident_summary_source(
manifest: &manifest::WorkerManifest,
client: Arc<dyn WorkspaceClient>,
) -> std::io::Result<Option<Arc<dyn SystemPromptContributionSource>>> {
if !ordinary_subjektiv_features_enabled(manifest) {
return Ok(None);
}
manifest
.feature
.subjektiv
.validate_execution()
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
if !client.is_available() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"subjektiv resident Memory requires Backend Workspace API authority",
));
}
let workspace_id = client.workspace_id().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"subjektiv resident Memory requires Backend Workspace API authority",
)
})?;
let settings = manifest
.feature
.subjektiv
.workspace_settings()
.expect("validated enabled subjektiv config has Workspace settings");
if settings.workspace_id != workspace_id {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"subjektiv Workspace settings belong to {} instead of {}",
settings.workspace_id, workspace_id
),
));
}
Ok(Some(Arc::new(WorkspaceSubjektivResidentSummarySource {
client,
})))
}
pub(crate) struct SubjektivConsolidationFeatureInstallPlan {
pub(crate) module: SubjektivConsolidationToolsFeature,
pub(crate) system_prompt_override: String,
}
impl SubjektivConsolidationFeatureInstallPlan {
pub(crate) fn prepare(
manifest: &manifest::WorkerManifest,
client: Arc<dyn WorkspaceClient>,
prompts: Arc<crate::prompt::catalog::PromptCatalog>,
) -> std::io::Result<Option<Self>> {
if !is_builtin_subjektiv_consolidation_profile(manifest) {
return Ok(None);
}
let config = &manifest.feature.subjektiv;
config
.validate_execution()
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
if !config.profile.enabled || config.profile.extraction.enabled {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"subjektiv consolidation profile requires subject settings without automatic extraction",
));
}
let workspace_id = client.workspace_id().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"subjektiv consolidation tools require Backend Workspace API authority",
)
})?;
let settings = config
.workspace_settings()
.expect("validated enabled subjektiv config has Workspace settings");
if settings.workspace_id != workspace_id {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"subjektiv settings belong to {} instead of {}",
settings.workspace_id, workspace_id
),
));
}
let system_prompt_override = prompts
.subjektiv_memory_consolidation_system(&settings.language)
.map_err(|error| std::io::Error::other(error.to_string()))?;
Ok(Some(Self {
module: SubjektivConsolidationToolsFeature {
tools: workspace_http_subjektiv_consolidation_tools(client),
},
system_prompt_override,
}))
}
}
#[derive(Clone)]
pub(crate) struct SubjektivConsolidationToolsFeature {
tools: Vec<ToolDefinition>,
}
impl FeatureModule for SubjektivConsolidationToolsFeature {
fn descriptor(&self) -> FeatureDescriptor {
let mut descriptor = FeatureDescriptor::builtin(
"subjektiv-memory-consolidation",
"subjektiv Memory Consolidation",
)
.with_description("Dedicated subject Memory staging consolidation tools.");
for tool in &self.tools {
let (meta, _) = tool();
descriptor = descriptor.with_tool(ToolDeclaration::new(meta.name, meta.description));
}
descriptor
}
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
for tool in &self.tools {
let (meta, _) = tool();
context
.tools()
.register(ToolContribution::new(meta.name, tool.clone()))?;
}
Ok(())
}
}
#[derive(Clone)]
pub struct MemoryToolsFeature {
tools: Vec<ToolDefinition>,
@@ -797,10 +970,6 @@ impl MemoryToolsFeature {
};
Self { tools }
}
fn from_tools(tools: Vec<ToolDefinition>) -> Self {
Self { tools }
}
}
impl FeatureModule for MemoryToolsFeature {
@@ -828,7 +997,9 @@ impl FeatureModule for MemoryToolsFeature {
#[cfg(test)]
mod tests {
use super::*;
use crate::WorkspaceResponse;
use agen::tool::ToolDefinition;
use std::sync::Mutex;
fn test_client() -> Arc<dyn WorkspaceClient> {
Arc::new(crate::worker::TestWorkspaceHttpClient::new(
@@ -837,13 +1008,100 @@ mod tests {
))
}
#[derive(Debug)]
struct SubjectResidentClient {
availability: memory::backend::MemoryResidentSummaryAvailability,
content: Option<String>,
scope_allowed: bool,
paths: Mutex<Vec<String>>,
}
impl SubjectResidentClient {
fn new(
availability: memory::backend::MemoryResidentSummaryAvailability,
content: Option<&str>,
) -> Self {
Self {
availability,
content: content.map(str::to_string),
scope_allowed: true,
paths: Mutex::new(Vec::new()),
}
}
fn denied() -> Self {
Self {
availability: memory::backend::MemoryResidentSummaryAvailability::Ready,
content: Some("must not be injected".into()),
scope_allowed: false,
paths: Mutex::new(Vec::new()),
}
}
}
impl WorkspaceClient for SubjectResidentClient {
fn workspace_id(&self) -> Option<&str> {
Some("workspace")
}
fn kind(&self) -> &str {
"subject-resident-test"
}
fn is_available(&self) -> bool {
true
}
fn execute(
&self,
request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
self.paths.lock().unwrap().push(request.path.clone());
assert!(request.path.ends_with("/subjektiv/memory"));
let request: server_api::SubjektivMemoryBackendRequest =
serde_json::from_str(request.body.as_deref().unwrap_or_default()).unwrap();
assert!(matches!(
request.operation,
server_api::SubjektivMemoryBackendOperation::ResidentSummary(_)
));
if !self.scope_allowed {
return Ok(WorkspaceResponse {
status: 403,
body: "subject scope unavailable".into(),
});
}
Ok(WorkspaceResponse {
status: 200,
body: serde_json::to_string(
&server_api::SubjektivMemoryBackendResponse::ResidentSummary(
memory::backend::MemoryResidentSummaryOutput {
availability: self.availability,
content: self.content.clone(),
},
),
)
.unwrap(),
})
}
}
fn resident_client(content: &str) -> Arc<dyn WorkspaceClient> {
resident_client_state(
memory::backend::MemoryResidentSummaryAvailability::Ready,
Some(content),
)
}
fn resident_client_state(
availability: memory::backend::MemoryResidentSummaryAvailability,
content: Option<&str>,
) -> Arc<dyn WorkspaceClient> {
use std::io::{Read, Write};
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let content = content.to_string();
let content = content.map(str::to_string);
std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = [0_u8; 1024];
@@ -852,7 +1110,7 @@ mod tests {
"status": "ok",
"result": {
"kind": "resident_summary",
"availability": "ready",
"availability": availability,
"content": content,
}
})
@@ -989,6 +1247,60 @@ mod tests {
);
}
#[test]
fn dedicated_subjektiv_consolidator_uses_subject_settings_and_only_consolidation_tools() {
let workspace = tempfile::tempdir().unwrap();
let mut manifest = manifest::ProfileResolver::new()
.with_workspace_base(workspace.path())
.resolve(
&manifest::ProfileSelector::source_named(
manifest::ProfileRegistrySource::Builtin,
"subjektiv-memory-consolidation",
),
manifest::ProfileResolveOptions::with_worker_name("subject-consolidator"),
)
.unwrap()
.manifest;
manifest
.feature
.subjektiv
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
workspace_id: "workspace".into(),
settings_revision: 1,
language: "English".into(),
})
.unwrap();
assert!(!manifest.feature.memory.profile.enabled);
assert!(!ordinary_subjektiv_features_enabled(&manifest));
assert!(
MemoryFeatureInstallPlan::prepare(
&manifest,
test_client(),
crate::prompt::catalog::PromptCatalog::builtins_only().unwrap(),
)
.unwrap()
.is_none()
);
let plan = SubjektivConsolidationFeatureInstallPlan::prepare(
&manifest,
test_client(),
crate::prompt::catalog::PromptCatalog::builtins_only().unwrap(),
)
.unwrap()
.unwrap();
assert_eq!(
plan.module.descriptor().id.as_str(),
"builtin:subjektiv-memory-consolidation"
);
let names = tool_names(plan.module.tools);
assert!(names.contains(&"SubjektivMemoryQuery".to_string()));
assert!(names.contains(&"MemoryApplyCandidate".to_string()));
assert!(!names.contains(&"MemoryQuery".to_string()));
assert!(!names.contains(&"SubjektivRemember".to_string()));
}
#[tokio::test]
async fn memory_prompt_contribution_defers_resident_summary_until_loaded() {
let prompts = crate::prompt::catalog::PromptCatalog::builtins_only().unwrap();
@@ -1029,6 +1341,132 @@ mod tests {
);
}
fn subject_manifest(workspace_id: &str) -> manifest::WorkerManifest {
let mut manifest = manifest::WorkerManifest::from_toml(
r#"
[worker]
name = "subject-worker"
scope = "main"
[model]
scheme = "anthropic"
model_id = "test-model"
[engine]
[[scope.allow]]
target = "/subject-resident-test"
permission = "write"
"#,
)
.unwrap();
manifest.feature.subjektiv.profile.enabled = true;
manifest
.feature
.subjektiv
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
workspace_id: workspace_id.to_string(),
settings_revision: 1,
language: "English".to_string(),
})
.unwrap();
manifest
}
#[tokio::test]
async fn subject_resident_source_preserves_only_current_ready_surface() {
let manifest = subject_manifest("workspace");
let ready_client = Arc::new(SubjectResidentClient::new(
memory::backend::MemoryResidentSummaryAvailability::Ready,
Some("current subject surface"),
));
let ready = ordinary_subjektiv_resident_summary_source(&manifest, ready_client.clone())
.unwrap()
.unwrap();
assert_eq!(
ready.load().await,
SystemPromptContribution::Ready("current subject surface".into())
);
assert_eq!(
ready_client.paths.lock().unwrap().clone(),
vec!["/api/w/workspace/subjektiv/memory".to_string()]
);
let ready_empty_client = Arc::new(SubjectResidentClient::new(
memory::backend::MemoryResidentSummaryAvailability::Ready,
None,
));
let ready_empty = ordinary_subjektiv_resident_summary_source(&manifest, ready_empty_client)
.unwrap()
.unwrap();
assert_eq!(
ready_empty.load().await,
SystemPromptContribution::Ready(READY_EMPTY_RESIDENT_SURFACE.into())
);
for availability in [
memory::backend::MemoryResidentSummaryAvailability::Ungenerated,
memory::backend::MemoryResidentSummaryAvailability::Stale,
memory::backend::MemoryResidentSummaryAvailability::Failed,
] {
let source = ordinary_subjektiv_resident_summary_source(
&manifest,
Arc::new(SubjectResidentClient::new(
availability,
Some("must not be injected"),
)),
)
.unwrap()
.unwrap();
assert_eq!(source.load().await, SystemPromptContribution::Unavailable);
}
}
#[tokio::test]
async fn subject_resident_source_fails_closed_without_trusted_subject_scope() {
let manifest = subject_manifest("workspace");
let client = Arc::new(SubjectResidentClient::denied());
let source = ordinary_subjektiv_resident_summary_source(&manifest, client.clone())
.unwrap()
.unwrap();
assert_eq!(source.load().await, SystemPromptContribution::Unavailable);
assert_eq!(
client.paths.lock().unwrap().clone(),
vec!["/api/w/workspace/subjektiv/memory".to_string()]
);
}
#[test]
fn subject_resident_source_requires_trusted_matching_workspace_config() {
let disabled = manifest::WorkerManifest::from_toml(
r#"
[worker]
name = "ordinary-worker"
scope = "main"
[model]
scheme = "anthropic"
model_id = "test-model"
[engine]
[[scope.allow]]
target = "/subject-resident-test"
permission = "write"
"#,
)
.unwrap();
assert!(
ordinary_subjektiv_resident_summary_source(&disabled, test_client())
.unwrap()
.is_none()
);
let foreign = subject_manifest("other-workspace");
assert!(ordinary_subjektiv_resident_summary_source(&foreign, test_client()).is_err());
}
#[test]
fn memory_feature_owns_normal_and_staging_tool_surfaces() {
let normal = MemoryToolsFeature::new(test_client(), false);
@@ -49,6 +49,35 @@ enum ExtractionTarget {
Subjektiv,
}
#[derive(Clone)]
struct LifecycleConfig {
extraction: manifest::MemoryExtractionProfileConfig,
consolidation_request_enabled: bool,
workspace_settings: Option<manifest::WorkspaceMemorySettingsSnapshot>,
}
impl LifecycleConfig {
fn from_memory(config: manifest::ResolvedMemoryFeatureConfig) -> Self {
Self {
extraction: config.profile.extraction,
consolidation_request_enabled: config.profile.consolidation.request_enabled,
workspace_settings: config.workspace_settings,
}
}
fn from_subjektiv(config: manifest::ResolvedSubjektivFeatureConfig) -> Self {
Self {
extraction: config.profile.extraction,
consolidation_request_enabled: true,
workspace_settings: config.workspace_settings,
}
}
fn workspace_settings(&self) -> Option<manifest::WorkspaceMemorySettingsSnapshot> {
self.workspace_settings.clone()
}
}
/// Parent-Worker lifecycle Feature that observes committed runs and schedules
/// bounded extraction work. It owns the Memory pointer, audit, restricted
/// Internal Worker, and staging disposition; Worker core owns only generic
@@ -67,7 +96,7 @@ pub(crate) struct SubjektivLifecycleFeature {
#[derive(Clone)]
struct MemoryLifecycleTask {
config: manifest::ResolvedMemoryFeatureConfig,
config: LifecycleConfig,
target: ExtractionTarget,
capture: CommittedSessionCaptureHandle,
extensions: SessionExtensionHandle,
@@ -141,7 +170,7 @@ impl MemoryLifecycleFeature {
) -> Self {
Self {
task: MemoryLifecycleTask {
config,
config: LifecycleConfig::from_memory(config),
target: ExtractionTarget::WorkspaceMemory,
capture,
extensions,
@@ -170,7 +199,7 @@ impl SubjektivLifecycleFeature {
workspace_context: WorkerWorkspaceContext,
event_tx: Option<broadcast::Sender<Event>>,
) -> std::io::Result<Option<Self>> {
if !lifecycle_enabled || !config.profile.enabled {
if !lifecycle_enabled || !config.profile.enabled || !config.profile.extraction.enabled {
return Ok(None);
}
config
@@ -182,23 +211,9 @@ impl SubjektivLifecycleFeature {
"subjektiv extraction requires Backend Workspace API authority",
));
}
let memory_config = manifest::ResolvedMemoryFeatureConfig {
profile: manifest::MemoryFeatureProfileConfig {
enabled: true,
staging_tools: false,
resident: manifest::MemoryResidentProfileConfig {
inject_summary: false,
},
extraction: config.profile.extraction,
consolidation: manifest::MemoryConsolidationProfileConfig {
request_enabled: false,
},
},
workspace_settings: config.workspace_settings,
};
Ok(Some(Self {
task: MemoryLifecycleTask {
config: memory_config,
config: LifecycleConfig::from_subjektiv(config),
target: ExtractionTarget::Subjektiv,
capture,
extensions,
@@ -282,10 +297,10 @@ impl MemoryLifecycleTask {
fn extraction_manifest(&self) -> WorkerManifest {
let mut manifest = self.manifest.clone();
if let Some(model) = self.config.profile.extraction.model.clone() {
if let Some(model) = self.config.extraction.model.clone() {
manifest.model = model;
}
if let Some(reasoning) = self.config.profile.extraction.reasoning.clone() {
if let Some(reasoning) = self.config.extraction.reasoning.clone() {
manifest.engine.reasoning = Some(reasoning);
}
manifest
@@ -301,14 +316,13 @@ impl MemoryLifecycleTask {
memory::audit::AuditWorker::MemoryExtract,
memory::audit::AuditTrigger::TokenThreshold,
self.config
.profile
.extraction
.model
.as_ref()
.or(Some(&self.manifest.model))
.map(model_audit_from_manifest),
)
.with_memory_settings(&self.config);
.with_workspace_settings(self.config.workspace_settings.as_ref());
let audit = if self.target == ExtractionTarget::Subjektiv {
audit.without_legacy_backend()
} else {
@@ -364,7 +378,6 @@ impl MemoryLifecycleTask {
};
let Some(threshold) = self
.config
.profile
.extraction
.threshold
.filter(|threshold| *threshold > 0)
@@ -470,7 +483,7 @@ impl MemoryLifecycleTask {
capture.session_id.clone(),
),
};
let client = if let Some(model) = self.config.profile.extraction.model.as_ref() {
let client = if let Some(model) = self.config.extraction.model.as_ref() {
match crate::model_client::build_client(model) {
Ok(client) => client,
Err(error) => {
@@ -534,7 +547,6 @@ impl MemoryLifecycleTask {
cache_key: Some(capture.segment_id.clone()),
max_turns: self
.config
.profile
.extraction
.worker_max_turns
.or(manifest::defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS),
@@ -710,7 +722,7 @@ impl FeatureBackgroundTask for MemoryLifecycleTask {
context: BackgroundTaskContext,
cancellation: BackgroundTaskCancellation,
) -> Result<(), HookError> {
let extraction = if self.config.profile.extraction.enabled {
let extraction = if self.config.extraction.enabled {
self.run_extraction(context.clone(), cancellation.clone())
.await
} else {
@@ -719,9 +731,7 @@ impl FeatureBackgroundTask for MemoryLifecycleTask {
if !cancellation.is_cancelled() {
context.generation_fence.ensure_current()?;
match self.target {
ExtractionTarget::WorkspaceMemory
if self.config.profile.consolidation.request_enabled =>
{
ExtractionTarget::WorkspaceMemory if self.config.consolidation_request_enabled => {
self.request_consolidation(false).await;
}
ExtractionTarget::Subjektiv => {
@@ -770,7 +780,7 @@ impl MemoryLifecycleTask {
memory::audit::AuditTrigger::StagingBacklog,
Some(model_audit_from_manifest(&self.manifest.model)),
)
.with_memory_settings(&self.config);
.with_workspace_settings(self.config.workspace_settings.as_ref());
let operation = memory::backend::MemoryConsolidateStagingOperation { force: false };
let request = if subjektiv {
self.workspace_client
@@ -981,15 +991,15 @@ impl WorkerAuditBase {
self
}
fn with_memory_settings(mut self, config: &manifest::ResolvedMemoryFeatureConfig) -> Self {
self.memory_settings =
config
.workspace_settings()
.map(|snapshot| memory::audit::MemorySettingsAudit {
workspace_id: snapshot.workspace_id,
settings_revision: snapshot.settings_revision,
language: snapshot.language,
});
fn with_workspace_settings(
mut self,
settings: Option<&manifest::WorkspaceMemorySettingsSnapshot>,
) -> Self {
self.memory_settings = settings.map(|snapshot| memory::audit::MemorySettingsAudit {
workspace_id: snapshot.workspace_id.clone(),
settings_revision: snapshot.settings_revision,
language: snapshot.language.clone(),
});
self
}
@@ -1407,7 +1417,7 @@ permission = "write"
Ok(true)
});
MemoryLifecycleTask {
config: test_config(),
config: LifecycleConfig::from_memory(test_config()),
target: ExtractionTarget::WorkspaceMemory,
capture: capture_handle,
extensions,
@@ -1483,6 +1493,35 @@ permission = "write"
assert!(feature.is_none());
}
#[test]
fn subjektiv_without_extraction_installs_no_session_lifecycle() {
let capture = CommittedSessionCaptureHandle::new(|| {
panic!("subjektiv without extraction must not capture a Session")
});
let extensions = SessionExtensionHandle::new(|_, _, _| {
panic!("subjektiv without extraction must not write a pointer")
});
let mut config = manifest::ResolvedSubjektivFeatureConfig::default();
config.profile.enabled = true;
config.profile.extraction.enabled = false;
let feature = SubjektivLifecycleFeature::from_resolved_config(
true,
config,
capture,
extensions,
crate::worker::marker_workspace_client(None, "extraction-disabled"),
test_manifest(),
Box::new(ScriptClient::new(Vec::new())),
Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())),
WorkerWorkspaceContext::no_workspace(),
None,
)
.unwrap();
assert!(feature.is_none());
}
#[test]
fn child_host_suppresses_enabled_subjektiv_lifecycle_without_capture_side_effects() {
let capture = CommittedSessionCaptureHandle::new(|| {
@@ -1536,12 +1575,11 @@ permission = "write"
event_tx,
workspace_client,
);
task.config.profile.extraction.model = Some(manifest::ModelManifest {
task.config.extraction.model = Some(manifest::ModelManifest {
ref_: Some("codex-oauth/gpt-5.6-luna".to_string()),
..Default::default()
});
task.config.profile.extraction.reasoning =
Some(ReasoningControl::Effort(ReasoningEffort::Medium));
task.config.extraction.reasoning = Some(ReasoningControl::Effort(ReasoningEffort::Medium));
let manifest = task.extraction_manifest();
assert_eq!(
@@ -1658,7 +1696,7 @@ permission = "write"
workspace_client.clone(),
);
task.target = ExtractionTarget::Subjektiv;
task.config.profile.consolidation.request_enabled = false;
task.config.consolidation_request_enabled = false;
run_background_task(task).await;
let writes = extension_writes.lock().unwrap();
@@ -1711,7 +1749,7 @@ permission = "write"
workspace_client.clone(),
);
task.target = ExtractionTarget::Subjektiv;
task.config.profile.consolidation.request_enabled = false;
task.config.consolidation_request_enabled = false;
run_background_task(task).await;
let writes = extension_writes.lock().unwrap();
@@ -1747,7 +1785,7 @@ permission = "write"
workspace_client.clone(),
);
task.target = ExtractionTarget::Subjektiv;
task.config.profile.consolidation.request_enabled = false;
task.config.consolidation_request_enabled = false;
let registry = start_background_task(task);
tokio::time::timeout(Duration::from_secs(5), async {
while !workspace_client.session_started.load(Ordering::Acquire) {
@@ -1797,7 +1835,7 @@ permission = "write"
workspace_client.clone(),
);
task.target = ExtractionTarget::Subjektiv;
task.config.profile.consolidation.request_enabled = false;
task.config.consolidation_request_enabled = false;
run_background_task(task).await;
assert!(extension_writes.lock().unwrap().is_empty());
@@ -1914,7 +1952,7 @@ permission = "write"
workspace_client.clone(),
);
task.target = ExtractionTarget::Subjektiv;
task.config.profile.consolidation.request_enabled = false;
task.config.consolidation_request_enabled = false;
let registry = start_background_task(task);
tokio::time::timeout(Duration::from_secs(5), async {
while calls.load(Ordering::SeqCst) == 0 {
@@ -2011,7 +2049,7 @@ permission = "write"
event_tx,
workspace_client.clone(),
);
task.config.profile.consolidation.request_enabled = false;
task.config.consolidation_request_enabled = false;
run_background_task(task).await;
let requests = workspace_client.requests.lock().unwrap();
@@ -2041,8 +2079,8 @@ permission = "write"
event_tx,
workspace_client.clone(),
);
task.config.profile.extraction.enabled = false;
task.config.profile.consolidation.request_enabled = true;
task.config.extraction.enabled = false;
task.config.consolidation_request_enabled = true;
run_background_task(task).await;
assert!(extension_writes.lock().unwrap().is_empty());
@@ -14,7 +14,9 @@ use crate::feature::background::{
BackgroundTaskCancellation, BackgroundTaskContext, BackgroundTaskSpec, BackgroundTaskTrigger,
FeatureBackgroundTask,
};
use crate::feature::builtin::memory::WorkspaceMemoryBackendError;
use crate::feature::builtin::memory::{
WorkspaceMemoryBackendError, is_builtin_subjektiv_consolidation_profile,
};
use crate::feature::builtin::memory_surface_output::{
MemorySurfaceOutputFeature, MemorySurfaceOutputState, submit_llm_tool_definition,
};
@@ -98,19 +100,15 @@ impl SubjektivSurfaceLifecycleFeature {
prompts: Arc<ArcSwap<PromptCatalog>>,
workspace_context: WorkerWorkspaceContext,
) -> std::io::Result<Option<Self>> {
let dedicated = manifest.profile.as_ref().is_some_and(|snapshot| {
matches!(
&snapshot.source,
manifest::ProfileSource::Registry {
source: manifest::ProfileRegistrySource::Builtin,
name,
..
} if name == "subjektiv-memory-consolidation"
)
});
let dedicated = is_builtin_subjektiv_consolidation_profile(&manifest);
if !lifecycle_enabled || !dedicated {
return Ok(None);
}
manifest
.feature
.subjektiv
.validate_execution()
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
if !workspace_client.is_available() || workspace_client.workspace_id().is_none() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
@@ -260,13 +258,13 @@ impl SubjektivSurfaceLifecycleTask {
let language = self
.manifest
.feature
.memory
.subjektiv
.workspace_settings()
.map(|settings| settings.language)
.ok_or_else(|| {
SurfaceEditorFailure::editor(HookError::new(
HookErrorCategory::Internal,
"surface editor requires bound Workspace Memory settings",
"surface editor requires bound subjektiv Workspace settings",
))
})?;
let system_prompt = self
@@ -721,10 +719,10 @@ permission = "write"
"#,
)
.unwrap();
manifest.feature.memory.profile.enabled = true;
manifest.feature.subjektiv.profile.enabled = true;
manifest
.feature
.memory
.subjektiv
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
workspace_id: "workspace-1".into(),
settings_revision: 1,
+14 -5
View File
@@ -1044,16 +1044,25 @@ async fn builtin_orchestrator_exposes_worker_remove_and_workdir_delete() {
ProfileResolveOptions::with_worker_name("orchestrator-worker"),
)
.unwrap();
let settings = manifest::WorkspaceMemorySettingsSnapshot {
workspace_id: "workspace-test".to_string(),
settings_revision: 1,
language: "English".to_string(),
};
if resolved.manifest.feature.memory.enabled() {
resolved
.manifest
.feature
.memory
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
workspace_id: "workspace-test".to_string(),
settings_revision: 1,
language: "English".to_string(),
})
.bind_workspace_settings(settings.clone())
.unwrap();
}
if resolved.manifest.feature.subjektiv.enabled() {
resolved
.manifest
.feature
.subjektiv
.bind_workspace_settings(settings)
.unwrap();
}
let workspace_context =