fix: preserve Memory consolidation enablement

This commit is contained in:
2026-09-04 23:53:59 +09:00
parent 12646b6ca0
commit 4df277c81f
5 changed files with 142 additions and 7 deletions
+38 -5
View File
@@ -18,11 +18,11 @@ use crate::model::{AuthRef, ModelManifest, ReasoningControl};
use crate::plugin::PluginConfig; use crate::plugin::PluginConfig;
use crate::{ use crate::{
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits, CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryExtractionProfileConfig, McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConsolidationProfileConfig,
MemoryFeatureProfileConfig, MemoryResidentProfileConfig, MergeRequestFeatureConfig, MemoryExtractionProfileConfig, MemoryFeatureProfileConfig, MemoryResidentProfileConfig,
ResolvedMemoryFeatureConfig, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureConfig, MergeRequestFeatureConfig, ResolvedMemoryFeatureConfig, ScopeConfig, SessionConfig,
ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerFeatureConfig, SkillsConfig, TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule,
WorkerManifest, WorkerMeta, WebConfig, WorkerFeatureConfig, WorkerManifest, WorkerMeta,
}; };
/// Partial-form Worker manifest. Every field is optional; one or more /// Partial-form Worker manifest. Every field is optional; one or more
@@ -201,6 +201,8 @@ pub struct MemoryFeatureConfigPartial {
pub resident: Option<MemoryResidentProfileConfigPartial>, pub resident: Option<MemoryResidentProfileConfigPartial>,
#[serde(default)] #[serde(default)]
pub extraction: Option<MemoryExtractionProfileConfigPartial>, pub extraction: Option<MemoryExtractionProfileConfigPartial>,
#[serde(default)]
pub consolidation: Option<MemoryConsolidationProfileConfigPartial>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -223,6 +225,13 @@ pub struct MemoryExtractionProfileConfigPartial {
pub worker_max_turns: Option<u32>, pub worker_max_turns: Option<u32>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MemoryConsolidationProfileConfigPartial {
#[serde(default)]
pub request_enabled: Option<bool>,
}
impl MemoryFeatureConfigPartial { impl MemoryFeatureConfigPartial {
fn merge(self, other: Self) -> Self { fn merge(self, other: Self) -> Self {
Self { Self {
@@ -238,6 +247,11 @@ impl MemoryFeatureConfigPartial {
other.extraction, other.extraction,
MemoryExtractionProfileConfigPartial::merge, 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<FeatureConfigPartial> for FeatureConfig { impl From<FeatureConfigPartial> for FeatureConfig {
fn from(value: FeatureConfigPartial) -> Self { fn from(value: FeatureConfigPartial) -> Self {
Self { Self {
@@ -385,6 +407,7 @@ impl From<MemoryFeatureConfigPartial> for ResolvedMemoryFeatureConfig {
fn from(value: MemoryFeatureConfigPartial) -> Self { fn from(value: MemoryFeatureConfigPartial) -> Self {
let resident = value.resident.unwrap_or_default(); let resident = value.resident.unwrap_or_default();
let extraction = value.extraction.unwrap_or_default(); let extraction = value.extraction.unwrap_or_default();
let consolidation = value.consolidation.unwrap_or_default();
Self { Self {
profile: MemoryFeatureProfileConfig { profile: MemoryFeatureProfileConfig {
enabled: value.enabled.unwrap_or_default(), enabled: value.enabled.unwrap_or_default(),
@@ -400,6 +423,9 @@ impl From<MemoryFeatureConfigPartial> for ResolvedMemoryFeatureConfig {
.worker_max_turns .worker_max_turns
.or(defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS), .or(defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS),
}, },
consolidation: MemoryConsolidationProfileConfig {
request_enabled: consolidation.request_enabled.unwrap_or(true),
},
}, },
workspace_settings: None, workspace_settings: None,
} }
@@ -420,6 +446,9 @@ impl From<ResolvedMemoryFeatureConfig> for MemoryFeatureConfigPartial {
threshold: value.profile.extraction.threshold, threshold: value.profile.extraction.threshold,
worker_max_turns: value.profile.extraction.worker_max_turns, 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 enabled = true
threshold = 42000 threshold = 42000
worker_max_turns = 2 worker_max_turns = 2
[feature.memory.consolidation]
request_enabled = false
"#, "#,
) )
.unwrap(); .unwrap();
@@ -1906,6 +1938,7 @@ worker_max_turns = 2
assert_eq!(memory.enabled, Some(true)); assert_eq!(memory.enabled, Some(true));
assert_eq!(memory.staging_tools, Some(false)); assert_eq!(memory.staging_tools, Some(false));
assert_eq!(memory.resident.unwrap().inject_summary, 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(); let extraction = memory.extraction.unwrap();
assert_eq!(extraction.enabled, Some(true)); assert_eq!(extraction.enabled, Some(true));
assert_eq!(extraction.threshold, Some(42_000)); assert_eq!(extraction.threshold, Some(42_000));
+67 -1
View File
@@ -226,6 +226,7 @@ pub struct MemoryFeatureProfileConfig {
pub staging_tools: bool, pub staging_tools: bool,
pub resident: MemoryResidentProfileConfig, pub resident: MemoryResidentProfileConfig,
pub extraction: MemoryExtractionProfileConfig, pub extraction: MemoryExtractionProfileConfig,
pub consolidation: MemoryConsolidationProfileConfig,
} }
impl MemoryFeatureProfileConfig { impl MemoryFeatureProfileConfig {
@@ -248,6 +249,7 @@ impl Default for MemoryFeatureProfileConfig {
staging_tools: false, staging_tools: false,
resident: MemoryResidentProfileConfig::default(), resident: MemoryResidentProfileConfig::default(),
extraction: MemoryExtractionProfileConfig::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. /// Immutable Memory execution configuration persisted in a resolved Worker Manifest.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
@@ -1070,6 +1086,31 @@ fn migrate_legacy_resolved_manifest_snapshot(
.cloned() .cloned()
.unwrap_or(serde_json::Value::Null); .unwrap_or(serde_json::Value::Null);
let extraction_enabled = !extraction_threshold.is_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!({ let mut resolved = serde_json::json!({
"profile": { "profile": {
"enabled": enabled, "enabled": enabled,
@@ -1089,6 +1130,9 @@ fn migrate_legacy_resolved_manifest_snapshot(
.cloned() .cloned()
.unwrap_or(serde_json::Value::Null), .unwrap_or(serde_json::Value::Null),
}, },
"consolidation": {
"request_enabled": consolidation_enabled,
},
}, },
}); });
if let Some(workspace_settings) = workspace_settings { if let Some(workspace_settings) = workspace_settings {
@@ -1490,7 +1534,8 @@ model_id = "claude-sonnet-4-20250514"
"language": "Français", "language": "Français",
"extract_threshold": 1234, "extract_threshold": 1234,
"extract_worker_max_turns": 3, "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(); 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, migrated.feature.memory.profile.extraction.threshold,
Some(1234) Some(1234)
); );
assert!(
migrated
.feature
.memory
.profile
.consolidation
.request_enabled
);
assert_eq!( assert_eq!(
migrated migrated
.feature .feature
@@ -1523,6 +1576,19 @@ model_id = "claude-sonnet-4-20250514"
mixed["feature"]["memory"] = serde_json::json!({ "enabled": true, "profile": {} }); mixed["feature"]["memory"] = serde_json::json!({ "enabled": true, "profile": {} });
mixed["memory"] = serde_json::json!({}); mixed["memory"] = serde_json::json!({});
assert!(read_persisted_worker_manifest_snapshot(mixed).is_err()); 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!( assert!(
read_persisted_worker_manifest_snapshot(serde_json::json!({ read_persisted_worker_manifest_snapshot(serde_json::json!({
"schema_version": 3, "schema_version": 3,
@@ -526,7 +526,9 @@ impl FeatureBackgroundTask for MemoryLifecycleTask {
.await; .await;
if !cancellation.is_cancelled() { if !cancellation.is_cancelled() {
context.generation_fence.ensure_current()?; context.generation_fence.ensure_current()?;
self.request_consolidation().await; if self.config.profile.consolidation.request_enabled {
self.request_consolidation().await;
}
} }
extraction 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( fn internal_result(
lifecycle: WorkerRunResult, lifecycle: WorkerRunResult,
) -> Result<InternalWorkerResult, InternalWorkerError> { ) -> Result<InternalWorkerResult, InternalWorkerError> {
+3
View File
@@ -246,6 +246,9 @@ permission = "write"
# # [feature.memory.profile.extraction.model] # # [feature.memory.profile.extraction.model]
# # ref = "anthropic/claude-haiku-4-5" # # ref = "anthropic/claude-haiku-4-5"
# #
# [feature.memory.profile.consolidation]
# request_enabled = true
#
# # Backendがresolved Manifestへbindする。手書き/Profile入力では指定しない。 # # Backendがresolved Manifestへbindする。手書き/Profile入力では指定しない。
# # [feature.memory.workspace_settings] # # [feature.memory.workspace_settings]
# # workspace_id = "workspace-id" # # workspace_id = "workspace-id"
+1
View File
@@ -30,6 +30,7 @@ feature = {
enabled = true; enabled = true;
threshold = 50000; threshold = 50000;
}; };
consolidation = { request_enabled = true; };
}; };
web = { enabled = true; }; web = { enabled = true; };
image = { enabled = true; }; image = { enabled = true; };