diff --git a/crates/tracedecay-agent-hosts/src/agents/codex/tests.rs b/crates/tracedecay-agent-hosts/src/agents/codex/tests.rs index b146a5e75e..aad60f6961 100644 --- a/crates/tracedecay-agent-hosts/src/agents/codex/tests.rs +++ b/crates/tracedecay-agent-hosts/src/agents/codex/tests.rs @@ -992,7 +992,17 @@ fn prepare_stages_the_source_and_returns_ready_for_cli_activation() { let outcome = CodexIntegration .prepare_non_interactive_install(&install_ctx(home.path())) .unwrap(); - assert!(matches!(outcome, NonInteractiveInstallOutcome::Ready)); + // Activation is the host CLI's job. CI and other machines without `codex` + // must stop after staging, with the same command the operator runs. + match outcome { + NonInteractiveInstallOutcome::Ready => {} + NonInteractiveInstallOutcome::DeferredUserAction(deferred) => { + assert_eq!( + deferred.remediation, + "Codex activates plugins through its native cache. Run `codex plugin add tracedecay@personal` after TraceDecay stages the source package." + ); + } + } assert!(codex_plugin_manifest_path(home.path()).is_file()); assert!(codex_personal_marketplace_path(home.path()).is_file()); assert_eq!( diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/locking.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/locking.rs index 8d53fed465..6bdc552abd 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/locking.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/locking.rs @@ -47,10 +47,7 @@ pub fn try_acquire_code_generation_store_read_lock( ) -> Result, CodeGenerationRetentionErrorV1> { let store_root = canonical_store_root(store_root)?; let lock = open_lock_file(&store_root.join(STORE_LOCK_FILE))?; - match lock - .try_lock_shared() - .map_err(std::io::Error::from) - { + match lock.try_lock_shared().map_err(std::io::Error::from) { Ok(()) => Ok(Some(CodeGenerationStoreLockV1 { file: lock, store_root, diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs index 394cc15419..fa3da19743 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs @@ -1055,6 +1055,16 @@ pub(super) fn text_artifact_admitted_build_budget( .saturating_sub(watermark_headroom); let admitted_bytes = preferred_bytes.min(available_for_growth); if admitted_bytes < minimum_bytes { + // Reservations that fill the ledger are releasable pressure. The + // measured process being over the watermark is not. Collapsing them + // makes a transient competing reservation look like a dead authority + // and the successor never retries when that reservation drops. + let host_capacity = limit_bytes + .saturating_sub(unmodeled_live_bytes) + .saturating_sub(watermark_headroom); + if used_bytes > 0 && host_capacity >= minimum_bytes { + return Err(RetrievalPortError::BudgetExceeded); + } return Err(RetrievalPortError::AuthorityUnavailable(format!( "text-artifact build needs at least {minimum_bytes} bytes; \ {available_for_growth} bytes are available below the resident-memory watermark" diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs index dd5516dc71..fc1851c415 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs @@ -933,7 +933,18 @@ async fn dashboard_freshness_does_not_join_a_clone_backfill_slice() { latest.advance_text_serving(1).expect("advance text build"); } - let held_slot = latest.text_projection_build.lock_slot(); + // Hold the backfill slot on another thread. A std mutex must not cross + // this task's await, and freshness must still answer without taking it. + let build = Arc::clone(&latest.text_projection_build); + let (acquired_tx, acquired_rx) = std::sync::mpsc::sync_channel(1); + let (release_tx, release_rx) = std::sync::mpsc::sync_channel(1); + let holder = thread::spawn(move || { + let held_slot = build.lock_slot(); + acquired_tx.send(()).expect("notify slot hold"); + let _release = release_rx.recv(); + drop(held_slot); + }); + acquired_rx.recv().expect("clone backfill slot is held"); let freshness = tokio::time::timeout( Duration::from_millis(100), registry.dashboard_freshness(fixture.path()), @@ -941,6 +952,8 @@ async fn dashboard_freshness_does_not_join_a_clone_backfill_slice() { .await .expect("dashboard freshness must not wait for the clone backfill slice") .expect("mounted dashboard freshness"); + let _ = release_tx.send(()); + holder.join().expect("slot holder"); assert!(matches!( freshness.clone_index, Some( @@ -950,7 +963,6 @@ async fn dashboard_freshness_does_not_join_a_clone_backfill_slice() { ) if reason == "clone-index status is being updated" )); - drop(held_slot); registry.shutdown().await; } @@ -986,6 +998,14 @@ async fn query_admission_serves_v14_while_clone_successor_is_pending() { let worktree = mounted .get(&fixture.path().canonicalize().expect("canonical root")) .expect("mounted worktree"); + // The text owner and the serving seat must be the same seal. A + // second mount mints a different invalidation fingerprint, so + // grafting only the text handle leaves the query on the other + // generation and the pending clone is never the one that was queried. + *worktree + .serving_generation + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(latest.clone()); *worktree .text_generation .write() @@ -1060,6 +1080,10 @@ async fn expired_source_proof_reschedules_pending_clone_backfill() { let worktree = mounted .get(&fixture.path().canonicalize().expect("canonical root")) .expect("mounted worktree"); + *worktree + .serving_generation + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(latest.clone()); *worktree .text_generation .write() @@ -2432,6 +2456,20 @@ fn text_build_budget_shrinks_to_available_headroom_without_dropping_below_its_fl ), "less than the builder's supported floor must remain a typed capacity refusal" ); + + let held = limit - watermark_headroom - 64 * MIB; + assert_eq!( + super::super::text_artifact_admitted_build_budget( + minimum, + minimum, + limit, + held, + held, + watermark_headroom, + ), + Err(RetrievalPortError::BudgetExceeded), + "a reservation that leaves the host able to admit the floor is releasable pressure, not a dead authority" + ); } /// The artifact build and reader ceilings must reserve through the process diff --git a/crates/tracedecay-code-index/tests/code_index_suite/production_orchestration.rs b/crates/tracedecay-code-index/tests/code_index_suite/production_orchestration.rs index 151889363d..f5beb287d7 100644 --- a/crates/tracedecay-code-index/tests/code_index_suite/production_orchestration.rs +++ b/crates/tracedecay-code-index/tests/code_index_suite/production_orchestration.rs @@ -3244,22 +3244,22 @@ fn partitioned_codec_fixture() -> ( } const PARTITIONED_FORMAT_STATE_DIGEST: &str = - "sha256:28f30287a415e81bf589922385146f921a539734ec0a3600bd39578ad3c8dcd3"; + "sha256:8d84348830efc4452a078cfac1cc78e0ed44112a37f1025bd6f4f4bc152fe196"; const PARTITIONED_FORMAT_SEGMENTS: &[(&str, u64)] = &[ ( - "sha256:924c1f0b7b171b7bf5433a6eb04767eb244e7c9decc1f2343b06a657908f3a7b", - 11_070, + "sha256:e50d2733b5f594d79fdccc3e44b5d30d5efb66d14805b5fe0c67d5ceb0a1d66f", + 11_071, ), ( - "sha256:1a6e240c8fcc1084d82cee42cbaa889bb479ff1df7a76432dcec2d51d66e1d1a", - 5_170, + "sha256:1095d61bb8bbbf6637f85ca957a510d221aaba7923af8e60b0f3eef07042e6ff", + 5_171, ), ( - "sha256:4461a4ce08e5f59299030959a2773bd48b2c2d48056c188e06867db99609847a", - 6_278, + "sha256:9921ca7da5c489307887ab570a5e8d5a7cebf192b9b6664c487e9a327943e396", + 6_279, ), ( - "sha256:4c54bba48f3fcf2fd0085ab5aecd8b35451a8cfc56381fa99605327165afbb15", + "sha256:52b5707b5312bcb1e29849372b0dbb882205b3289b345643620a35c1d260c246", 6_837, ), ]; diff --git a/crates/tracedecay-dashboard-api/src/delivery_api.rs b/crates/tracedecay-dashboard-api/src/delivery_api.rs index df715d0121..4d76aea547 100644 --- a/crates/tracedecay-dashboard-api/src/delivery_api.rs +++ b/crates/tracedecay-dashboard-api/src/delivery_api.rs @@ -2474,7 +2474,7 @@ mod tests { panic!("a gated mount must project as typed unavailable"); }; assert!( - reason.contains("configure a token"), + reason.contains("Configure a token"), "the credential gate must tell the reader what to do: {reason}" ); diff --git a/crates/tracedecay-privacy/src/rules.rs b/crates/tracedecay-privacy/src/rules.rs index 131c2d3f15..438600dae7 100644 --- a/crates/tracedecay-privacy/src/rules.rs +++ b/crates/tracedecay-privacy/src/rules.rs @@ -654,10 +654,10 @@ fn compile_regex( /// so it is *both* a different match and vastly larger to compile: three /// upstream rules that repeat `\w` over a wide bound /// (`pypi-...[\w-]{50,1000}`) blow past the compiler's 10 MB program limit. -/// Expanding `\w` to its RE2 meaning fixes the semantics and the size at once -/// , every rule in the catalogue then compiles under the default limit, with +/// Expanding `\w` to its RE2 meaning fixes the semantics and the size at once, +/// so every rule in the catalogue then compiles under the default limit, with /// no memory headroom bought and no rule dropped. -////// * **`\b` / `\B`.** RE2's word boundary is ASCII. Rust's is Unicode-aware, +/// * **`\b` / `\B`.** RE2's word boundary is ASCII. Rust's is Unicode-aware, /// and a Unicode boundary is the one construct the lazy DFA gives up on the /// moment the haystack holds a non-ASCII byte: every file with an em-dash or /// an emoji in a comment was then scanned by the PikeVM, the slowest engine, diff --git a/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/query-lexical-graph-workload-v1.json b/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/query-lexical-graph-workload-v1.json index 864c0e0578..4646d75e25 100644 --- a/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/query-lexical-graph-workload-v1.json +++ b/crates/tracedecay-query/assets/runtime-root/tests/fixtures/search_quality/query-lexical-graph-workload-v1.json @@ -135,8 +135,8 @@ } ], "expected_query_fallback_digests": { - "train": "sha256:d020b61b1658145487d4d1ea9622ad6075293c329e202bb351c49290bc398067", - "validation": "sha256:934459d069cc368f06ce81d950116d4c823965a95c8f752eef9d41b45a0c01cb" + "train": "sha256:5750d4a588f7a7e14c381ec3a4285400a29e164babbf88e677ce7441aaf8e9b2", + "validation": "sha256:e7a459efb1655bb71e30fd302690cd5ac7937197add94a3ac1cab5812a1f5a48" }, "profile_matrix": [ { diff --git a/crates/tracedecay-query/src/search_quality/packaged.rs b/crates/tracedecay-query/src/search_quality/packaged.rs index 774f01bb55..4b3556a777 100644 --- a/crates/tracedecay-query/src/search_quality/packaged.rs +++ b/crates/tracedecay-query/src/search_quality/packaged.rs @@ -7,7 +7,7 @@ use super::candidate_output::{ use super::evaluate::SearchEvalError; const WORKLOAD_PATH: &str = "tests/fixtures/search_quality/query-lexical-graph-workload-v1.json"; -const WORKLOAD_SHA256: &str = "20322067510f57f5fa75f68674b18290a65d0535af7da363b6b6f29384042ca4"; +const WORKLOAD_SHA256: &str = "267e2bd2e9b90d258cbeed829920ab735f6af0ebc2a6e870d59eeef29b1cdb93"; const FILES: &[(&str, &[u8])] = &[ ( diff --git a/crates/tracedecay-runtime-core/src/lifecycle_lease.rs b/crates/tracedecay-runtime-core/src/lifecycle_lease.rs index d7a88ae33d..05b95672bd 100644 --- a/crates/tracedecay-runtime-core/src/lifecycle_lease.rs +++ b/crates/tracedecay-runtime-core/src/lifecycle_lease.rs @@ -221,10 +221,7 @@ pub fn acquire_shared_or_inherited(operation: &str) -> Result { fn acquire_shared_or_inherited_at(path: &Path, operation: &str) -> Result { let mut file = open_lock_file(path)?; - match file - .try_lock_shared() - .map_err(std::io::Error::from) - { + match file.try_lock_shared().map_err(std::io::Error::from) { Ok(()) => Ok(LifecycleLease { hold: LeaseHold::File(file), token: None, @@ -384,10 +381,7 @@ fn acquire_exclusive_at_with_timeout( #[hotpath::measure(label = "runtime_core.lifecycle.acquire_shared")] fn acquire_shared_at(path: &Path, operation: &str) -> Result { let mut file = open_lock_file(path)?; - match file - .try_lock_shared() - .map_err(std::io::Error::from) - { + match file.try_lock_shared().map_err(std::io::Error::from) { Ok(()) => Ok(LifecycleLease { hold: LeaseHold::File(file), token: None, @@ -404,10 +398,7 @@ fn acquire_shared_at(path: &Path, operation: &str) -> Result { fn try_acquire_shared_at(path: &Path, operation: &str) -> Result { let file = open_lock_file(path)?; - match file - .try_lock_shared() - .map_err(std::io::Error::from) - { + match file.try_lock_shared().map_err(std::io::Error::from) { Ok(()) => Ok(SharedLeaseAttempt::Acquired(LifecycleLease { hold: LeaseHold::File(file), token: None, diff --git a/crates/tracedecay-search-eval/src/bin/tracedecay-search-eval-direct.rs b/crates/tracedecay-search-eval/src/bin/tracedecay-search-eval-direct.rs index 1a2b3d692a..eca9235503 100644 --- a/crates/tracedecay-search-eval/src/bin/tracedecay-search-eval-direct.rs +++ b/crates/tracedecay-search-eval/src/bin/tracedecay-search-eval-direct.rs @@ -246,7 +246,7 @@ mod tests { assert_eq!(summary.status, DirectEvaluationStatusV1::Pass); assert_eq!( summary.workload_digest, - "sha256:c7c97a6ab08da36ba02a89ca0d705dee0cc62d3a12d6bd3698f6c2185dd2d708" + "sha256:8657aa486a4c58e17c9969c7aa5d143a4d30e88dca7d26f13e61c7d3effab091" ); assert_eq!(summary.profile_count, 1); assert_eq!(summary.query_count, 67); diff --git a/crates/tracedecay/src/daemon/production_harness.rs b/crates/tracedecay/src/daemon/production_harness.rs index 1f2c6daaaf..a0ffb46bf6 100644 --- a/crates/tracedecay/src/daemon/production_harness.rs +++ b/crates/tracedecay/src/daemon/production_harness.rs @@ -1535,6 +1535,9 @@ mod generation_retention_test; #[cfg(test)] mod configuration_idempotency_journey_test; +#[cfg(test)] +mod configuration_protected_preview_journey_test; + #[cfg(test)] mod read_only_project_open_journey_test; diff --git a/crates/tracedecay/src/daemon/production_harness/configuration_protected_preview_journey_test.rs b/crates/tracedecay/src/daemon/production_harness/configuration_protected_preview_journey_test.rs new file mode 100644 index 0000000000..74b2191aa4 --- /dev/null +++ b/crates/tracedecay/src/daemon/production_harness/configuration_protected_preview_journey_test.rs @@ -0,0 +1,275 @@ +//! Host-facing behavior of `tracedecay_configuration_protected_preview`. +//! +//! The tool is a dry-run: the answer is a redacted plan bound to the revision +//! the caller supplied, and a wrong revision or an invalid change is a typed +//! problem rather than a committed setting. Callers observe that through MCP +//! `tools/call`, which is the path this journey drives. + +use std::collections::BTreeSet; +use std::path::Path; + +use serde_json::{Value, json}; +use tempfile::TempDir; +use tracedecay_contracts::ConfigurationProtectedPreviewRequestV1; +use tracedecay_domain::configuration::{ + AccessRuleId, AuthorityRef, ConfigurationRevisionId, ProtectedChange, RuleEffect, + ScopeAccessRule, ScopeAccessSubjectV1, SourceBindingId, SourceKindV1, +}; +use tracedecay_domain::{CapabilityId, ManifestDigest}; + +use super::journey_test_support::{git, tool_answer}; +use super::*; + +const ACCESS_RULE_ID: &str = "access-rule.preview-cursor-deny"; +const DENIED_CAPABILITY: &str = "capability.work.generate_proposal"; +const ABSENT_BINDING_ID: &str = "source-binding.preview-absent"; +const STALE_REVISION: &str = "configuration.revision.protected-preview-not-current"; + +fn initialize_project(project: &Path) { + std::fs::create_dir_all(project.join("src")).expect("project source"); + std::fs::write(project.join("src/lib.rs"), "pub fn preview_probe() {}\n") + .expect("project source file"); + git(project, &["init", "--quiet"]); +} + +fn preview_arguments(change: &ProtectedChange, revision: &ConfigurationRevisionId) -> Value { + let mut arguments = serde_json::to_value(ConfigurationProtectedPreviewRequestV1 { + change: change.clone(), + expected_revision: revision.clone(), + }) + .expect("protected preview arguments"); + arguments["format"] = json!("json"); + arguments +} + +fn deny_cursor_work(project_id: tracedecay_domain::ProjectId) -> ProtectedChange { + ProtectedChange::UpsertAccessRule( + ScopeAccessRule::new( + AccessRuleId::new(ACCESS_RULE_ID).expect("access rule identity"), + ScopeAccessSubjectV1 { + actor: None, + operation: None, + source_kind: Some(SourceKindV1::Cursor), + }, + AuthorityRef::Project(project_id), + BTreeSet::from([ + CapabilityId::new(DENIED_CAPABILITY).expect("generate proposal capability") + ]), + RuleEffect::Deny, + None, + ) + .expect("deny-only work rule"), + ) +} + +async fn call_preview( + harness: &ProductionProjectCompositionHarnessV1, + project: &Path, + arguments: Value, +) -> (bool, Value) { + let response = harness + .call_tool( + project, + "tracedecay_configuration_protected_preview", + arguments, + ) + .await + .expect("protected preview tools/call"); + tool_answer(&response) +} + +fn assert_redacted_plan( + payload: &Value, + revision: &str, + setting_key: &str, + operation: &str, + before_digest: &str, + after_digest: &str, + hidden: &[&str], +) { + assert_eq!(payload["outcome"]["outcome"], "preview"); + assert_eq!( + payload["outcome"]["value"]["effect_class"], + "configuration_write" + ); + let plan = &payload["outcome"]["value"]["payload"]; + assert_eq!(plan["base_revision_id"], revision); + assert_eq!( + plan["redacted_changes"], + json!([{ + "setting_key": setting_key, + "operation": operation, + "before_digest": before_digest, + "after_digest": after_digest, + }]) + ); + assert_eq!(plan["operation_digest"], after_digest); + assert_eq!(payload["outcome"]["value"]["preview_digest"], after_digest); + assert_eq!( + payload["outcome"]["value"]["preview_id"], plan["plan_id"], + "the preview id the host applies is the plan id" + ); + let plan_id = plan["plan_id"].as_str().expect("plan id"); + assert!( + plan_id.starts_with("configuration.plan.v1."), + "plan id {plan_id} is not a configuration plan" + ); + let created_at = plan["created_at"].as_i64().expect("plan created_at"); + let expires_at = plan["expires_at"].as_i64().expect("plan expires_at"); + assert_eq!( + expires_at - created_at, + 300_000_000, + "a protected preview stays valid for five minutes" + ); + let rendered = serde_json::to_string(payload).expect("preview json"); + for secret in hidden { + assert!( + !rendered.contains(secret), + "preview leaked {secret}: {rendered}" + ); + } +} + +fn assert_problem( + payload: &Value, + kind: &str, + code: &str, + message: &str, + retry: &str, + legal_actions: Value, +) { + assert_eq!(payload["problem"]["kind"], kind, "{payload}"); + assert_eq!(payload["problem"]["code"], code, "{payload}"); + assert_eq!(payload["problem"]["message"], message, "{payload}"); + assert_eq!(payload["problem"]["diagnostic"]["code"], code, "{payload}"); + assert_eq!( + payload["problem"]["diagnostic"]["message"], message, + "{payload}" + ); + assert_eq!(payload["problem"]["retry"], retry, "{payload}"); + assert_eq!( + payload["problem"]["legal_actions"], legal_actions, + "{payload}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn protected_preview_redacts_the_change_and_refuses_stale_or_invalid_input() { + let isolation = TempDir::new().expect("journey isolation"); + let project = isolation.path().join("project"); + initialize_project(&project); + + let harness = ProductionProjectCompositionHarnessV1::open(isolation.path(), [project.clone()]) + .await + .expect("production composition"); + let graph = harness.server(&project).expect("project server").cg().await; + let project_id = graph + .configuration_runtime() + .configuration_target() + .project_id + .clone(); + let current = graph + .configuration_runtime() + .client() + .current() + .await + .expect("current configuration"); + let revision = current.revision_id().clone(); + let before_digest: ManifestDigest = current.snapshot().effective_behavior_digest.clone(); + drop(graph); + + let access_rule = deny_cursor_work(project_id.clone()); + let access_digest = access_rule + .compute_digest() + .expect("access rule digest") + .as_str() + .to_owned(); + let (refused, accepted) = call_preview( + &harness, + &project, + preview_arguments(&access_rule, &revision), + ) + .await; + assert!(!refused, "access-rule preview was refused: {accepted}"); + assert_redacted_plan( + &accepted, + revision.as_str(), + "scope.access_rules.v1", + "access_rule_upsert", + before_digest.as_str(), + &access_digest, + &[ACCESS_RULE_ID, DENIED_CAPABILITY], + ); + + let unbind = ProtectedChange::UnbindSource { + binding_id: SourceBindingId::new(ABSENT_BINDING_ID).expect("binding identity"), + }; + let unbind_digest = unbind + .compute_digest() + .expect("unbind digest") + .as_str() + .to_owned(); + assert_ne!( + access_digest, unbind_digest, + "the two submitted changes must not share a digest" + ); + let (refused, unbound) = + call_preview(&harness, &project, preview_arguments(&unbind, &revision)).await; + assert!(!refused, "unbind preview was refused: {unbound}"); + assert_redacted_plan( + &unbound, + revision.as_str(), + "scope.source_bindings.v1", + "source_unbind", + before_digest.as_str(), + &unbind_digest, + &[ABSENT_BINDING_ID], + ); + + let mut stale = preview_arguments(&access_rule, &revision); + stale["expected_revision"] = json!(STALE_REVISION); + let (refused, conflict) = call_preview(&harness, &project, stale).await; + assert!(refused, "a stale revision must be a tool error: {conflict}"); + assert_problem( + &conflict, + "conflict", + "configuration.conflict", + "The configuration request conflicts with current state", + "after_revalidate", + json!(["refresh"]), + ); + + let mut invalid = preview_arguments(&access_rule, &revision); + invalid["change"]["value"]["capabilities"] = json!([]); + let (refused, rejected) = call_preview(&harness, &project, invalid).await; + assert!( + refused, + "an empty capability set must be a tool error: {rejected}" + ); + assert_problem( + &rejected, + "invalid_request", + "configuration.invalid_request", + "The configuration request is invalid: access rule capabilities must not be empty", + "never", + json!([]), + ); + + let graph = harness.server(&project).expect("project server").cg().await; + let unchanged = graph + .configuration_runtime() + .client() + .current() + .await + .expect("configuration after previews") + .revision_id() + .clone(); + drop(graph); + assert_eq!( + unchanged.as_str(), + revision.as_str(), + "protected preview must not commit a revision" + ); + + harness.shutdown().await; +}