diff --git a/TASKS.md b/TASKS.md index c067b31..4e8f5dc 100644 --- a/TASKS.md +++ b/TASKS.md @@ -62,6 +62,7 @@ Progress: - `ExecutionConfig.response_cache` is now part of the public operation model, and `admin-api` validates the safe Community baseline: only explicit `REST GET` operations without `auth_profile_ref` - cache boundary and namespace rules are now documented for `platform / coordination cache` versus `response cache` - runtime executor now performs actual response cache reads/writes for eligible `REST GET` operations with keys isolated by `workspace + agent + operation + request fingerprint` + - response cache keys now also include `operation version`, so a new published version does not reuse stale cached payloads from the previous one - mcp-server published tool catalog now uses shared coordination snapshots across instances, while preserving the same `workspace + agent` isolation model - pending: - preserve the same isolation model while extending response cache beyond the first `REST GET` hot path diff --git a/crates/crank-runtime/src/executor.rs b/crates/crank-runtime/src/executor.rs index 7f96661..1910717 100644 --- a/crates/crank-runtime/src/executor.rs +++ b/crates/crank-runtime/src/executor.rs @@ -789,10 +789,11 @@ fn response_cache_key( let fingerprint_hash = URL_SAFE_NO_PAD.encode(Sha256::digest(fingerprint_bytes)); Some(format!( - "crank:response:workspace:{}:agent:{}:operation:{}:request:{}", + "crank:response:workspace:{}:agent:{}:operation:{}:version:{}:request:{}", scope.workspace_key, scope.agent_key, operation.operation_id.as_str(), + operation.operation_version, fingerprint_hash )) } @@ -1062,6 +1063,32 @@ mod tests { assert_eq!(request_count.load(Ordering::SeqCst), 2); } + #[tokio::test] + async fn invalidates_rest_get_cache_on_operation_version_change() { + let request_count = Arc::new(AtomicUsize::new(0)); + let base_url = spawn_cached_rest_server(Arc::clone(&request_count)).await; + let executor = RuntimeExecutor::new() + .with_response_cache_store(Arc::new(InMemoryResponseCacheStore::default())); + let operation_v1 = test_cached_rest_get_operation(&base_url); + let mut operation_v2 = operation_v1.clone(); + let context = RuntimeRequestContext::from_request_id("req_cache_version") + .with_response_cache_scope("ws_cache", "agent_sales"); + + let first_output = executor + .execute_with_context(&operation_v1, &json!({ "city": "msk" }), Some(&context)) + .await + .unwrap(); + operation_v2.operation_version = 2; + let second_output = executor + .execute_with_context(&operation_v2, &json!({ "city": "msk" }), Some(&context)) + .await + .unwrap(); + + assert_eq!(first_output, json!({ "city": "msk", "request_count": 1 })); + assert_eq!(second_output, json!({ "city": "msk", "request_count": 2 })); + assert_eq!(request_count.load(Ordering::SeqCst), 2); + } + #[tokio::test] async fn emits_runtime_tracing_with_request_context() { let base_url = spawn_runtime_server().await; diff --git a/crates/crank-runtime/src/model.rs b/crates/crank-runtime/src/model.rs index af54c04..034463e 100644 --- a/crates/crank-runtime/src/model.rs +++ b/crates/crank-runtime/src/model.rs @@ -9,6 +9,7 @@ use serde_json::Value; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RuntimeOperation { pub operation_id: OperationId, + pub operation_version: u32, pub tool_name: String, pub protocol: Protocol, pub target: Target, @@ -24,6 +25,7 @@ impl From> for RuntimeOperation { fn from(value: Operation) -> Self { Self { operation_id: value.id, + operation_version: value.version, tool_name: value.name, protocol: value.protocol, target: value.target, diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index 51d584c..b81a567 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -195,7 +195,7 @@ - Enterprise может подключать свой `Valkey/Redis` на уровне single-node или cluster deployment. - cache namespaces документированы так, чтобы: - platform / coordination state не смешивался с response cache; - - response cache по умолчанию изолировался по `workspace + agent + operation + request fingerprint`; + - response cache по умолчанию изолировался по `workspace + agent + operation + operation version + request fingerprint`; - внешний cache backend можно было безопасно шарить между несколькими рабочими областями и агентами. - shared coordination cache уже применяется не только для rate limiting, но и для multi-instance snapshots published MCP catalogs. diff --git a/docs/mcp-interface.md b/docs/mcp-interface.md index e60b632..4eabdd7 100644 --- a/docs/mcp-interface.md +++ b/docs/mcp-interface.md @@ -241,7 +241,7 @@ Crank должен корректно работать, если MCP client де - `REST` - `GET` - без `auth_profile_ref` - - cache keys изолируются минимум по `workspace + agent + operation + request fingerprint` + - cache keys изолируются минимум по `workspace + agent + operation + operation version + request fingerprint` - streaming operations публикуются как bounded tools или tool families; - reload published tools без пересборки сервиса; - никакой draft-логики или admin CRUD в MCP слое. diff --git a/docs/runtime-config.md b/docs/runtime-config.md index 1b305b8..afb2ba6 100644 --- a/docs/runtime-config.md +++ b/docs/runtime-config.md @@ -195,7 +195,7 @@ Demo/deployment: Стартовая модель namespace для response cache: -- `workspace + agent + operation + request fingerprint` +- `workspace + agent + operation + operation version + request fingerprint` Стартовая модель namespace для platform / coordination cache: