fix: preserve Memory consolidation enablement
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -30,6 +30,7 @@ feature = {
|
||||
enabled = true;
|
||||
threshold = 50000;
|
||||
};
|
||||
consolidation = { request_enabled = true; };
|
||||
};
|
||||
web = { enabled = true; };
|
||||
image = { enabled = true; };
|
||||
|
||||
Reference in New Issue
Block a user