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::{
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<MemoryResidentProfileConfigPartial>,
#[serde(default)]
pub extraction: Option<MemoryExtractionProfileConfigPartial>,
#[serde(default)]
pub consolidation: Option<MemoryConsolidationProfileConfigPartial>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -223,6 +225,13 @@ pub struct MemoryExtractionProfileConfigPartial {
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 {
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<FeatureConfigPartial> for FeatureConfig {
fn from(value: FeatureConfigPartial) -> Self {
Self {
@@ -385,6 +407,7 @@ impl From<MemoryFeatureConfigPartial> 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<MemoryFeatureConfigPartial> 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<ResolvedMemoryFeatureConfig> 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));
+67 -1
View File
@@ -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,
@@ -526,8 +526,10 @@ impl FeatureBackgroundTask for MemoryLifecycleTask {
.await;
if !cancellation.is_cancelled() {
context.generation_fence.ensure_current()?;
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<InternalWorkerResult, InternalWorkerError> {
+3
View File
@@ -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"
+1
View File
@@ -30,6 +30,7 @@ feature = {
enabled = true;
threshold = 50000;
};
consolidation = { request_enabled = true; };
};
web = { enabled = true; };
image = { enabled = true; };