diff --git a/crates/manifest/src/config.rs b/crates/manifest/src/config.rs index 6a6e6e96..d5ae7049 100644 --- a/crates/manifest/src/config.rs +++ b/crates/manifest/src/config.rs @@ -18,11 +18,11 @@ use crate::model::{AuthRef, ModelManifest, ReasoningControl}; use crate::plugin::PluginConfig; use crate::{ CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits, - McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryExtractionProfileConfig, - MemoryFeatureProfileConfig, MemoryResidentProfileConfig, MergeRequestFeatureConfig, - ResolvedMemoryFeatureConfig, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureConfig, - ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerFeatureConfig, - WorkerManifest, WorkerMeta, + McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConsolidationProfileConfig, + MemoryExtractionProfileConfig, MemoryFeatureProfileConfig, MemoryResidentProfileConfig, + MergeRequestFeatureConfig, ResolvedMemoryFeatureConfig, ScopeConfig, SessionConfig, + SkillsConfig, TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, + WebConfig, WorkerFeatureConfig, WorkerManifest, WorkerMeta, }; /// Partial-form Worker manifest. Every field is optional; one or more @@ -201,6 +201,8 @@ pub struct MemoryFeatureConfigPartial { pub resident: Option, #[serde(default)] pub extraction: Option, + #[serde(default)] + pub consolidation: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -223,6 +225,13 @@ pub struct MemoryExtractionProfileConfigPartial { pub worker_max_turns: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MemoryConsolidationProfileConfigPartial { + #[serde(default)] + pub request_enabled: Option, +} + impl MemoryFeatureConfigPartial { fn merge(self, other: Self) -> Self { Self { @@ -238,6 +247,11 @@ impl MemoryFeatureConfigPartial { other.extraction, MemoryExtractionProfileConfigPartial::merge, ), + consolidation: merge_option( + self.consolidation, + other.consolidation, + MemoryConsolidationProfileConfigPartial::merge, + ), } } } @@ -305,6 +319,14 @@ impl MergeRequestFeatureConfigPartial { } } +impl MemoryConsolidationProfileConfigPartial { + fn merge(self, other: Self) -> Self { + Self { + request_enabled: other.request_enabled.or(self.request_enabled), + } + } +} + impl From for FeatureConfig { fn from(value: FeatureConfigPartial) -> Self { Self { @@ -385,6 +407,7 @@ impl From for ResolvedMemoryFeatureConfig { fn from(value: MemoryFeatureConfigPartial) -> Self { let resident = value.resident.unwrap_or_default(); let extraction = value.extraction.unwrap_or_default(); + let consolidation = value.consolidation.unwrap_or_default(); Self { profile: MemoryFeatureProfileConfig { enabled: value.enabled.unwrap_or_default(), @@ -400,6 +423,9 @@ impl From for ResolvedMemoryFeatureConfig { .worker_max_turns .or(defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS), }, + consolidation: MemoryConsolidationProfileConfig { + request_enabled: consolidation.request_enabled.unwrap_or(true), + }, }, workspace_settings: None, } @@ -420,6 +446,9 @@ impl From for MemoryFeatureConfigPartial { threshold: value.profile.extraction.threshold, worker_max_turns: value.profile.extraction.worker_max_turns, }), + consolidation: Some(MemoryConsolidationProfileConfigPartial { + request_enabled: Some(value.profile.consolidation.request_enabled), + }), } } } @@ -1899,6 +1928,9 @@ inject_summary = false enabled = true threshold = 42000 worker_max_turns = 2 + +[feature.memory.consolidation] +request_enabled = false "#, ) .unwrap(); @@ -1906,6 +1938,7 @@ worker_max_turns = 2 assert_eq!(memory.enabled, Some(true)); assert_eq!(memory.staging_tools, Some(false)); assert_eq!(memory.resident.unwrap().inject_summary, Some(false)); + assert_eq!(memory.consolidation.unwrap().request_enabled, Some(false)); let extraction = memory.extraction.unwrap(); assert_eq!(extraction.enabled, Some(true)); assert_eq!(extraction.threshold, Some(42_000)); diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index 0e892677..5ebdc72f 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -226,6 +226,7 @@ pub struct MemoryFeatureProfileConfig { pub staging_tools: bool, pub resident: MemoryResidentProfileConfig, pub extraction: MemoryExtractionProfileConfig, + pub consolidation: MemoryConsolidationProfileConfig, } impl MemoryFeatureProfileConfig { @@ -248,6 +249,7 @@ impl Default for MemoryFeatureProfileConfig { staging_tools: false, resident: MemoryResidentProfileConfig::default(), extraction: MemoryExtractionProfileConfig::default(), + consolidation: MemoryConsolidationProfileConfig::default(), } } } @@ -286,6 +288,20 @@ impl Default for MemoryExtractionProfileConfig { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default, deny_unknown_fields)] +pub struct MemoryConsolidationProfileConfig { + pub request_enabled: bool, +} + +impl Default for MemoryConsolidationProfileConfig { + fn default() -> Self { + Self { + request_enabled: true, + } + } +} + /// Immutable Memory execution configuration persisted in a resolved Worker Manifest. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(default, deny_unknown_fields)] @@ -1070,6 +1086,31 @@ fn migrate_legacy_resolved_manifest_snapshot( .cloned() .unwrap_or(serde_json::Value::Null); let extraction_enabled = !extraction_threshold.is_null(); + if legacy_memory + .get("consolidation_model") + .is_some_and(|model| !model.is_null()) + { + return Err(serde_json::Error::io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "legacy resolved Worker manifest uses a Worker-owned consolidation model that cannot be migrated to Backend authority", + ))); + } + let threshold_files = legacy_memory + .get("consolidation_threshold_files") + .and_then(serde_json::Value::as_u64); + let threshold_bytes = legacy_memory + .get("consolidation_threshold_bytes") + .and_then(serde_json::Value::as_u64); + let consolidation_enabled = match (threshold_files, threshold_bytes) { + (None, None) => false, + (Some(5), Some(50_000)) => true, + _ => { + return Err(serde_json::Error::io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "legacy resolved Worker manifest uses custom consolidation thresholds that cannot be migrated to Backend policy", + ))); + } + }; let mut resolved = serde_json::json!({ "profile": { "enabled": enabled, @@ -1089,6 +1130,9 @@ fn migrate_legacy_resolved_manifest_snapshot( .cloned() .unwrap_or(serde_json::Value::Null), }, + "consolidation": { + "request_enabled": consolidation_enabled, + }, }, }); if let Some(workspace_settings) = workspace_settings { @@ -1490,7 +1534,8 @@ model_id = "claude-sonnet-4-20250514" "language": "Français", "extract_threshold": 1234, "extract_worker_max_turns": 3, - "consolidation_threshold_files": 99, + "consolidation_threshold_files": 5, + "consolidation_threshold_bytes": 50000, }); let migrated = read_persisted_worker_manifest_snapshot(manifest).unwrap(); @@ -1501,6 +1546,14 @@ model_id = "claude-sonnet-4-20250514" migrated.feature.memory.profile.extraction.threshold, Some(1234) ); + assert!( + migrated + .feature + .memory + .profile + .consolidation + .request_enabled + ); assert_eq!( migrated .feature @@ -1523,6 +1576,19 @@ model_id = "claude-sonnet-4-20250514" mixed["feature"]["memory"] = serde_json::json!({ "enabled": true, "profile": {} }); mixed["memory"] = serde_json::json!({}); assert!(read_persisted_worker_manifest_snapshot(mixed).is_err()); + + let mut custom_policy = + serde_json::to_value(WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap()).unwrap(); + custom_policy["feature"]["memory"] = serde_json::json!({ "enabled": true }); + custom_policy["memory"] = serde_json::json!({ + "workspace_id": "workspace-1", + "settings_revision": 1, + "language": "English", + "consolidation_threshold_files": 99, + "consolidation_threshold_bytes": 50000, + }); + assert!(read_persisted_worker_manifest_snapshot(custom_policy).is_err()); + assert!( read_persisted_worker_manifest_snapshot(serde_json::json!({ "schema_version": 3, diff --git a/crates/worker/src/feature/builtin/memory_lifecycle.rs b/crates/worker/src/feature/builtin/memory_lifecycle.rs index f2d0caab..7349f842 100644 --- a/crates/worker/src/feature/builtin/memory_lifecycle.rs +++ b/crates/worker/src/feature/builtin/memory_lifecycle.rs @@ -526,7 +526,9 @@ impl FeatureBackgroundTask for MemoryLifecycleTask { .await; if !cancellation.is_cancelled() { context.generation_fence.ensure_current()?; - self.request_consolidation().await; + if self.config.profile.consolidation.request_enabled { + self.request_consolidation().await; + } } extraction } @@ -1323,6 +1325,36 @@ permission = "write" ); } + #[tokio::test] + async fn lifecycle_task_does_not_request_consolidation_when_profile_disables_it() { + let client = ScriptClient::new(Vec::new()); + let extension_writes = Arc::new(Mutex::new(Vec::new())); + let (event_tx, _) = broadcast::channel(16); + let workspace_client = Arc::new(RecordingWorkspaceClient::default()); + let mut interrupted = capture(2, 250); + interrupted.run_exit = CommittedRunExit::Interrupted; + let mut task = test_task( + interrupted, + Box::new(client), + extension_writes, + event_tx, + workspace_client.clone(), + ); + task.config.profile.consolidation.request_enabled = false; + run_background_task(task).await; + + let requests = workspace_client.requests.lock().unwrap(); + assert!( + !requests.iter().any(|request| { + request + .body + .as_deref() + .is_some_and(|body| body == "{\"force\":false}") + }), + "recorded requests: {requests:?}" + ); + } + fn internal_result( lifecycle: WorkerRunResult, ) -> Result { diff --git a/docs/manifest.toml b/docs/manifest.toml index 2becb8bc..e7f72fd3 100644 --- a/docs/manifest.toml +++ b/docs/manifest.toml @@ -246,6 +246,9 @@ permission = "write" # # [feature.memory.profile.extraction.model] # # ref = "anthropic/claude-haiku-4-5" # +# [feature.memory.profile.consolidation] +# request_enabled = true +# # # Backendがresolved Manifestへbindする。手書き/Profile入力では指定しない。 # # [feature.memory.workspace_settings] # # workspace_id = "workspace-id" diff --git a/resources/profiles/base.dcdl b/resources/profiles/base.dcdl index 88db46c7..c6cdc32c 100644 --- a/resources/profiles/base.dcdl +++ b/resources/profiles/base.dcdl @@ -30,6 +30,7 @@ feature = { enabled = true; threshold = 50000; }; + consolidation = { request_enabled = true; }; }; web = { enabled = true; }; image = { enabled = true; };