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::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));
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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; };
|
||||||
|
|||||||
Reference in New Issue
Block a user