diff --git a/.bazelrc b/.bazelrc index 29d703f130..fa5ba0b8ab 100644 --- a/.bazelrc +++ b/.bazelrc @@ -5,6 +5,9 @@ startup --windows_enable_symlinks common --enable_bzlmod common --announce_rc common --show_progress_rate_limit=15 +# MODULE.bazel registers only nightly/2026-09-27. Without this, rules_rust +# resolves the stable channel and the build fails closed. +build --@rules_rust//rust/toolchain/channel=nightly build --incompatible_enable_cc_toolchain_resolution build --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 build --workspace_status_command=scripts/bazel/workspace_status.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d412fc15ca..776e8b0f73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -617,8 +617,6 @@ jobs: # One graph walk per push. The release resolver repeats it on the tag it # builds, since a release is dispatched without waiting on this run. - name: Check the production feature graph - env: - RUSTUP_TOOLCHAIN: stable run: python3 scripts/check-production-feature-profile.py # The configuration users actually receive: the release build of diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8505c8b38e..9791b2b079 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -667,9 +667,18 @@ jobs: ref: ${{ needs.validate-release.outputs.source_sha }} persist-credentials: false + # rust-toolchain.toml is the only compiler. A floating stable channel + # would rebuild the SDK conformance daemon on a different rustc than + # the release binaries. + - name: Resolve pinned toolchain + id: rust-toolchain + shell: bash + run: | + python3 -c 'import pathlib, tomllib; print("toolchain=" + tomllib.loads(pathlib.Path("rust-toolchain.toml").read_text(encoding="utf-8"))["toolchain"]["channel"])' >> "$GITHUB_OUTPUT" + - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable with: - toolchain: stable + toolchain: ${{ steps.rust-toolchain.outputs.toolchain }} - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: diff --git a/.github/workflows/upstream-compat.yml b/.github/workflows/upstream-compat.yml index 690fc98dc3..9cd943b7fb 100644 --- a/.github/workflows/upstream-compat.yml +++ b/.github/workflows/upstream-compat.yml @@ -7,11 +7,6 @@ on: schedule: - cron: "0 4 * * 1" workflow_dispatch: - inputs: - nightly-date: - description: "Rust nightly snapshot to probe; empty uses yesterday UTC." - type: string - default: "" permissions: contents: read @@ -45,37 +40,3 @@ jobs: --incompatible_no_implicit_file_export \ --rewind_lost_inputs \ //crates/... - - # Same lanes, nightly rustc: the probe runs one build against a recent - # upstream snapshot so next releases stop being a surprise. Never adopt - # nightly; the pinned stable 1.99.0 stays the authority lane. - nightly-rust-probe: - name: Rust nightly toolchain probe - runs-on: ubuntu-latest - timeout-minutes: 240 - continue-on-error: ${{ github.event_name == 'pull_request' }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Re-pin MODULE.bazel to the nightly snapshot - env: - NIGHTLY_DATE_INPUT: ${{ inputs.nightly-date }} - run: | - set -euo pipefail - NIGHTLY_DATE="${NIGHTLY_DATE_INPUT:-$(date -u -d yesterday +%F)}" - echo "Probing Rust nightly $NIGHTLY_DATE" - if ! grep -F 'versions = ["1.99.0"],' MODULE.bazel >/dev/null; then - echo "MODULE.bazel no longer names 1.99.0; refresh this probe's patterns" - exit 2 - fi - if ! grep -F 'rustfmt_version = "1.99.0"' MODULE.bazel >/dev/null; then - echo "MODULE.bazel no longer pins rustfmt to 1.99.0; refresh this probe's patterns" - exit 2 - fi - sed -i "s|versions = \[\"1\.99\.0\"\]|versions = [\"nightly/$NIGHTLY_DATE\"]|" MODULE.bazel - sed -i "s|rustfmt_version = \"1\.99\.0\"|rustfmt_version = \"nightly/$NIGHTLY_DATE\"|" MODULE.bazel - grep -n "nightly" MODULE.bazel - - uses: ./.github/actions/setup-bazel - with: - cache-key: upstream-compat-nightly - - name: Build the Rust workspace on nightly - run: bazel build --config=ci //crates/... --keep_going diff --git a/Cargo.toml b/Cargo.toml index 12ff2aa4cf..6f56fe4d9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,10 @@ edition = "2024" version = "1.0.0-beta.87" [workspace.dependencies] -asupersync = { version = "=0.5.0", default-features = false, features = ["proc-macros", "native-runtime"] } +# nightly-outcome-try is an asupersync default (Outcome's Try impls). It +# compiles only on the pinned nightly. native-runtime stays explicit because +# it is not part of that default set. +asupersync = { version = "=0.5.0", default-features = false, features = ["proc-macros", "native-runtime", "nightly-outcome-try"] } # grafeo 0.5.43 from our fork (ScriptedAlchemy/grafeo, main). The # published crates carry a tiered-storage # arena whose `HotVersionRef` addresses a single 1 MiB chunk, which caps an diff --git a/MODULE.bazel b/MODULE.bazel index 18b585279f..81b6375bd9 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -32,8 +32,8 @@ rust.toolchain( "fastbuild": "1", "opt": "3", }, - versions = ["1.99.0"], - rustfmt_version = "1.99.0", + versions = ["nightly/2026-09-27"], + rustfmt_version = "nightly/2026-09-27", ) use_repo(rust, "rust_toolchains") register_toolchains("@rust_toolchains//:all") diff --git a/README.md b/README.md index 8e6961aa86..50048fe6f7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

GitHub release License: MIT - Rust 1.99.0 + Rust nightly-2026-09-27 macOS Linux Windows diff --git a/crates/tracedecay-agent-hosts/src/agents/chatgpt.rs b/crates/tracedecay-agent-hosts/src/agents/chatgpt.rs index 1cf7731fea..1d5b2ede2b 100644 --- a/crates/tracedecay-agent-hosts/src/agents/chatgpt.rs +++ b/crates/tracedecay-agent-hosts/src/agents/chatgpt.rs @@ -93,8 +93,7 @@ impl AgentIntegration for ChatGptIntegration { // proof that nothing was deployed. chatgpt_staged_plugin_dir(home) .read_dir() - .map(|mut entries| entries.next().is_some()) - .unwrap_or(false) + .is_ok_and(|mut entries| entries.next().is_some()) } fn detected_host_surface(&self, home: &Path, _profile: &ProfileRoot) -> Option { @@ -170,8 +169,7 @@ fn doctor_check_plugin(dc: &mut DoctorCounters, home: &Path) { // fails rather than warning as if nothing were staged. let has_residue = staged_dir .read_dir() - .map(|mut entries| entries.next().is_some()) - .unwrap_or(false); + .is_ok_and(|mut entries| entries.next().is_some()); if has_residue { dc.fail(&format!( "ChatGPT staged bundle at {} is incomplete: {} missing, run `tracedecay reinstall --agent chatgpt`", diff --git a/crates/tracedecay-agent-hosts/src/agents/claude.rs b/crates/tracedecay-agent-hosts/src/agents/claude.rs index 0109461af6..b1f1285355 100644 --- a/crates/tracedecay-agent-hosts/src/agents/claude.rs +++ b/crates/tracedecay-agent-hosts/src/agents/claude.rs @@ -1158,8 +1158,7 @@ fn doctor_check_permissions_json(dc: &mut DoctorCounters, home: &Path) { let installed: Vec<&str> = settings["permissions"]["allow"] .as_array() - .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect()) - .unwrap_or_default(); + .map_or_default(|arr| arr.iter().filter_map(|v| v.as_str()).collect()); // The plugin-namespace entries are the ones the plugin MCP server actually // matches against; without coverage every call to a tool prompts @@ -1241,8 +1240,7 @@ pub fn check_install_stale(home: &Path) { fn warn_missing_permissions(settings: &serde_json::Value) { let installed: Vec<&str> = settings["permissions"]["allow"] .as_array() - .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect()) - .unwrap_or_default(); + .map_or_default(|arr| arr.iter().filter_map(|v| v.as_str()).collect()); // Check the plugin namespace, the entries the plugin MCP server matches. // Missing coverage of `mcp__plugin_tracedecay_graph__*` is exactly what diff --git a/crates/tracedecay-agent-hosts/src/agents/codex/mcp_registry.rs b/crates/tracedecay-agent-hosts/src/agents/codex/mcp_registry.rs index 4c2fa29528..a92e3a17e8 100644 --- a/crates/tracedecay-agent-hosts/src/agents/codex/mcp_registry.rs +++ b/crates/tracedecay-agent-hosts/src/agents/codex/mcp_registry.rs @@ -724,8 +724,7 @@ exit 0"#; config .get(CODEX_MCP_SERVERS_KEY) .and_then(toml::Value::as_table) - .map(|servers| servers.keys().cloned().collect()) - .unwrap_or_default() + .map_or_default(|servers| servers.keys().cloned().collect()) } /// A fresh MCP-only install ends with exactly one standalone server that diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/address_registry.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/address_registry.rs index 8347ee8fb0..c062b8955f 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/address_registry.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/address_registry.rs @@ -827,16 +827,13 @@ where .latest_committed(context, observed_at) .await .filter(|publication| publication_matches_pin(publication, pin)); - let candidates = latest_publication - .as_ref() - .map(|publication| { - context_scout_candidates_from_publication( - publication, - pin.configuration.control(), - observed_at, - ) - }) - .unwrap_or_default(); + let candidates = latest_publication.as_ref().map_or_default(|publication| { + context_scout_candidates_from_publication( + publication, + pin.configuration.control(), + observed_at, + ) + }); Some(ContextScoutCanonicalInputV1 { address, control: pin.configuration.control(), diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs index 7cd16aea08..0f73a69e46 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/owner.rs @@ -97,8 +97,7 @@ pub fn lookup_registered_context_scout_owners( owners .get(&project_id) .cloned() - .map(|owner| vec![owner]) - .unwrap_or_default() + .map_or_default(|owner| vec![owner]) } /// What one conditional unregister did to the project's owner slot. diff --git a/crates/tracedecay-agent-hosts/src/agents/host_bundle/writer.rs b/crates/tracedecay-agent-hosts/src/agents/host_bundle/writer.rs index 2b4de2bf44..d2925976b7 100644 --- a/crates/tracedecay-agent-hosts/src/agents/host_bundle/writer.rs +++ b/crates/tracedecay-agent-hosts/src/agents/host_bundle/writer.rs @@ -739,14 +739,11 @@ impl HostBundleWriterV1 { )?; let mut component_manifests = previous .as_ref() - .map(|receipt| receipt.component_manifests.clone()) - .unwrap_or_default(); + .map_or_default(|receipt| receipt.component_manifests.clone()); component_manifests.retain(|previous| previous.component != manifest.component); component_manifests.push(manifest.clone()); component_manifests.sort_by_key(|manifest| manifest.component); - let mut component_receipts = previous - .map(|receipt| receipt.component_receipts) - .unwrap_or_default(); + let mut component_receipts = previous.map_or_default(|receipt| receipt.component_receipts); component_receipts.retain(|previous| previous.component != component_receipt.component); component_receipts.push(component_receipt.clone()); component_receipts.sort_by_key(|receipt| receipt.component); diff --git a/crates/tracedecay-agent-hosts/src/agents/host_bundle_registry.rs b/crates/tracedecay-agent-hosts/src/agents/host_bundle_registry.rs index 462e1a60c4..e6efb295af 100644 --- a/crates/tracedecay-agent-hosts/src/agents/host_bundle_registry.rs +++ b/crates/tracedecay-agent-hosts/src/agents/host_bundle_registry.rs @@ -984,9 +984,8 @@ mod tests { /// component-set journal. #[test] fn opencode_core_assets_do_not_depend_on_the_running_executable_path() { - let running = std::env::current_exe() - .map(|path| path.to_string_lossy().into_owned()) - .unwrap_or_default(); + let running = + std::env::current_exe().map_or_default(|path| path.to_string_lossy().into_owned()); let installed = super::super::which_tracedecay().unwrap_or_else(|| "tracedecay".to_string()); let assets = component_assets( @@ -1049,9 +1048,8 @@ mod tests { /// `component-set-journal.hermes.v1.json` behind. #[test] fn hermes_core_assets_do_not_depend_on_the_running_executable_path() { - let running = std::env::current_exe() - .map(|path| path.to_string_lossy().into_owned()) - .unwrap_or_default(); + let running = + std::env::current_exe().map_or_default(|path| path.to_string_lossy().into_owned()); let installed = super::super::which_tracedecay().unwrap_or_else(|| "tracedecay".to_string()); let assets = component_assets( diff --git a/crates/tracedecay-agent-hosts/src/agents/host_cli.rs b/crates/tracedecay-agent-hosts/src/agents/host_cli.rs index 39f73d47e2..e648e304f2 100644 --- a/crates/tracedecay-agent-hosts/src/agents/host_cli.rs +++ b/crates/tracedecay-agent-hosts/src/agents/host_cli.rs @@ -170,8 +170,8 @@ pub(crate) fn run_host_cli(program: &Path, args: &[&str], home: &Path) -> Result } }; - let stdout = stdout_handle.map(join_reader).unwrap_or_default(); - let stderr = stderr_handle.map(join_reader).unwrap_or_default(); + let stdout = stdout_handle.map_or_default(join_reader); + let stderr = stderr_handle.map_or_default(join_reader); Ok(HostCliOutcomeV1 { program: rendered_program, diff --git a/crates/tracedecay-agent-hosts/src/hooks/tool_hints/tests.rs b/crates/tracedecay-agent-hosts/src/hooks/tool_hints/tests.rs index 26e0132ebf..049120fced 100644 --- a/crates/tracedecay-agent-hosts/src/hooks/tool_hints/tests.rs +++ b/crates/tracedecay-agent-hosts/src/hooks/tool_hints/tests.rs @@ -91,8 +91,10 @@ fn every_category_has_compact_skill_backed_rendering() { let skill = visible .split_once("Skill: tracedecay:") .and_then(|(_, rest)| rest.split_once('.')) - .map(|(skill, _)| skill) - .unwrap_or_else(|| panic!("{category:?} hint names no skill:\n{visible}")); + .map_or_else( + || panic!("{category:?} hint names no skill:\n{visible}"), + |(skill, _)| skill, + ); let skill_path = format!("skills/{skill}/SKILL.md"); let (_, skill_body) = shipped .iter() diff --git a/crates/tracedecay-agent-hosts/src/native_integration/registry.rs b/crates/tracedecay-agent-hosts/src/native_integration/registry.rs index 431550398a..23cdbe07f6 100644 --- a/crates/tracedecay-agent-hosts/src/native_integration/registry.rs +++ b/crates/tracedecay-agent-hosts/src/native_integration/registry.rs @@ -164,6 +164,33 @@ pub struct DaemonNativeIntegrationOwner { stack_runtimes: Arc>>>, } +// The stack-runtime map's `Send` proof walks the native coordinator past the +// recursion limit (rust-lang/rust#159228). Each field is checked on its own. +unsafe impl Send for DaemonNativeIntegrationOwner {} +unsafe impl Sync for DaemonNativeIntegrationOwner {} + +const _: fn(&DaemonNativeIntegrationOwner) = |value| { + fn assert_send_sync(_: &T) {} + let DaemonNativeIntegrationOwner { + project_id, + repository_id, + service, + snapshots, + worktrees, + store, + scope_sets, + stack_runtimes, + } = value; + assert_send_sync(project_id); + assert_send_sync(repository_id); + assert_send_sync(service); + assert_send_sync(snapshots); + assert_send_sync(worktrees); + assert_send_sync(store); + assert_send_sync(scope_sets); + assert_send_sync(stack_runtimes); +}; + impl DaemonNativeIntegrationOwner { pub fn service(&self) -> &DaemonProjectNativeIntegrationService { &self.service diff --git a/crates/tracedecay-agent-hosts/src/native_integration/stack_runtime.rs b/crates/tracedecay-agent-hosts/src/native_integration/stack_runtime.rs index a4ea2c3f05..38d0f33ffb 100644 --- a/crates/tracedecay-agent-hosts/src/native_integration/stack_runtime.rs +++ b/crates/tracedecay-agent-hosts/src/native_integration/stack_runtime.rs @@ -792,6 +792,25 @@ async fn drain_loop(coordinator: Arc, ports: Sta } } +// A `BTreeMap` of these runtimes proves the native service before `Send` +// bottoms out (rust-lang/rust#159228). Each field is checked on its own. +unsafe impl Send for DaemonGitHubStackRuntimeV1 {} +unsafe impl Sync for DaemonGitHubStackRuntimeV1 {} + +const _: fn(&DaemonGitHubStackRuntimeV1) = |value| { + fn assert_send_sync(_: &T) {} + let DaemonGitHubStackRuntimeV1 { + scope, + coordinator, + ports, + tick_task, + } = value; + assert_send_sync(scope); + assert_send_sync(coordinator); + assert_send_sync(ports); + assert_send_sync(tick_task); +}; + impl DaemonGitHubStackRuntimeV1 { #[allow(clippy::too_many_arguments)] #[tracing::instrument( diff --git a/crates/tracedecay-application/src/advisory/github_runtime.rs b/crates/tracedecay-application/src/advisory/github_runtime.rs index 8172e2b03a..89cfba964c 100644 --- a/crates/tracedecay-application/src/advisory/github_runtime.rs +++ b/crates/tracedecay-application/src/advisory/github_runtime.rs @@ -691,9 +691,7 @@ impl GitHubReviewRefreshStateV1 { } else { previous.and_then(|state| state.last_complete.clone()) }; - let mut attempt_receipts = previous - .map(|state| state.attempt_receipts.clone()) - .unwrap_or_default(); + let mut attempt_receipts = previous.map_or_default(|state| state.attempt_receipts.clone()); if let Some(receipt) = receipt { attempt_receipts.push(receipt); if attempt_receipts.len() > MAX_GITHUB_REFRESH_ATTEMPT_RECEIPTS_V1 { diff --git a/crates/tracedecay-application/src/advisory/github_runtime/anchors.rs b/crates/tracedecay-application/src/advisory/github_runtime/anchors.rs index f561154cf6..b81a3f094d 100644 --- a/crates/tracedecay-application/src/advisory/github_runtime/anchors.rs +++ b/crates/tracedecay-application/src/advisory/github_runtime/anchors.rs @@ -798,8 +798,7 @@ fn safe_github_review_url(value: &str) -> bool { }; let segments = url .path_segments() - .map(std::iter::Iterator::collect::>) - .unwrap_or_default(); + .map_or_default(std::iter::Iterator::collect::>); url.scheme() == "https" && url.host_str() == Some("github.com") && url.port().is_none() diff --git a/crates/tracedecay-application/src/advisory/proximity_runtime/authority.rs b/crates/tracedecay-application/src/advisory/proximity_runtime/authority.rs index d7be6e48fe..1ac15adab9 100644 --- a/crates/tracedecay-application/src/advisory/proximity_runtime/authority.rs +++ b/crates/tracedecay-application/src/advisory/proximity_runtime/authority.rs @@ -228,8 +228,7 @@ impl ProximityAssembly<'_> { let ranges = self .session_edit_spans .get(&((*key).clone(), path.clone())) - .map(Vec::as_slice) - .unwrap_or_default(); + .map_or_default(Vec::as_slice); let (span, symbol) = participant_graph_address(participant_nodes, ranges); let source = ObservationSourceIdentityV1::for_provider( observation.envelope.provider().clone(), diff --git a/crates/tracedecay-application/src/feedback/cycle_runtime.rs b/crates/tracedecay-application/src/feedback/cycle_runtime.rs index 398251e561..3b222f817d 100644 --- a/crates/tracedecay-application/src/feedback/cycle_runtime.rs +++ b/crates/tracedecay-application/src/feedback/cycle_runtime.rs @@ -1,7 +1,9 @@ //! Concrete one-shot feedback-cycle composition over existing authorities. +use std::future::Future; use std::ops::Deref; use std::path::{Path, PathBuf}; +use std::pin::Pin; use std::sync::Arc; use std::time::Instant; @@ -380,14 +382,25 @@ impl FeedbackCycleRuntime { invocation: FeedbackCycleInvocation, ) -> Result { invocation.validate()?; - if !self.admits_current_provider_set(&invocation.request).await - || !self.admits_provider_set(&invocation.request.providers) - { + let admitted: Pin + Send + '_>> = + Box::pin(self.admits_current_provider_set(&invocation.request)); + if !admitted.await || !self.admits_provider_set(&invocation.request.providers) { return Err(FeedbackCycleRuntimeError::ProviderSetMismatch); } let FeedbackCycleInvocation { context, request } = invocation; let requested_durability = request.input.request.durability(); - let execution = self.service.execute(&context, request).await?; + let execution: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_contracts::feedback::FeedbackCycleExecutionResult, + tracedecay_contracts::ApplicationContractError, + >, + > + Send + + '_, + >, + > = Box::pin(self.service.execute(&context, request)); + let execution = execution.await?; compose_canonical_result(&self.feedback, execution, requested_durability) } @@ -660,7 +673,14 @@ impl FeedbackCycleRuntimePort for FeedbackCycleRuntime { admission_duration_micros, ), ); - let result = Box::pin(runtime.run_once(invocation)).await; + let result: Pin< + Box< + dyn Future< + Output = Result, + > + Send, + >, + > = Box::pin(runtime.run_once(invocation)); + let result = result.await; let duration_micros = tracedecay_runtime_core::tracedecay::saturating_duration_micros( started_at.elapsed(), ); diff --git a/crates/tracedecay-application/src/graph_health_delta.rs b/crates/tracedecay-application/src/graph_health_delta.rs index 06cf7649c7..5586be64c2 100644 --- a/crates/tracedecay-application/src/graph_health_delta.rs +++ b/crates/tracedecay-application/src/graph_health_delta.rs @@ -4,6 +4,8 @@ //! opening an admitted, generation-pinned graph reader. use std::collections::BTreeMap; +use std::future::Future; +use std::pin::Pin; use tracedecay_contracts::retrieval::{ HealthDeltaCoverageV1, HealthDeltaCurrentnessV1, HealthDeltaPointV1, HealthDeltaResult, @@ -278,7 +280,18 @@ async fn find_health_delta_point( ) -> Result> { health_delta_digest_from_cursor(cursor)?; let port = RegisteredObservabilityPortV1::new(db); - port.read_exact(scope.scope_digest.as_str(), cursor) + let stored: Pin< + Box< + dyn Future< + Output = std::result::Result< + Option, + ApplicationContractError, + >, + > + Send + + '_, + >, + > = Box::pin(port.read_exact(scope.scope_digest.as_str(), cursor)); + stored .await .map_err(|error| storage("failed to read health-delta observation", error))? .map(|envelope| verify_health_delta_point(scope, envelope)) @@ -427,29 +440,30 @@ pub async fn compute_verified_health_delta( let scope = health_delta_scope(project_id, path_prefix)?; let current_cursor = health_delta_cursor(&scope, graph.generation().as_str())?; let pinned_before = if let Some(cursor) = before_cursor { - let stored = tracing::Instrument::instrument( - load_health_delta_point(db, &scope, cursor), - tracing::trace_span!("usecases.graph.health_delta.load"), - ) - .await?; + let stored: Pin> + Send + '_>> = + Box::pin(load_health_delta_point(db, &scope, cursor)); + let stored = stored.await?; Some((stored, cursor.to_owned())) } else { None }; - let current = tracing::Instrument::instrument( - find_health_delta_point(db, &scope, ¤t_cursor), - tracing::trace_span!("usecases.graph.health_delta.find"), - ) - .await?; + let current: Pin>> + Send + '_>> = + Box::pin(find_health_delta_point(db, &scope, ¤t_cursor)); + let current = current.await?; let after = if let Some(after) = current { after } else { - let snapshot = tracing::Instrument::instrument( - compute_verified_health_snapshot(graph, scope.path_prefix.as_deref()), - tracing::trace_span!("usecases.graph.health_delta.snapshot"), - ) - .await - .map_err(HealthDeltaError::Graph)?; + let snapshot: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(compute_verified_health_snapshot( + graph, + scope.path_prefix.as_deref(), + )); + let snapshot = snapshot.await.map_err(HealthDeltaError::Graph)?; let observed_at = tracedecay_contracts::now_micros(); let dimensions = health_delta_dimensions(&snapshot); let watermark = health_delta_watermark( diff --git a/crates/tracedecay-application/src/lsp_runtime/feedback_source.rs b/crates/tracedecay-application/src/lsp_runtime/feedback_source.rs index f077d995a7..fd2c02c5c0 100644 --- a/crates/tracedecay-application/src/lsp_runtime/feedback_source.rs +++ b/crates/tracedecay-application/src/lsp_runtime/feedback_source.rs @@ -801,9 +801,10 @@ impl CanonicalContextProjectionAuthority for ConcreteFeedbackLspSource { }; } }; - let mut producer_state = advisory_projection_producer(&kind) - .map(|producer| advisory_projection_status(&cycle, producer).1) - .unwrap_or_else(|| producer_state_for_cycle(&cycle)); + let mut producer_state = advisory_projection_producer(&kind).map_or_else( + || producer_state_for_cycle(&cycle), + |producer| advisory_projection_status(&cycle, producer).1, + ); if coverage == ContextCoverage::Partial && producer_state == ContextProducerState::Complete { diff --git a/crates/tracedecay-application/src/observability/producer.rs b/crates/tracedecay-application/src/observability/producer.rs index 4ffa46de49..aa5eba35ac 100644 --- a/crates/tracedecay-application/src/observability/producer.rs +++ b/crates/tracedecay-application/src/observability/producer.rs @@ -216,6 +216,44 @@ struct ObservabilityProducerCoreV1 { worker: Mutex>>, } +// Proving these fields `Send + Sync` walks the registered database lease past +// the recursion limit (rust-lang/rust#159228). The const is that field proof; +// the impls keep every registry that stores this producer from repeating it. +unsafe impl Send for ObservabilityProducerCoreV1 {} +unsafe impl Sync for ObservabilityProducerCoreV1 {} + +const _: fn(&ObservabilityProducerCoreV1) = |value| { + fn assert_send_sync(_: &T) {} + let ObservabilityProducerCoreV1 { + db, + identity, + data, + control, + pending_drops, + total_dropped, + next_sequence, + state, + final_coverage_persisted, + durable_emission_lock, + deadlines, + emission_lock, + worker, + } = value; + assert_send_sync(db); + assert_send_sync(identity); + assert_send_sync(data); + assert_send_sync(control); + assert_send_sync(pending_drops); + assert_send_sync(total_dropped); + assert_send_sync(next_sequence); + assert_send_sync(state); + assert_send_sync(final_coverage_persisted); + assert_send_sync(durable_emission_lock); + assert_send_sync(deadlines); + assert_send_sync(emission_lock); + assert_send_sync(worker); +}; + pub struct BoundedObservabilityProducerV1 { core: Arc, /// The identity this frontend stamps on admitted envelopes. It differs @@ -923,12 +961,13 @@ async fn record_batch( return false; } let count = u64::try_from(envelopes.len()).unwrap_or(u64::MAX); - match timeout( - persistence_deadline, + // Erased so the worker's `Send` proof does not nest the batch write + // (rust-lang/rust#159228). + let recorded = tracedecay_runtime_core::erase_send!( record_observability_batch(db, envelopes), - ) - .await - { + Result, ApplicationContractError> + ); + match timeout(persistence_deadline, recorded).await { Ok(Ok(_)) => { *persisted = persisted.saturating_add(count); true @@ -998,10 +1037,10 @@ async fn settle_worker( for carried_drop in carried_drops { push_drop_range(&mut ranges, carried_drop); } - let emission_identity = owner_fact - .as_ref() - .map(|owner| owner.emission_identity.clone()) - .unwrap_or_else(|| identity_from_envelope(&envelope)); + let emission_identity = owner_fact.as_ref().map_or_else( + || identity_from_envelope(&envelope), + |owner| owner.emission_identity.clone(), + ); let sequence = if owner_fact.is_some() { state.next_sequence.fetch_add(1, Ordering::AcqRel) } else { diff --git a/crates/tracedecay-application/src/observability/producer/outbox.rs b/crates/tracedecay-application/src/observability/producer/outbox.rs index ca4667402e..2b47dfbec3 100644 --- a/crates/tracedecay-application/src/observability/producer/outbox.rs +++ b/crates/tracedecay-application/src/observability/producer/outbox.rs @@ -1,3 +1,6 @@ +use std::future::Future; +use std::pin::Pin; + use super::*; impl BoundedObservabilityProducerV1 { @@ -76,42 +79,81 @@ impl BoundedObservabilityProducerV1 { /// Durably admit one owner-derived fact before offering its exact delivery /// to the bounded worker. - pub async fn emit_owner_fact( + pub fn emit_owner_fact( &self, envelope: ObservabilityEnvelopeV1, - ) -> Result { - timeout( - self.core.deadlines.persistence, - self.emit_owner_fact_unbounded(envelope), - ) - .await - .map_err(|_| { - ApplicationContractError::Domain("observability_outbox_admission_deadline".to_owned()) - })? + ) -> Pin< + Box< + dyn Future< + Output = Result, + > + Send + + '_, + >, + > { + let persistence = self.core.deadlines.persistence; + Box::pin(async move { + timeout(persistence, self.emit_owner_fact_unbounded(envelope)) + .await + .map_err(|_| { + ApplicationContractError::Domain( + "observability_outbox_admission_deadline".to_owned(), + ) + })? + }) } /// Admit a bounded owner batch under one persistence deadline. A claim /// committed before cancellation remains pending for worker recovery. - pub async fn emit_owner_facts( + /// + /// The batch future only awaits already-erased admissions. Nesting each + /// owner fact's storage future inside this batch overflowed the next + /// solver's `Send` proof. + pub fn emit_owner_facts( &self, envelopes: Vec, - ) -> Result, ApplicationContractError> { - if envelopes.is_empty() || envelopes.len() > MAX_PRODUCER_CAPACITY { - return Err(ApplicationContractError::Domain( - "observability_owner_batch_bounds".to_owned(), - )); - } - timeout(self.core.deadlines.persistence, async { - let mut outcomes = Vec::with_capacity(envelopes.len()); - for envelope in envelopes { - outcomes.push(self.emit_owner_fact_unbounded(envelope).await?); + ) -> Pin< + Box< + dyn Future< + Output = Result< + Vec, + ApplicationContractError, + >, + > + Send + + '_, + >, + > { + let persistence = self.core.deadlines.persistence; + Box::pin(async move { + if envelopes.is_empty() || envelopes.len() > MAX_PRODUCER_CAPACITY { + return Err(ApplicationContractError::Domain( + "observability_owner_batch_bounds".to_owned(), + )); } - Ok(outcomes) + timeout(persistence, async { + let mut outcomes = Vec::with_capacity(envelopes.len()); + for envelope in envelopes { + let admitted: Pin< + Box< + dyn Future< + Output = Result< + ObservabilityOwnerEmissionOutcomeV1, + ApplicationContractError, + >, + > + Send + + '_, + >, + > = Box::pin(self.emit_owner_fact_unbounded(envelope)); + outcomes.push(admitted.await?); + } + Ok(outcomes) + }) + .await + .map_err(|_| { + ApplicationContractError::Domain( + "observability_outbox_admission_deadline".to_owned(), + ) + })? }) - .await - .map_err(|_| { - ApplicationContractError::Domain("observability_outbox_admission_deadline".to_owned()) - })? } async fn emit_owner_fact_unbounded( diff --git a/crates/tracedecay-application/src/primitives/production/extended_primitive.rs b/crates/tracedecay-application/src/primitives/production/extended_primitive.rs index 5a8c4415a7..89cc5ca139 100644 --- a/crates/tracedecay-application/src/primitives/production/extended_primitive.rs +++ b/crates/tracedecay-application/src/primitives/production/extended_primitive.rs @@ -1,7 +1,9 @@ //! Extended primitive port: module API, qualified names, diagnostics, and storage status history. use std::collections::HashMap; +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; use std::sync::{Arc, Mutex, OnceLock, PoisonError}; use sha2::{Digest, Sha256}; @@ -45,7 +47,7 @@ use super::{ }; use crate::diagnostics_publication::CodeIndexPublicationIdentityPortV1; use crate::diagnostics_query::{DiagnosticPageRequest, DiagnosticQueryCoverage, DiagnosticsQuery}; -use crate::graph_health_delta::compute_verified_health_delta; +use crate::graph_health_delta::{HealthDeltaError, compute_verified_health_delta}; use crate::lsp_runtime::LspCodeIndexProjectionIdentityPort; pub(super) fn public_module_symbols( @@ -807,15 +809,21 @@ impl ExtendedPrimitivePort for TraceDecayExtendedPrimitivePortV1 { } }; let query = GraphQueryManager::new(&reader, cancellation); - match compute_verified_health_delta( + let verified_delta: Pin< + Box< + dyn Future< + Output = std::result::Result, + > + Send + + '_, + >, + > = Box::pin(compute_verified_health_delta( Some(context.request.scope().project_id.as_str().to_owned()), &query, self.observation_database.as_ref(), request.before_cursor.as_deref(), request.path_prefix.as_deref(), - ) - .await - { + )); + match verified_delta.await { Ok(result) => completed(result, EvidenceDomain::Operational, now_observed()), Err(error) => health_delta_outcome(&error, observed_at), } diff --git a/crates/tracedecay-application/src/project_adoption.rs b/crates/tracedecay-application/src/project_adoption.rs index 0150bab20b..b599acdb8d 100644 --- a/crates/tracedecay-application/src/project_adoption.rs +++ b/crates/tracedecay-application/src/project_adoption.rs @@ -24,7 +24,9 @@ //! explicit init at the renamed path mints a fresh identity instead of //! offering the old one. +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; use tracedecay_daemon_protocol::MovedStoreAdoption; use tracedecay_domain::errors::{Result, TraceDecayError}; @@ -84,7 +86,13 @@ pub async fn adopt_moved_nongit_project( return refuse_if_adoption_conflicts(adoption, &existing.project.project_id, &new_root); } - let candidates = discover_moved_nongit_candidates(&new_root, profile_root, registry).await?; + let discovered: Pin>> + Send + '_>> = + Box::pin(discover_moved_nongit_candidates( + &new_root, + profile_root, + registry, + )); + let candidates = discovered.await?; let resuming = candidates .iter() .filter(|candidate| candidate.records_new_root) @@ -152,7 +160,10 @@ pub async fn adopt_moved_nongit_project( } }; - remap_moved_nongit_project(&new_root, profile_root, registry, selected).await + let remapped: Pin>> + Send + '_>> = Box::pin( + remap_moved_nongit_project(&new_root, profile_root, registry, selected), + ); + remapped.await } fn candidate_ids(candidates: &[MovedNongitCandidate]) -> String { diff --git a/crates/tracedecay-automation-runtime/src/automation/artifact_payloads.rs b/crates/tracedecay-automation-runtime/src/automation/artifact_payloads.rs index 8d72f4fa82..3ec8065cd5 100644 --- a/crates/tracedecay-automation-runtime/src/automation/artifact_payloads.rs +++ b/crates/tracedecay-automation-runtime/src/automation/artifact_payloads.rs @@ -79,22 +79,18 @@ pub(super) fn traces_payload(ctx: &ArtifactPayloadContext<'_>) -> Value { "input_hash": ctx.record.input_hash, "output_hash": ctx.record.output_hash, "context_keys": ctx.request.context.as_object() - .map(|object| object.keys().cloned().collect::>()) - .unwrap_or_default(), + .map_or_default(|object| object.keys().cloned().collect::>()), "curation_result": curation_result, }) } fn context_evidence_mode(context: &Value) -> Value { - let modes = context - .as_object() - .map(|object| { - object - .values() - .filter_map(|value| value.get("evidence_mode").and_then(Value::as_str)) - .collect::>() - }) - .unwrap_or_default(); + let modes = context.as_object().map_or_default(|object| { + object + .values() + .filter_map(|value| value.get("evidence_mode").and_then(Value::as_str)) + .collect::>() + }); if modes.contains(&"session_replay_with_grep") { json!("session_replay_with_grep") } else if modes.contains(&"grep_only") { diff --git a/crates/tracedecay-automation-runtime/src/automation/hermes_skill_bridge.rs b/crates/tracedecay-automation-runtime/src/automation/hermes_skill_bridge.rs index 37eed5425e..1f247abc13 100644 --- a/crates/tracedecay-automation-runtime/src/automation/hermes_skill_bridge.rs +++ b/crates/tracedecay-automation-runtime/src/automation/hermes_skill_bridge.rs @@ -448,17 +448,15 @@ fn read_bounded_regular_utf8( } fn parse_scalar_frontmatter(contents: &str) -> BTreeMap { - parse_skill_frontmatter(contents) - .map(|fields| { - fields - .into_iter() - .filter_map(|(key, value)| match value { - SkillFrontmatterValue::Scalar(value) => Some((key.to_ascii_lowercase(), value)), - SkillFrontmatterValue::Block(_) => None, - }) - .collect() - }) - .unwrap_or_default() + parse_skill_frontmatter(contents).map_or_default(|fields| { + fields + .into_iter() + .filter_map(|(key, value)| match value { + SkillFrontmatterValue::Scalar(value) => Some((key.to_ascii_lowercase(), value)), + SkillFrontmatterValue::Block(_) => None, + }) + .collect() + }) } #[cfg(test)] diff --git a/crates/tracedecay-automation-runtime/src/automation/lifecycle.rs b/crates/tracedecay-automation-runtime/src/automation/lifecycle.rs index 7c6025698f..16a8fc4e7b 100644 --- a/crates/tracedecay-automation-runtime/src/automation/lifecycle.rs +++ b/crates/tracedecay-automation-runtime/src/automation/lifecycle.rs @@ -1,9 +1,13 @@ use std::{ + future::Future, path::{Path, PathBuf}, + pin::Pin, sync::atomic::{AtomicBool, AtomicU64, Ordering}, sync::{Arc, OnceLock}, }; +use tracing::Instrument; + use serde_json::{Value, json}; use tracedecay_automation::backend::extract_json_object_prefix; use tracedecay_contracts::retained_surfaces::AutomationSkipReasonV1; @@ -506,75 +510,86 @@ pub(crate) fn task_skip_reason( None } -#[tracing::instrument(name = "automation.scheduler.gate", level = "trace", skip_all)] -async fn scheduler_gate_with_lock_retention( - config: &AutomationConfig, - executable: Option<&Path>, - dashboard_root: &Path, - sessions_db: &RegisteredGlobalDb, +/// The scheduler's gate and, when it admitted a run, that run's ledger task. +type GatedTask = (SchedulerGate, Option); + +fn scheduler_gate_with_lock_retention<'a>( + config: &'a AutomationConfig, + executable: Option<&'a Path>, + dashboard_root: &'a Path, + sessions_db: &'a RegisteredGlobalDb, task: AgentTaskKind, trigger: AutomationTrigger, - retained_state: Option<&RetainedAutomationState>, -) -> Result<(SchedulerGate, Option)> { - let scheduled = matches!( - trigger, - AutomationTrigger::Scheduler | AutomationTrigger::HostReceipt - ); + retained_state: Option<&'a RetainedAutomationState>, +) -> Pin> + Send + 'a>> { + let span = tracing::trace_span!("automation.scheduler.gate"); + Box::pin( + async move { + let scheduled = matches!( + trigger, + AutomationTrigger::Scheduler | AutomationTrigger::HostReceipt + ); - let lock_now_secs = current_timestamp(); - let Some(lock) = AutomationTaskLock::try_acquire( - dashboard_root, - task, - stale_lock_secs(config, task), - lock_now_secs, - ) - .await? - else { - super::scheduler_trace::observe_skip_reason(AutomationSkipReasonV1::SchedulerLockActive); - let summary = if scheduled { - Some(load_run_ledger_task_summary(dashboard_root, task, task_key(task)).await?) - } else { - None - }; - return Ok(( - SchedulerGate::Skip(AutomationSkipReasonV1::SchedulerLockActive), - summary, - )); - }; - let lock = retain_task_lock(lock, retained_state)?; - if !scheduled { - return Ok((SchedulerGate::Proceed(lock), None)); - } + let lock_now_secs = current_timestamp(); + let Some(lock) = AutomationTaskLock::try_acquire( + dashboard_root, + task, + stale_lock_secs(config, task), + lock_now_secs, + ) + .await? + else { + super::scheduler_trace::observe_skip_reason( + AutomationSkipReasonV1::SchedulerLockActive, + ); + let summary = if scheduled { + Some(load_run_ledger_task_summary(dashboard_root, task, task_key(task)).await?) + } else { + None + }; + return Ok(( + SchedulerGate::Skip(AutomationSkipReasonV1::SchedulerLockActive), + summary, + )); + }; + let lock = retain_task_lock(lock, retained_state)?; + if !scheduled { + return Ok((SchedulerGate::Proceed(lock), None)); + } - let summary = load_run_ledger_task_summary(dashboard_root, task, task_key(task)).await?; - let activity = load_session_activity(sessions_db).await; - let decision_now_secs = current_timestamp(); - let decision = if trigger == AutomationTrigger::HostReceipt { - super::scheduler::host_receipt_decision( - config, - executable, - task, - summary.records(), - activity, - decision_now_secs, - ) - } else { - schedule_decision( - config, - executable, - task, - summary.records(), - activity, - decision_now_secs, - ) - }; - if let Some(reason) = decision.skip_reason() { - super::scheduler_trace::observe_skip_reason(reason); - return Ok((SchedulerGate::Skip(reason), Some(summary))); - } + let summary = + load_run_ledger_task_summary(dashboard_root, task, task_key(task)).await?; + let activity = load_session_activity(sessions_db).await; + let decision_now_secs = current_timestamp(); + let decision = if trigger == AutomationTrigger::HostReceipt { + super::scheduler::host_receipt_decision( + config, + executable, + task, + summary.records(), + activity, + decision_now_secs, + ) + } else { + schedule_decision( + config, + executable, + task, + summary.records(), + activity, + decision_now_secs, + ) + }; + if let Some(reason) = decision.skip_reason() { + super::scheduler_trace::observe_skip_reason(reason); + return Ok((SchedulerGate::Skip(reason), Some(summary))); + } - super::scheduler_trace::observe_due(); - Ok((SchedulerGate::Proceed(lock), Some(summary))) + super::scheduler_trace::observe_due(); + Ok((SchedulerGate::Proceed(lock), Some(summary))) + } + .instrument(span), + ) } pub(crate) async fn task_run_gate( @@ -584,7 +599,7 @@ pub(crate) async fn task_run_gate( sessions_db: &RegisteredGlobalDb, task: AgentTaskKind, trigger: AutomationTrigger, -) -> Result<(SchedulerGate, Option)> { +) -> Result { task_run_gate_with_lock_retention( config, executable, @@ -605,7 +620,7 @@ pub(crate) async fn task_run_gate_for_retained_settlement( task: AgentTaskKind, trigger: AutomationTrigger, settlement_guard: &AutomationRunSettlementGuard, -) -> Result<(SchedulerGate, Option)> { +) -> Result { let retention = settlement_guard.retention(); task_run_gate_with_lock_retention( config, @@ -627,7 +642,7 @@ async fn task_run_gate_with_lock_retention( task: AgentTaskKind, trigger: AutomationTrigger, retained_state: Option<&RetainedAutomationState>, -) -> Result<(SchedulerGate, Option)> { +) -> Result { let (gate, records) = scheduler_gate_with_lock_retention( config, executable, diff --git a/crates/tracedecay-automation-runtime/src/automation/memory_curator/review.rs b/crates/tracedecay-automation-runtime/src/automation/memory_curator/review.rs index 90f16558f6..38cf9ea595 100644 --- a/crates/tracedecay-automation-runtime/src/automation/memory_curator/review.rs +++ b/crates/tracedecay-automation-runtime/src/automation/memory_curator/review.rs @@ -1,12 +1,15 @@ use std::collections::BTreeMap; +use std::future::Future; use std::path::Path; +use std::pin::Pin; use serde_json::{Value, json}; use tracedecay_domain::{FactEventId, FactId, FactOwnerV1}; -use tracedecay_session_memory::memory::MemoryApplication; +use tracedecay_session_memory::memory::{MemoryApplication, MemoryApplicationError}; use tracedecay_store::{ - ProjectMemoryFactListQueryV1, ProjectMemoryFactProjectionV1, ProjectMemoryFactStore, - ProjectMemoryGraphQueryV1, ProjectMemoryGraphStore, ProjectMemoryGraphTargetV1, + ProjectMemoryFactListQueryV1, ProjectMemoryFactPageV1, ProjectMemoryFactProjectionV1, + ProjectMemoryFactStore, ProjectMemoryGraphPageV1, ProjectMemoryGraphQueryV1, + ProjectMemoryGraphStore, ProjectMemoryGraphTargetV1, }; use tracedecay_domain::errors::Result; @@ -70,14 +73,21 @@ pub(super) async fn memory_curator_review Result { let limit = fact_review_limit.clamp(1, CURATION_FACT_REVIEW_LIMIT); - let page = memory - .list_project_memory_facts( + let listed: Pin< + Box< + dyn Future< + Output = std::result::Result, + > + Send + + '_, + >, + > = Box::pin( + memory.list_project_memory_facts( ProjectMemoryFactListQueryV1::new(owner.clone(), None, None, after_fact_id, limit) .map_err(memory_contract_error)?, run_control.read_control(), - ) - .await - .map_err(memory_application_error)?; + ), + ); + let page = listed.await.map_err(memory_application_error)?; let mut allowed_facts = BTreeMap::new(); let mut unavailable_count = 0usize; let facts = page @@ -113,8 +123,15 @@ pub(super) async fn memory_curator_review, + > + Send + + '_, + >, + > = Box::pin( + memory.project_memory_graph( ProjectMemoryGraphQueryV1::new( owner.clone(), allowed_facts.keys().cloned().collect(), @@ -122,9 +139,9 @@ pub(super) async fn memory_curator_review( let mut accepted = Vec::new(); let mut quarantined = Vec::new(); for proposal in proposals { - match validate_fact_candidate(memory, run_control, proposal, &citations).await? { + let validated: Pin> + Send + '_>> = + Box::pin(validate_fact_candidate( + memory, + run_control, + proposal, + &citations, + )); + match validated.await? { FactCandidateValidation::Accepted(value) => accepted.push(value), FactCandidateValidation::Quarantined(value) => quarantined.push(value), } diff --git a/crates/tracedecay-automation-runtime/src/automation/skill_targets.rs b/crates/tracedecay-automation-runtime/src/automation/skill_targets.rs index 60bfafcd0d..f4d86ceee9 100644 --- a/crates/tracedecay-automation-runtime/src/automation/skill_targets.rs +++ b/crates/tracedecay-automation-runtime/src/automation/skill_targets.rs @@ -506,8 +506,7 @@ fn target_marker_slug(target: SkillInstallTarget) -> &'static str { fn unique_overlay_sibling(overlay_root: &Path, suffix: &str) -> PathBuf { let nonce = SystemTime::now() .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or_default(); + .map_or_default(|duration| duration.as_nanos()); overlay_root.with_file_name(format!( ".{NATIVE_NAMESPACE_DIR}.{suffix}-{}-{nonce}", std::process::id() diff --git a/crates/tracedecay-automation-runtime/src/automation/skill_usage/overlap.rs b/crates/tracedecay-automation-runtime/src/automation/skill_usage/overlap.rs index 343aaa095c..0d08b27b7c 100644 --- a/crates/tracedecay-automation-runtime/src/automation/skill_usage/overlap.rs +++ b/crates/tracedecay-automation-runtime/src/automation/skill_usage/overlap.rs @@ -136,13 +136,12 @@ fn overlap_candidate(a: &ManagedSkill, b: &ManagedSkill) -> Option _ => property_value.is_some(), }; if !valid_type { - let suffix = expected_type - .map(|kind| format!(" {kind}")) - .unwrap_or_default(); + let suffix = expected_type.map_or_default(|kind| format!(" {kind}")); return Err(config_error(format!( "automation backend output must include a {property}{suffix}" ))); diff --git a/crates/tracedecay-cli/build-support/host_cli_fixture.rs b/crates/tracedecay-cli/build-support/host_cli_fixture.rs index d23db310e9..fb2ff61054 100644 --- a/crates/tracedecay-cli/build-support/host_cli_fixture.rs +++ b/crates/tracedecay-cli/build-support/host_cli_fixture.rs @@ -24,8 +24,9 @@ fn main() -> ExitCode { let raw_args: Vec = env::args().collect(); let (program, args) = raw_args .split_first() - .map(|(program, args)| (program.as_str(), args.to_vec())) - .unwrap_or(("", Vec::new())); + .map_or(("", Vec::new()), |(program, args)| { + (program.as_str(), args.to_vec()) + }); let Some(home) = admitted_home() else { eprintln!("host-cli fixture requires HOME or USERPROFILE"); diff --git a/crates/tracedecay-cli/src/commands/branch.rs b/crates/tracedecay-cli/src/commands/branch.rs index 251181682f..e0bf6a56db 100644 --- a/crates/tracedecay-cli/src/commands/branch.rs +++ b/crates/tracedecay-cli/src/commands/branch.rs @@ -123,8 +123,7 @@ fn handle_branch_action_inner( let parent = branch .get("parent") .and_then(serde_json::Value::as_str) - .map(|p| format!(" (from {p})")) - .unwrap_or_default(); + .map_or_default(|p| format!(" (from {p})")); let is_ready = branch .get("is_ready") .and_then(serde_json::Value::as_bool) @@ -470,9 +469,9 @@ async fn handle_branch_autotrack_action( message: "PR auto-tracking poll interval is not unsigned".to_owned(), }); }; - let effective_poll_secs = poll_secs - .map(|secs| secs.max(MIN_AUTO_TRACK_PR_POLL_SECS)) - .unwrap_or(current_poll_secs); + let effective_poll_secs = poll_secs.map_or(current_poll_secs, |secs| { + secs.max(MIN_AUTO_TRACK_PR_POLL_SECS) + }); let mut mutations = Vec::new(); if !current_enabled { mutations.push(super::settings::project_configuration_set( diff --git a/crates/tracedecay-cli/src/commands/daemon.rs b/crates/tracedecay-cli/src/commands/daemon.rs index a86d548f4f..fc3654390c 100644 --- a/crates/tracedecay-cli/src/commands/daemon.rs +++ b/crates/tracedecay-cli/src/commands/daemon.rs @@ -179,13 +179,23 @@ pub(crate) async fn daemon_tool_json( ) -> tracedecay_domain::errors::Result { tracing::trace!(name: "cli.daemon.tool", value = ?tool_name); let handshake = client_handshake(profile, project_path)?; + // Both calls are erased so every command's `Send` proof stops here + // (rust-lang/rust#159228). let result = tracing::Instrument::instrument( - tracedecay::daemon::call_default_tool(profile, &handshake, tool_name, arguments), + tracedecay_runtime_core::erase_send!( + tracedecay::daemon::call_default_tool(profile, &handshake, tool_name, arguments), + tracedecay_domain::errors::Result + ), tracing::trace_span!("cli.daemon.request"), ) .await?; - tracedecay::daemon::recover_truncated_tool_payload(profile, &handshake, tool_name, result, None) - .await + tracedecay_runtime_core::erase_send!( + tracedecay::daemon::recover_truncated_tool_payload( + profile, &handshake, tool_name, result, None + ), + tracedecay_domain::errors::Result + ) + .await } /// Deadline-carrying variant for CLI journeys that deliberately trigger a cold diff --git a/crates/tracedecay-cli/src/commands/storage.rs b/crates/tracedecay-cli/src/commands/storage.rs index 118f531759..026042925c 100644 --- a/crates/tracedecay-cli/src/commands/storage.rs +++ b/crates/tracedecay-cli/src/commands/storage.rs @@ -1,5 +1,7 @@ +use std::future::Future; use std::io::{self, BufRead, IsTerminal, Write}; use std::path::{Path, PathBuf}; +use std::pin::Pin; use std::time::Duration; use tracedecay_maintenance::retention::orphan_stores::dir_size_bytes; use tracedecay_runtime_core::config::{GLOBAL_DB_FILENAME, ProfileRoot}; @@ -230,8 +232,27 @@ pub(crate) async fn try_admit_profile_registry( return Ok(None); } let identity = tracedecay_daemon_identity::profile_identity::load_existing(&profile_root)?; - let registry = tracedecay_store_runtime::DaemonSessionRuntimeRegistryV1::open(identity).await?; - let profile_database = registry.profile_database().await?; + let registry: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + tracedecay_store_runtime::DaemonSessionRuntimeRegistryV1, + >, + > + Send, + >, + > = Box::pin(tracedecay_store_runtime::DaemonSessionRuntimeRegistryV1::open(identity)); + let registry = registry.await?; + let profile_database: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + tracedecay_global_db::RegisteredGlobalDbLeaseV1, + >, + > + Send + + '_, + >, + > = Box::pin(registry.profile_database()); + let profile_database = profile_database.await?; Ok(Some( ProfileRegistryMaintenanceRuntime::from_admitted_lease(profile_database), )) @@ -957,9 +978,10 @@ fn handle_list_inner( }; println!( "Total: {} on disk · {} tokens saved{}", - total_size - .map(tracedecay_runtime_core::text::format_bytes) - .unwrap_or_else(|| "—".to_string()), + total_size.map_or_else( + || "—".to_string(), + tracedecay_runtime_core::text::format_bytes + ), total_tokens_str, total_suffix ); diff --git a/crates/tracedecay-cli/src/cost_cmd.rs b/crates/tracedecay-cli/src/cost_cmd.rs index b2a22c66db..4bff847877 100644 --- a/crates/tracedecay-cli/src/cost_cmd.rs +++ b/crates/tracedecay-cli/src/cost_cmd.rs @@ -104,32 +104,26 @@ fn print_cost_csv(summary: &CostSummaryPayload, by_model: bool) { for model in &usage.by_model { let cost = model .cost_usd - .map(|cost| format!("{cost:.4}")) - .unwrap_or_else(|| "unavailable".to_owned()); + .map_or_else(|| "unavailable".to_owned(), |cost| format!("{cost:.4}")); let tokens = model .total_tokens - .map(|tokens| tokens.to_string()) - .unwrap_or_else(|| "unavailable".to_owned()); + .map_or_else(|| "unavailable".to_owned(), |tokens| tokens.to_string()); println!("{},{},{cost},{tokens}", model.provider, model.model); } } else { println!("total_cost_usd,input_tokens,output_tokens,tokens_saved,efficiency"); let total_cost = usage .total_cost_usd - .map(|cost| format!("{cost:.4}")) - .unwrap_or_else(|| "unavailable".to_owned()); + .map_or_else(|| "unavailable".to_owned(), |cost| format!("{cost:.4}")); let input = usage .total_input_tokens - .map(|tokens| tokens.to_string()) - .unwrap_or_else(|| "unavailable".to_owned()); + .map_or_else(|| "unavailable".to_owned(), |tokens| tokens.to_string()); let output = usage .total_output_tokens - .map(|tokens| tokens.to_string()) - .unwrap_or_else(|| "unavailable".to_owned()); + .map_or_else(|| "unavailable".to_owned(), |tokens| tokens.to_string()); let efficiency = summary .efficiency_ratio - .map(|ratio| format!("{ratio:.4}")) - .unwrap_or_else(|| "unavailable".to_owned()); + .map_or_else(|| "unavailable".to_owned(), |ratio| format!("{ratio:.4}")); println!( "{total_cost},{input},{output},{},{efficiency}", summary.tokens_saved @@ -148,16 +142,17 @@ fn print_model_table(summary: &CostSummaryPayload) { .total_cost_usd .zip(model.cost_usd) .filter(|(total, _)| *total > 0.0) - .map(|(total, cost)| format!("{:.0}%", cost / total * 100.0)) - .unwrap_or_else(|| "n/a".to_owned()); - let token_count = model - .total_tokens - .map(tracedecay_runtime_core::text::format_token_count) - .unwrap_or_else(|| "unknown".to_owned()); + .map_or_else( + || "n/a".to_owned(), + |(total, cost)| format!("{:.0}%", cost / total * 100.0), + ); + let token_count = model.total_tokens.map_or_else( + || "unknown".to_owned(), + tracedecay_runtime_core::text::format_token_count, + ); let cost = model .cost_usd - .map(|cost| format!("${cost:.2}")) - .unwrap_or_else(|| "unavailable".to_owned()); + .map_or_else(|| "unavailable".to_owned(), |cost| format!("${cost:.2}")); println!( " {:<12} {:<24} {:>10} {:>10} {:>6}", model.provider, model.model, cost, token_count, share @@ -216,18 +211,16 @@ fn print_cost_row( let denominator = input.checked_add(cache_read)?; (denominator > 0).then_some((cache_read as f64 / denominator as f64) * 100.0) }); - let input = input - .map(tracedecay_runtime_core::text::format_token_count) - .unwrap_or_else(|| "unknown".to_owned()); - let output = output - .map(tracedecay_runtime_core::text::format_token_count) - .unwrap_or_else(|| "unknown".to_owned()); - let cost = cost - .map(|cost| format!("${cost:.2}")) - .unwrap_or_else(|| "unavailable".to_owned()); - let cache = cache_pct - .map(|percent| format!("{percent:.0}%")) - .unwrap_or_else(|| "unknown".to_owned()); + let input = input.map_or_else( + || "unknown".to_owned(), + tracedecay_runtime_core::text::format_token_count, + ); + let output = output.map_or_else( + || "unknown".to_owned(), + tracedecay_runtime_core::text::format_token_count, + ); + let cost = cost.map_or_else(|| "unavailable".to_owned(), |cost| format!("${cost:.2}")); + let cache = cache_pct.map_or_else(|| "unknown".to_owned(), |percent| format!("{percent:.0}%")); println!( " {:<10} {:>10} {:>10} {:>10} {:>10}", label, cost, input, output, cache diff --git a/crates/tracedecay-cli/src/lsp_cmd.rs b/crates/tracedecay-cli/src/lsp_cmd.rs index fccafc1963..849756e45d 100644 --- a/crates/tracedecay-cli/src/lsp_cmd.rs +++ b/crates/tracedecay-cli/src/lsp_cmd.rs @@ -83,8 +83,7 @@ async fn run_stdio_bridge( .map(|binding| binding.canonical_root_uri.clone()), initialize .as_ref() - .map(|binding| binding.workspace_folders.clone()) - .unwrap_or_default(), + .map_or_default(|binding| binding.workspace_folders.clone()), deadline, cancellation, ) diff --git a/crates/tracedecay-cli/src/main.rs b/crates/tracedecay-cli/src/main.rs index 6afbb3ee3f..92cf52b7dd 100644 --- a/crates/tracedecay-cli/src/main.rs +++ b/crates/tracedecay-cli/src/main.rs @@ -302,8 +302,7 @@ fn try_print_cli_version(args: &[std::ffi::OsString]) -> Option { fn tokio_blocking_thread_limit() -> usize { let available = std::thread::available_parallelism().map_or(1, usize::from); let effective = tracedecay_code_index::parallelism::installed_worker_status() - .map(|status| usize::from(status.effective_workers)) - .unwrap_or(available); + .map_or(available, |status| usize::from(status.effective_workers)); tokio_blocking_thread_limit_from(available, effective) } diff --git a/crates/tracedecay-cli/src/tool_command.rs b/crates/tracedecay-cli/src/tool_command.rs index 3e4e8bd7ef..b74e3ff6c9 100644 --- a/crates/tracedecay-cli/src/tool_command.rs +++ b/crates/tracedecay-cli/src/tool_command.rs @@ -328,8 +328,7 @@ fn run_inner( let canonical = canonical_tool_name(&raw_name); let Some(def) = defs.iter().find(|definition| definition.name == canonical) else { let suggestion = nearest_tool_name(&canonical, defs) - .map(|name| format!(" Did you mean '{name}'?")) - .unwrap_or_default(); + .map_or_default(|name| format!(" Did you mean '{name}'?")); return Err(TraceDecayError::project_route( "unknown_tool", false, @@ -541,8 +540,9 @@ fn cli_request_controls( let remaining = deadline.saturating_duration_since(Instant::now()); let now = SystemTime::now() .duration_since(UNIX_EPOCH) - .map(|duration| i64::try_from(duration.as_micros()).unwrap_or(i64::MAX)) - .unwrap_or(i64::MAX); + .map_or(i64::MAX, |duration| { + i64::try_from(duration.as_micros()).unwrap_or(i64::MAX) + }); let request_deadline = Deadline::new(UtcMicros( now.saturating_add(i64::try_from(remaining.as_micros()).unwrap_or(i64::MAX)), )) @@ -1133,14 +1133,13 @@ fn tool_result_process_outcome(result_value: &Value, tool_name: &str) -> Result< let untested = result_value .pointer("/structuredContent/test_gate/untested") .and_then(Value::as_array) - .map(|items| { + .map_or_default(|items| { items .iter() .filter_map(Value::as_str) .collect::>() .join(", ") - }) - .unwrap_or_default(); + }); std::io::stdout().flush()?; return Err(TraceDecayError::tool_refused( tool_name, diff --git a/crates/tracedecay-cli/src/tool_command/args.rs b/crates/tracedecay-cli/src/tool_command/args.rs index adb1c32062..990d95ce25 100644 --- a/crates/tracedecay-cli/src/tool_command/args.rs +++ b/crates/tracedecay-cli/src/tool_command/args.rs @@ -390,8 +390,7 @@ fn unknown_key_error( required: &[String], ) -> TraceDecayError { let suggestion = nearest_key(key, props) - .map(|k| format!(", did you mean `--{}`?", k.replace('_', "-"))) - .unwrap_or_default(); + .map_or_default(|k| format!(", did you mean `--{}`?", k.replace('_', "-"))); let mut valid: Vec = props .keys() .map(|k| { @@ -475,12 +474,11 @@ fn schema_required_keys(def: &ToolDefinition) -> Vec { def.input_schema .get("required") .and_then(Value::as_array) - .map(|arr| { + .map_or_default(|arr| { arr.iter() .filter_map(|v| v.as_str().map(String::from)) .collect() }) - .unwrap_or_default() } fn max_typo_distance(name: &str) -> usize { diff --git a/crates/tracedecay-cli/src/upgrade/attestation/tests.rs b/crates/tracedecay-cli/src/upgrade/attestation/tests.rs index 9efed145d9..e1f996e7b3 100644 --- a/crates/tracedecay-cli/src/upgrade/attestation/tests.rs +++ b/crates/tracedecay-cli/src/upgrade/attestation/tests.rs @@ -71,9 +71,7 @@ fn verify_served( ) -> (Result, Vec) { let server = FakeGitHub::bind(); let base = server.base.clone(); - let responses = body - .map(|body| vec![(attestations_path(hex_digest), body)]) - .unwrap_or_default(); + let responses = body.map_or_default(|body| vec![(attestations_path(hex_digest), body)]); let requests = server.serve(responses); let provenance = ReleaseProvenance::for_release(&base, None, tag, is_beta); let outcome = verify_release_attestation(&provenance, digest(hex_digest)); diff --git a/crates/tracedecay-cli/tests/core_cli_suite/cli_non_interactive_test.rs b/crates/tracedecay-cli/tests/core_cli_suite/cli_non_interactive_test.rs index 1765e579ba..2cefe0ce31 100644 --- a/crates/tracedecay-cli/tests/core_cli_suite/cli_non_interactive_test.rs +++ b/crates/tracedecay-cli/tests/core_cli_suite/cli_non_interactive_test.rs @@ -1101,9 +1101,7 @@ fn child_output( stderr: Option>>, ) -> Output { let join = |handle: Option>>| { - handle - .map(|handle| handle.join().expect("child pipe drain thread panicked")) - .unwrap_or_default() + handle.map_or_default(|handle| handle.join().expect("child pipe drain thread panicked")) }; Output { status, @@ -1507,19 +1505,17 @@ fn install_codex_automation_enables_daemon_owned_project_configuration_nonintera ); let projects_dir = profile_root(home.path()).join("projects"); - let sidecars = std::fs::read_dir(&projects_dir) - .map(|entries| { - entries - .map(|entry| { - entry - .unwrap() - .path() - .join("dashboard/automation_config.json") - }) - .filter(|path| path.is_file()) - .collect::>() - }) - .unwrap_or_default(); + let sidecars = std::fs::read_dir(&projects_dir).map_or_default(|entries| { + entries + .map(|entry| { + entry + .unwrap() + .path() + .join("dashboard/automation_config.json") + }) + .filter(|path| path.is_file()) + .collect::>() + }); assert!( sidecars.is_empty(), "automation install must not write retired dashboard sidecars: {sidecars:?}" diff --git a/crates/tracedecay-cli/tests/core_cli_suite/source_provenance_test.rs b/crates/tracedecay-cli/tests/core_cli_suite/source_provenance_test.rs index 5647b17bc5..f59b31095b 100644 --- a/crates/tracedecay-cli/tests/core_cli_suite/source_provenance_test.rs +++ b/crates/tracedecay-cli/tests/core_cli_suite/source_provenance_test.rs @@ -495,13 +495,26 @@ impl CargoFixture { } fn build_script_runs(&self) -> u32 { + // Dated nightly cargo nests build-script units at + // `build///out` instead of `build/-/out`. let build_directory = self.target.join("debug/build"); - let counters = std::fs::read_dir(build_directory) - .expect("fixture build output directory") - .flatten() - .map(|entry| entry.path().join("out/build-script-runs")) - .filter(|path| path.is_file()) - .collect::>(); + let mut counters = Vec::new(); + let mut pending = vec![build_directory]; + while let Some(directory) = pending.pop() { + for entry in std::fs::read_dir(&directory) + .expect("fixture build output directory") + .flatten() + { + let path = entry.path(); + if path.is_dir() { + pending.push(path); + } else if path.file_name().and_then(|name| name.to_str()) + == Some("build-script-runs") + { + counters.push(path); + } + } + } assert_eq!( counters.len(), 1, diff --git a/crates/tracedecay-cli/tests/core_cli_suite/tracedecay_test.rs b/crates/tracedecay-cli/tests/core_cli_suite/tracedecay_test.rs index c7091b635d..82ed2b50d0 100644 --- a/crates/tracedecay-cli/tests/core_cli_suite/tracedecay_test.rs +++ b/crates/tracedecay-cli/tests/core_cli_suite/tracedecay_test.rs @@ -208,10 +208,10 @@ fn project_path_flags_resolve_a_relative_path_from_inside_the_project() { "sessions import --project-path . must import into the CLI's project\nstdout:\n{stdout}\nstderr:\n{}", String::from_utf8_lossy(&import.stderr) ); - let sync_status = stdout - .split_once("Check status with: ") - .map(|(_, command)| shell_words::split(command.trim_end()).unwrap()) - .unwrap_or_else(|| panic!("the import hint must name its status command: {stdout}")); + let sync_status = stdout.split_once("Check status with: ").map_or_else( + || panic!("the import hint must name its status command: {stdout}"), + |(_, command)| shell_words::split(command.trim_end()).unwrap(), + ); assert_eq!( sync_status[..4], ["tracedecay", "sessions", "sync-status", "--idempotency-key"], diff --git a/crates/tracedecay-cli/tests/work_route_exposure_conformance.rs b/crates/tracedecay-cli/tests/work_route_exposure_conformance.rs index 703a2f12f4..c891b04cb9 100644 --- a/crates/tracedecay-cli/tests/work_route_exposure_conformance.rs +++ b/crates/tracedecay-cli/tests/work_route_exposure_conformance.rs @@ -305,8 +305,7 @@ impl DashboardProcess { self.base_url, self.diagnostics .lock() - .map(|captured| captured.clone()) - .unwrap_or_default() + .map_or_default(|captured| captured.clone()) ); std::thread::sleep(Duration::from_millis(200)); } diff --git a/crates/tracedecay-code-extraction/src/bash_extractor.rs b/crates/tracedecay-code-extraction/src/bash_extractor.rs index 90ff53fa43..601746c8b4 100644 --- a/crates/tracedecay-code-extraction/src/bash_extractor.rs +++ b/crates/tracedecay-code-extraction/src/bash_extractor.rs @@ -265,8 +265,7 @@ impl BashExtractor { fn visit_command(state: &mut ExtractionState, node: TsNode<'_>) { let cmd_name = node .child_by_field_name("name") - .map(|n| state.node_text(n)) - .unwrap_or_default(); + .map_or_default(|n| state.node_text(n)); if cmd_name == "source" || cmd_name == "." { // Extract the source/dot import path from the first argument. diff --git a/crates/tracedecay-code-extraction/src/basic_common.rs b/crates/tracedecay-code-extraction/src/basic_common.rs index f4634b077d..c170d8d25d 100644 --- a/crates/tracedecay-code-extraction/src/basic_common.rs +++ b/crates/tracedecay-code-extraction/src/basic_common.rs @@ -270,11 +270,7 @@ fn visit_let_statement(state: &mut ExtractionState, basic_line: &BasicLine<'_>) /// Extract GOSUB/GOTO references from top-level lines (those not part of subroutines). pub(crate) fn extract_top_level_calls(state: &mut ExtractionState, lines: &[BasicLine<'_>]) { - let file_node_id = state - .node_stack - .last() - .map(|(_, id)| id.clone()) - .unwrap_or_default(); + let file_node_id = state.node_stack.last().map_or_default(|(_, id)| id.clone()); for_each_top_level_line(lines, |line| { walk_for_calls(state, line.node, &file_node_id); }); diff --git a/crates/tracedecay-code-extraction/src/c_extractor.rs b/crates/tracedecay-code-extraction/src/c_extractor.rs index a63f592b9b..28c7d09c94 100644 --- a/crates/tracedecay-code-extraction/src/c_extractor.rs +++ b/crates/tracedecay-code-extraction/src/c_extractor.rs @@ -32,8 +32,7 @@ impl<'s> ExtractionState<'s> { let end = node .child_by_field_name("body") .or_else(|| find_direct_child_by_kind(node, "compound_statement")) - .map(|body| body.start_byte()) - .unwrap_or_else(|| node.end_byte()); + .map_or_else(|| node.end_byte(), |body| body.start_byte()); self.text_before(node, end) .trim() .trim_end_matches(';') diff --git a/crates/tracedecay-code-extraction/src/cpp_extractor.rs b/crates/tracedecay-code-extraction/src/cpp_extractor.rs index 557f179916..e92406d754 100644 --- a/crates/tracedecay-code-extraction/src/cpp_extractor.rs +++ b/crates/tracedecay-code-extraction/src/cpp_extractor.rs @@ -104,8 +104,7 @@ impl<'s> ExtractionState<'s> { .or_else(|| find_direct_child_by_kind(node, "field_declaration_list")) .or_else(|| find_direct_child_by_kind(node, "declaration_list")) .or_else(|| find_direct_child_by_kind(node, "enumerator_list")) - .map(|body| body.start_byte()) - .unwrap_or_else(|| node.end_byte()); + .map_or_else(|| node.end_byte(), |body| body.start_byte()); self.text_before(node, end) .trim() .trim_end_matches(';') diff --git a/crates/tracedecay-code-extraction/src/cpp_extractor/functions.rs b/crates/tracedecay-code-extraction/src/cpp_extractor/functions.rs index a0e082fc68..e76bd96fef 100644 --- a/crates/tracedecay-code-extraction/src/cpp_extractor/functions.rs +++ b/crates/tracedecay-code-extraction/src/cpp_extractor/functions.rs @@ -180,9 +180,10 @@ impl CppExtractor { node: TsNode<'_>, name: Option<&str>, ) { - let name = name - .map(str::to_string) - .unwrap_or_else(|| Self::extract_destructor_name(state, node)); + let name = name.map_or_else( + || Self::extract_destructor_name(state, node), + str::to_string, + ); let signature = Some(Self::extract_function_signature(state, node)); let docstring = Self::extract_docstring(state, node); let start_line = node.start_position().row as u32; diff --git a/crates/tracedecay-code-extraction/src/csharp_extractor.rs b/crates/tracedecay-code-extraction/src/csharp_extractor.rs index 9cc3935f09..0ead568a0d 100644 --- a/crates/tracedecay-code-extraction/src/csharp_extractor.rs +++ b/crates/tracedecay-code-extraction/src/csharp_extractor.rs @@ -722,8 +722,7 @@ impl CSharpExtractor { let type_str = node .child_by_field_name("type") - .map(|n| state.node_text(n).to_string()) - .unwrap_or_default(); + .map_or_default(|n| state.node_text(n).to_string()); let sig = format!("{type_str} {name}"); let graph_node = Node { diff --git a/crates/tracedecay-code-extraction/src/go_extractor.rs b/crates/tracedecay-code-extraction/src/go_extractor.rs index bd0fe451b0..0a45448fc5 100644 --- a/crates/tracedecay-code-extraction/src/go_extractor.rs +++ b/crates/tracedecay-code-extraction/src/go_extractor.rs @@ -1436,12 +1436,10 @@ impl GoExtractor { ) -> GoMethodSignatureV1 { let name = node .child_by_field_name("name") - .map(|name| state.node_text(name).to_string()) - .unwrap_or_default(); + .map_or_default(|name| state.node_text(name).to_string()); let params = node .child_by_field_name("parameters") - .map(|list| Self::parameter_types(state, list, type_params)) - .unwrap_or_default(); + .map_or_default(|list| Self::parameter_types(state, list, type_params)); let results = match node.child_by_field_name("result") { Some(list) if list.kind() == "parameter_list" => { Self::parameter_types(state, list, type_params) diff --git a/crates/tracedecay-code-extraction/src/hlsl_extractor.rs b/crates/tracedecay-code-extraction/src/hlsl_extractor.rs index be8536c188..e547d3a3b8 100644 --- a/crates/tracedecay-code-extraction/src/hlsl_extractor.rs +++ b/crates/tracedecay-code-extraction/src/hlsl_extractor.rs @@ -111,9 +111,7 @@ impl HlslExtractor { ); let body = node.child_by_field_name("body"); - let metrics = body - .map(|b| count_complexity(b, &C_COMPLEXITY, state.source)) - .unwrap_or_default(); + let metrics = body.map_or_default(|b| count_complexity(b, &C_COMPLEXITY, state.source)); let graph_node = Node { id: id.clone(), diff --git a/crates/tracedecay-code-extraction/src/markdown_extractor.rs b/crates/tracedecay-code-extraction/src/markdown_extractor.rs index 75645a7b19..11907d2524 100644 --- a/crates/tracedecay-code-extraction/src/markdown_extractor.rs +++ b/crates/tracedecay-code-extraction/src/markdown_extractor.rs @@ -322,8 +322,7 @@ impl MarkdownExtractor { // field, which points to an `(inline)` node containing the text. let title = node .child_by_field_name("heading_content") - .map(|n| state.node_text(n).trim().to_string()) - .unwrap_or_default(); + .map_or_default(|n| state.node_text(n).trim().to_string()); if title.is_empty() { // Still recurse into the heading body so any `(inline)` children diff --git a/crates/tracedecay-code-extraction/src/objc_extractor.rs b/crates/tracedecay-code-extraction/src/objc_extractor.rs index 4036513ac7..b9a744a0a0 100644 --- a/crates/tracedecay-code-extraction/src/objc_extractor.rs +++ b/crates/tracedecay-code-extraction/src/objc_extractor.rs @@ -279,8 +279,7 @@ impl ObjcExtractor { // Check if this is an NS_ENUM pattern if let Some(macro_spec) = find_direct_child_by_kind(node, "macro_type_specifier") { let macro_name = find_direct_child_by_kind(macro_spec, "identifier") - .map(|n| state.node_text(n).to_string()) - .unwrap_or_default(); + .map_or_default(|n| state.node_text(n).to_string()); if macro_name == "NS_ENUM" || macro_name == "NS_OPTIONS" { Self::visit_ns_enum(state, node, ¯o_spec); return; diff --git a/crates/tracedecay-code-extraction/src/perl_extractor.rs b/crates/tracedecay-code-extraction/src/perl_extractor.rs index 8fe6c1465f..c95a5b3848 100644 --- a/crates/tracedecay-code-extraction/src/perl_extractor.rs +++ b/crates/tracedecay-code-extraction/src/perl_extractor.rs @@ -389,8 +389,7 @@ impl PerlExtractor { if is_our { let var_name = left_node .child_by_field_name("variable_name") - .map(|n| state.node_text(n)) - .unwrap_or_default(); + .map_or_default(|n| state.node_text(n)); // Only treat ALL_CAPS variables as constants. let bare_name = var_name.trim_start_matches('$'); diff --git a/crates/tracedecay-code-extraction/src/php_extractor.rs b/crates/tracedecay-code-extraction/src/php_extractor.rs index f02045fefb..cb9975f0ed 100644 --- a/crates/tracedecay-code-extraction/src/php_extractor.rs +++ b/crates/tracedecay-code-extraction/src/php_extractor.rs @@ -672,8 +672,7 @@ impl PhpExtractor { fn visit_use_group(state: &mut ExtractionState, group: TsNode<'_>, use_node: TsNode<'_>) { // The group has a prefix and individual clauses. let prefix = find_direct_child_by_kind(group, "namespace_name") - .map(|n| state.node_text(n).to_string()) - .unwrap_or_default(); + .map_or_default(|n| state.node_text(n).to_string()); let mut cursor = group.walk(); if cursor.goto_first_child() { diff --git a/crates/tracedecay-code-extraction/src/powershell_extractor.rs b/crates/tracedecay-code-extraction/src/powershell_extractor.rs index bbe885c89c..b91af76912 100644 --- a/crates/tracedecay-code-extraction/src/powershell_extractor.rs +++ b/crates/tracedecay-code-extraction/src/powershell_extractor.rs @@ -243,8 +243,7 @@ impl PowerShellExtractor { fn visit_command(state: &mut ExtractionState, node: TsNode<'_>) { let cmd_name = node .child_by_field_name("command_name") - .map(|n| state.node_text(n)) - .unwrap_or_default(); + .map_or_default(|n| state.node_text(n)); if cmd_name == "Import-Module" { if let Some(elements) = node.child_by_field_name("command_elements") diff --git a/crates/tracedecay-code-extraction/src/python_extractor.rs b/crates/tracedecay-code-extraction/src/python_extractor.rs index 0e74a2c6b2..9c838fc554 100644 --- a/crates/tracedecay-code-extraction/src/python_extractor.rs +++ b/crates/tracedecay-code-extraction/src/python_extractor.rs @@ -612,8 +612,7 @@ impl PythonExtractor { fn visit_import_from(state: &mut ExtractionState<'_>, node: TsNode<'_>) { let module_name = find_direct_child_by_kind(node, "dotted_name") .or_else(|| find_direct_child_by_kind(node, "relative_import")) - .map(|n| state.node_text(n)) - .unwrap_or_default() + .map_or_default(|n| state.node_text(n)) .to_string(); // Find the imported names in the import list or a single name. diff --git a/crates/tracedecay-code-extraction/src/qbasic_extractor.rs b/crates/tracedecay-code-extraction/src/qbasic_extractor.rs index 32a53cad2f..7c09143bd8 100644 --- a/crates/tracedecay-code-extraction/src/qbasic_extractor.rs +++ b/crates/tracedecay-code-extraction/src/qbasic_extractor.rs @@ -492,11 +492,7 @@ impl QBasicExtractor { None => return, }; - let from_node_id = state - .node_stack - .last() - .map(|(_, id)| id.clone()) - .unwrap_or_default(); + let from_node_id = state.node_stack.last().map_or_default(|(_, id)| id.clone()); state.unresolved_refs.push(UnresolvedRef { from_node_id, diff --git a/crates/tracedecay-code-extraction/src/rust_extractor.rs b/crates/tracedecay-code-extraction/src/rust_extractor.rs index 5af24c84ff..1d76c828fb 100644 --- a/crates/tracedecay-code-extraction/src/rust_extractor.rs +++ b/crates/tracedecay-code-extraction/src/rust_extractor.rs @@ -348,8 +348,10 @@ impl RustExtractor { .parent_node_id() .and_then(|parent_id| state.nodes.iter().rev().find(|node| node.id == parent_id)) .filter(|parent| parent.kind == NodeKind::Trait) - .map(|parent| parent.visibility.clone()) - .unwrap_or_else(|| Self::extract_visibility(node, state)) + .map_or_else( + || Self::extract_visibility(node, state), + |parent| parent.visibility.clone(), + ) } else { Self::extract_visibility(node, state) }; diff --git a/crates/tracedecay-code-extraction/src/typescript_extractor/imports.rs b/crates/tracedecay-code-extraction/src/typescript_extractor/imports.rs index 44f6e04e64..3ce6724294 100644 --- a/crates/tracedecay-code-extraction/src/typescript_extractor/imports.rs +++ b/crates/tracedecay-code-extraction/src/typescript_extractor/imports.rs @@ -599,8 +599,7 @@ fn visit_require_object_pattern( let local_name = child .child_by_field_name("value") .or_else(|| child.named_child(1)) - .map(|value| binding_name(state, value)) - .unwrap_or_else(|| imported_name.clone()); + .map_or_else(|| imported_name.clone(), |value| binding_name(state, value)); push_evidence( state, module_specifier, diff --git a/crates/tracedecay-code-extraction/src/typescript_extractor/test_calls.rs b/crates/tracedecay-code-extraction/src/typescript_extractor/test_calls.rs index 4cf8b31796..a7263bc752 100644 --- a/crates/tracedecay-code-extraction/src/typescript_extractor/test_calls.rs +++ b/crates/tracedecay-code-extraction/src/typescript_extractor/test_calls.rs @@ -167,9 +167,8 @@ pub(super) fn visit_test_call(state: &mut ExtractionState<'_>, call: TsNode<'_>) let callback = test_call_callback(args); let is_async = callback.is_some_and(|cb| TypeScriptExtractor::has_child_kind(cb, "async")); - let metrics = callback - .map(|cb| count_complexity(cb, &TYPESCRIPT_COMPLEXITY, state.source)) - .unwrap_or_default(); + let metrics = + callback.map_or_default(|cb| count_complexity(cb, &TYPESCRIPT_COMPLEXITY, state.source)); let call_text = state.node_text(call); let signature = truncate_title(call_text.lines().next().unwrap_or_default()); diff --git a/crates/tracedecay-code-extraction/src/wgsl_extractor.rs b/crates/tracedecay-code-extraction/src/wgsl_extractor.rs index aeb457c4a6..e4938b247f 100644 --- a/crates/tracedecay-code-extraction/src/wgsl_extractor.rs +++ b/crates/tracedecay-code-extraction/src/wgsl_extractor.rs @@ -112,9 +112,7 @@ impl WgslExtractor { ); let body = find_direct_child_by_kind(node, "compound_statement"); - let metrics = body - .map(|b| count_complexity(b, &C_COMPLEXITY, state.source)) - .unwrap_or_default(); + let metrics = body.map_or_default(|b| count_complexity(b, &C_COMPLEXITY, state.source)); let graph_node = Node { id: id.clone(), diff --git a/crates/tracedecay-code-extraction/tests/main/support/edges.rs b/crates/tracedecay-code-extraction/tests/main/support/edges.rs index cf65f0cee1..1573a6d01a 100644 --- a/crates/tracedecay-code-extraction/tests/main/support/edges.rs +++ b/crates/tracedecay-code-extraction/tests/main/support/edges.rs @@ -9,12 +9,10 @@ pub fn edge_pairs( kind: tracedecay_domain::EdgeKind, ) -> Vec<(&str, &str)> { let name_of = |id: &str| { - result - .nodes - .iter() - .find(|n| n.id == id) - .map(|n| n.name.as_str()) - .unwrap_or_else(|| panic!("edge endpoint {id} is not an extracted node")) + result.nodes.iter().find(|n| n.id == id).map_or_else( + || panic!("edge endpoint {id} is not an extracted node"), + |n| n.name.as_str(), + ) }; result .edges diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/generation_transactions.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/generation_transactions.rs index 4f2b7bb24b..8dbedd8db1 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/generation_transactions.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/generation_transactions.rs @@ -491,13 +491,10 @@ pub(super) fn path_still_names_open_file( return Ok(false); } let opened_metadata = opened.metadata().map_err(storage)?; - if !metadata_identity_matches(admitted, &opened_metadata) - || !metadata_identity_matches(¤t, &opened_metadata) - { - return Ok(false); - } + let identity_matches = metadata_identity_matches(admitted, &opened_metadata) + && metadata_identity_matches(¤t, &opened_metadata); #[cfg(windows)] - { + if identity_matches { let named = match File::open(path) { Ok(file) => file, Err(error) @@ -521,7 +518,7 @@ pub(super) fn path_still_names_open_file( return Ok(false); } } - Ok(true) + Ok(identity_matches) } /// Whether two metadata snapshots name the same stable file identity. On diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/tests.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/tests.rs index 1f8fb4d2dc..5efa84d3c2 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/tests.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/tests.rs @@ -2056,16 +2056,13 @@ fn failed_receipt_commit_withdraws_the_graph_replay_pool_exposure() { "rollback must withdraw the graph replay pool exposure" ); let queued_releases = store.path().join(GRAPH_REPLAY_RELEASE_QUEUE_DIRECTORY); - let queued = std::fs::read_dir(&queued_releases) - .map(|entries| entries.count()) - .unwrap_or(0); + let queued = std::fs::read_dir(&queued_releases).map_or(0, |entries| entries.count()); assert_eq!(queued, 0, "rollback must remove the queued release events"); } fn queued_release_count(store_root: &Path) -> usize { std::fs::read_dir(store_root.join(GRAPH_REPLAY_RELEASE_QUEUE_DIRECTORY)) - .map(|entries| entries.count()) - .unwrap_or(0) + .map_or(0, |entries| entries.count()) } #[test] diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/text_artifacts.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/text_artifacts.rs index dc7ba45c4a..c5061766b0 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/text_artifacts.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/text_artifacts.rs @@ -237,10 +237,7 @@ pub(super) fn plan_collectable_text_artifacts_cancellable( // build authority: every staging and sidecar file in its // staging directory is crash debris and therefore a candidate. let mut referenced = BTreeMap::new(); - for entry in active_pointer - .map(|pointer| pointer.generation_index.as_slice()) - .unwrap_or_default() - { + for entry in active_pointer.map_or_default(|pointer| pointer.generation_index.as_slice()) { if let Some(descriptor) = entry.text_artifact() { validate_text_artifact_descriptor(descriptor)?; // Generations that sealed the same content name one artifact. diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/publication_store.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/publication_store.rs index 12e5accceb..8b70084ecf 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/publication_store.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/publication_store.rs @@ -3137,8 +3137,7 @@ impl CodeIndexAtomicPublicationPort for DaemonCodeIndexPublicationStoreV1 { let exact_git_evidence = self.exact_git_evidence(generation.snapshot())?; let mut generation_index = prior_pointer .as_ref() - .map(|pointer| pointer.generation_index.clone()) - .unwrap_or_default(); + .map_or_default(|pointer| pointer.generation_index.clone()); generation_index.retain(|entry| { entry.generation_id != generation.manifest().generation_id.as_str() && exact_git_evidence diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs index b603c7bcbf..51c9925156 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs @@ -904,7 +904,7 @@ pub(super) enum FreshnessProbeVerdictV1 { pub(super) enum RetainedTextGenerationRestoreV1 { Servable(LatestCodeTextGenerationV1), - Refused(VerifiedSealedTextGenerationMetadataV1), + Refused(Box), Failed(CodeIndexPublicationStoreErrorV1), } @@ -1025,7 +1025,7 @@ impl HistoricalCodeIndexGenerationOwnerV1 { let generation_id = metadata.manifest().generation_id.clone(); let mut progress_slot = CodeIndexBuildProgressSlotStateV1::default(); let text_progress_owner_epoch = progress_slot.replace_generation(generation_id); - LatestCodeTextGenerationV1 { + LatestCodeTextGenerationV1::from_shared(super::serving::CodeTextGenerationSharedV1 { metadata: Arc::new(metadata), sealed_format_revision, query_owners: Arc::new(RwLock::new(None)), @@ -1050,11 +1050,10 @@ impl HistoricalCodeIndexGenerationOwnerV1 { ), preopened_source: Arc::new(Mutex::new(None)), publication_binding: None, - graph_predecessor: Arc::default(), catalog_warm_settled: Arc::new(tokio::sync::Notify::new()), park_release_after_catalog_warm_armed: Arc::new(AtomicBool::new(false)), parked_graph_release: Arc::new(RwLock::new(None)), - } + }) } pub(super) fn bind_complete( @@ -2661,7 +2660,7 @@ impl CodeIndexWorktreeSchedulerV1 { if !compatibility.may_serve_while_rebuilding() { text_control.retire(); self.request_background_reconcile(); - return Some(RetainedTextGenerationRestoreV1::Refused(metadata)); + return Some(RetainedTextGenerationRestoreV1::Refused(Box::new(metadata))); } if !compatibility.is_reusable() { self.request_background_reconcile(); @@ -2683,7 +2682,7 @@ impl CodeIndexWorktreeSchedulerV1 { } let metadata = Arc::new(metadata); Some(RetainedTextGenerationRestoreV1::Servable( - LatestCodeTextGenerationV1 { + LatestCodeTextGenerationV1::from_shared(super::serving::CodeTextGenerationSharedV1 { metadata, sealed_format_revision, query_owners: Arc::new(RwLock::new(None)), @@ -2704,11 +2703,10 @@ impl CodeIndexWorktreeSchedulerV1 { generation_file: pointer.generation_file, state_digest: ManifestDigest::new(pointer.state_digest).ok()?, })), - graph_predecessor: Arc::default(), catalog_warm_settled: Arc::new(tokio::sync::Notify::new()), park_release_after_catalog_warm_armed: Arc::new(AtomicBool::new(false)), parked_graph_release: Arc::new(RwLock::new(None)), - }, + }), )) } @@ -3655,9 +3653,8 @@ impl CodeIndexWorktreeSchedulerV1 { let metadata = Arc::new( VerifiedSealedTextGenerationMetadataV1::from_published_generation(&generation), ); - let text = retained_text - .cloned() - .unwrap_or_else(|| LatestCodeTextGenerationV1 { + let text = retained_text.cloned().unwrap_or_else(|| { + LatestCodeTextGenerationV1::from_shared(super::serving::CodeTextGenerationSharedV1 { metadata, sealed_format_revision: tracedecay_code_index::production::SEALED_GENERATION_FORMAT_REVISION_V1, @@ -3683,11 +3680,11 @@ impl CodeIndexWorktreeSchedulerV1 { ), preopened_source: Arc::new(Mutex::new(None)), publication_binding: None, - graph_predecessor: Arc::default(), catalog_warm_settled: Arc::new(tokio::sync::Notify::new()), park_release_after_catalog_warm_armed: Arc::new(AtomicBool::new(false)), parked_graph_release: Arc::new(RwLock::new(None)), - }); + }) + }); LatestCompleteCodeIndexV1 { generation, text, @@ -4045,22 +4042,18 @@ impl CodeIndexWorktreeSchedulerV1 { }) .flatten() }); - let active_files = reusable_active - .as_ref() - .map(|active| { - active - .snapshot() - .files - .iter() - .filter(|file| { - remembered_dirty_paths - .is_none_or(|paths| !paths.contains(&file.logical_path)) - && self.row_follows_path_policy(file) - }) - .map(|file| (file.logical_path.as_str(), file)) - .collect::>() - }) - .unwrap_or_default(); + let active_files = reusable_active.as_ref().map_or_default(|active| { + active + .snapshot() + .files + .iter() + .filter(|file| { + remembered_dirty_paths.is_none_or(|paths| !paths.contains(&file.logical_path)) + && self.row_follows_path_policy(file) + }) + .map(|file| (file.logical_path.as_str(), file)) + .collect::>() + }); let files = candidate_paths .iter() .filter(|logical_path| !changed_paths.contains(*logical_path)) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs index 1668821d59..d5411e2c11 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs @@ -877,6 +877,92 @@ pub struct MountedCodeIndexWorktreeV1 { pub task: native_runtime::NativeTask<()>, } +// The scheduler registry stores these worktrees in a `BTreeMap`. One `Send` +// proof of that map walks every slot past the recursion limit +// (rust-lang/rust#159228); each field is checked on its own. +unsafe impl Send for MountedCodeIndexWorktreeV1 {} +unsafe impl Sync for MountedCodeIndexWorktreeV1 {} + +const _: fn(&MountedCodeIndexWorktreeV1) = |value| { + fn assert_send_sync(_: &T) {} + let MountedCodeIndexWorktreeV1 { + project_id, + repository_id, + worktree_id, + query_authority, + scheduler, + path_policy, + build_publication_lock, + historical_generation_owner, + serving_generation, + complete_generation_requested, + complete_generation_requested_changed, + memory_retry, + source_freshness, + last_reconciled_at_micros, + text_generation, + convergence_park, + generation_recovery, + serving_source_witness, + build_progress, + serving_generation_epoch, + serving_generation_changed, + serving_generation_installation, + graph_activation, + residency, + _residency_registration, + graph_cursor_retention, + ignored_dependency_admissions, + hints, + wake, + epoch, + pending_wake, + index_observability, + shutting_down, + reconcile_in_progress, + worker_phase, + _active_generation_encoded_bytes, + task, + } = value; + assert_send_sync(project_id); + assert_send_sync(repository_id); + assert_send_sync(worktree_id); + assert_send_sync(query_authority); + assert_send_sync(scheduler); + assert_send_sync(path_policy); + assert_send_sync(build_publication_lock); + assert_send_sync(historical_generation_owner); + assert_send_sync(serving_generation); + assert_send_sync(complete_generation_requested); + assert_send_sync(complete_generation_requested_changed); + assert_send_sync(memory_retry); + assert_send_sync(source_freshness); + assert_send_sync(last_reconciled_at_micros); + assert_send_sync(text_generation); + assert_send_sync(convergence_park); + assert_send_sync(generation_recovery); + assert_send_sync(serving_source_witness); + assert_send_sync(build_progress); + assert_send_sync(serving_generation_epoch); + assert_send_sync(serving_generation_changed); + assert_send_sync(serving_generation_installation); + assert_send_sync(graph_activation); + assert_send_sync(residency); + assert_send_sync(_residency_registration); + assert_send_sync(graph_cursor_retention); + assert_send_sync(ignored_dependency_admissions); + assert_send_sync(hints); + assert_send_sync(wake); + assert_send_sync(epoch); + assert_send_sync(pending_wake); + assert_send_sync(index_observability); + assert_send_sync(shutting_down); + assert_send_sync(reconcile_in_progress); + assert_send_sync(worker_phase); + assert_send_sync(_active_generation_encoded_bytes); + assert_send_sync(task); +}; + /// Unique mounted worktree for one admitted repo+worktree scope. /// /// Real mounts key the registry from the same canonical root that derives the @@ -1940,6 +2026,54 @@ pub struct CodeIndexSchedulerRegistryV1 { worker_shutdown_signals: Arc>>, } +// Git watch and mount admission prove this registry `Send` through the mounted +// worktree map. That walk exceeds the recursion limit (rust-lang/rust#159228); +// each field is checked on its own. +unsafe impl Send for CodeIndexSchedulerRegistryV1 {} +unsafe impl Sync for CodeIndexSchedulerRegistryV1 {} + +const _: fn(&CodeIndexSchedulerRegistryV1) = |value| { + fn assert_send_sync(_: &T) {} + let CodeIndexSchedulerRegistryV1 { + max_worktrees, + progress_daemon_incarnation, + next_progress_producer_incarnation, + resident_memory, + resident_owners, + byte_pool, + mounted, + retiring, + cold_mount_reservations, + background_reconcile_admission, + serving_generation_installation_tokens, + generation_publications, + serving_seats, + root_mounted, + cadence_telemetry, + activations, + test_attribution_authorities, + worker_shutdown_signals, + } = value; + assert_send_sync(max_worktrees); + assert_send_sync(progress_daemon_incarnation); + assert_send_sync(next_progress_producer_incarnation); + assert_send_sync(resident_memory); + assert_send_sync(resident_owners); + assert_send_sync(byte_pool); + assert_send_sync(mounted); + assert_send_sync(retiring); + assert_send_sync(cold_mount_reservations); + assert_send_sync(background_reconcile_admission); + assert_send_sync(serving_generation_installation_tokens); + assert_send_sync(generation_publications); + assert_send_sync(serving_seats); + assert_send_sync(root_mounted); + assert_send_sync(cadence_telemetry); + assert_send_sync(activations); + assert_send_sync(test_attribution_authorities); + assert_send_sync(worker_shutdown_signals); +}; + /// One worker's early-shutdown handles, registered when it is mounted. struct WorkerShutdownSignalV1 { shutting_down: Weak, diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs index 793ea967f5..03ae75631f 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs @@ -1401,7 +1401,7 @@ impl CodeIndexSchedulerRegistryV1 { continue; } Ok(Ok(Some(RetainedTextGenerationRestoreV1::Refused(metadata)))) => { - Some(metadata) + Some(*metadata) } Ok(Ok(Some(RetainedTextGenerationRestoreV1::Failed(error)))) => { tracing::error!( 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 3f31c5ce7c..c0276375c1 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 @@ -371,8 +371,7 @@ pub struct LatestCompleteCodeIndexV1 { pub(super) type ParkedGraphReleaseCallback = Arc; -#[derive(Clone)] -pub struct LatestCodeTextGenerationV1 { +pub struct CodeTextGenerationSharedV1 { pub(super) metadata: Arc, pub(super) sealed_format_revision: u32, pub(super) query_owners: Arc>>>, @@ -403,9 +402,6 @@ pub struct LatestCodeTextGenerationV1 { /// decoded merely to discover text metadata or source layout. pub(super) preopened_source: Arc>>, pub(super) publication_binding: Option>, - /// The outgoing owner whose warm graph serves reads, stale, until this - /// generation's own graph first warms. - pub(super) graph_predecessor: Arc>>, /// The first catalog warm finishes on a blocking task that does not /// wake the parked worker. One permit lets that worker retry park-release /// without posting a cadence arrival. @@ -418,6 +414,32 @@ pub struct LatestCodeTextGenerationV1 { pub(super) parked_graph_release: Arc>>, } +#[derive(Clone)] +pub struct LatestCodeTextGenerationV1 { + shared: Arc, + /// Nearest predecessor first. The chain is shared generation state, not + /// `Self`: a field of type `Self` makes `Send`/`Sync` recurse with no base + /// case (rust-lang/rust#159228). + predecessors: Arc>>>, +} + +impl std::ops::Deref for LatestCodeTextGenerationV1 { + type Target = CodeTextGenerationSharedV1; + + fn deref(&self) -> &Self::Target { + &self.shared + } +} + +impl LatestCodeTextGenerationV1 { + pub(super) fn from_shared(shared: CodeTextGenerationSharedV1) -> Self { + Self { + shared: Arc::new(shared), + predecessors: Arc::new(RwLock::new(Vec::new())), + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub(super) struct DurableActiveSealedGenerationBindingV1 { pub(super) generation_id: CodeGenerationId, @@ -1630,6 +1652,25 @@ pub(super) struct ProductionCodeGraphServingV1 { _graph_authority: CodeGraphServingAuthorityV1, } +// The serving graph's fields are `Send + Sync`, but proving that walks the +// canonical graph lease and the evidence lane past the recursion limit +// (rust-lang/rust#159228). The const check below is the field proof; these +// impls stop that walk from repeating inside every owner that holds this graph. +unsafe impl Send for ProductionCodeGraphServingV1 {} +unsafe impl Sync for ProductionCodeGraphServingV1 {} + +const _: fn(&ProductionCodeGraphServingV1) = |value| { + fn assert_send_sync(_: &T) {} + let ProductionCodeGraphServingV1 { + graph, + store, + _graph_authority, + } = value; + assert_send_sync(graph); + assert_send_sync(store); + assert_send_sync(_graph_authority); +}; + pub(super) enum CodeGraphActivationStateV1 { Pending, Refused(&'static str), @@ -2277,22 +2318,24 @@ impl LatestCodeTextGenerationV1 { let Some(outgoing) = outgoing.filter(|outgoing| !outgoing.same_text_owner(self)) else { return; }; - let inherited = outgoing.take_graph_predecessor(); + let inherited = outgoing.take_predecessors(); // An activated outgoing still serves: a released engine or catalog // reports `warming` but a read re-warms it, while a cold store passes // the hold down to the graph it was itself holding. let serves_warm_graph = outgoing.interactive_graph_store().is_ok() && outgoing.code_graph_serving_readiness().is_activated(); - let held = if serves_warm_graph { - Some(outgoing.clone()) + let mut held = if serves_warm_graph { + vec![Arc::clone(&outgoing.shared)] } else { inherited + }; + if held.first().is_some_and(|head| { + head.metadata.manifest().generation_id == self.metadata.manifest().generation_id + }) { + held.clear(); } - .filter(|held| { - held.metadata().manifest().generation_id != self.metadata().manifest().generation_id - }); *self - .graph_predecessor + .predecessors .write() .unwrap_or_else(PoisonError::into_inner) = held; } @@ -2308,11 +2351,7 @@ impl LatestCodeTextGenerationV1 { CodeGraphServingReadinessV1::Pending | CodeGraphServingReadinessV1::Warming { .. } ) { - return self - .graph_predecessor - .read() - .unwrap_or_else(PoisonError::into_inner) - .clone(); + return self.held_graph_predecessor(); } self.release_graph_predecessor(); None @@ -2322,21 +2361,32 @@ impl LatestCodeTextGenerationV1 { /// resident-memory inventory must see it: it holds its graph until /// released or dropped, not until a read next checks readiness. pub(super) fn held_graph_predecessor(&self) -> Option { - self.graph_predecessor + let chain = self + .predecessors .read() .unwrap_or_else(PoisonError::into_inner) - .clone() + .clone(); + let (head, tail) = chain.split_first()?; + Some(Self { + shared: Arc::clone(head), + predecessors: Arc::new(RwLock::new(tail.to_vec())), + }) } pub(super) fn release_graph_predecessor(&self) { - drop(self.take_graph_predecessor()); - } - - fn take_graph_predecessor(&self) -> Option { - self.graph_predecessor + self.predecessors .write() .unwrap_or_else(PoisonError::into_inner) - .take() + .clear(); + } + + fn take_predecessors(&self) -> Vec> { + std::mem::take( + &mut *self + .predecessors + .write() + .unwrap_or_else(PoisonError::into_inner), + ) } /// Return a graph refused for resident memory to `Pending`, so the next @@ -3802,8 +3852,7 @@ fn ensure_private_text_artifacts_root(path: &Path) -> Result<(), RetrievalPortEr re-enroll the store", path.display(), observed_unix_mode(path) - .map(|mode| format!(" (mode {mode:o}, need 700)")) - .unwrap_or_default(), + .map_or_default(|mode| format!(" (mode {mode:o}, need 700)")), )) }) } diff --git a/crates/tracedecay-code-index-runtime/src/git_transactions/native.rs b/crates/tracedecay-code-index-runtime/src/git_transactions/native.rs index 28e97981ef..34b72389d5 100644 --- a/crates/tracedecay-code-index-runtime/src/git_transactions/native.rs +++ b/crates/tracedecay-code-index-runtime/src/git_transactions/native.rs @@ -753,8 +753,7 @@ fn unsupported_hunk_selection( return Some(GitIndexUnsupportedStateV1::RenameOrCopy); } let line_count = parse_hunk_header(&hunk.hunk_header) - .map(|header| header.old_count.max(header.new_count)) - .unwrap_or_default(); + .map_or_default(|header| header.old_count.max(header.new_count)); if hunk.selected_line_bitmap != tracedecay_domain::full_hunk_selection_bitmap(line_count) { return Some(GitIndexUnsupportedStateV1::PartialHunkSelection); } diff --git a/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs b/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs index 4fa17c6a99..8ce91325be 100644 --- a/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs +++ b/crates/tracedecay-code-index-runtime/src/git_transactions/test_support.rs @@ -87,20 +87,18 @@ impl FakeNative { apply_modes: impl IntoIterator, recovery_modes: impl IntoIterator, ) -> Self { - let preview_results = preview - .map(|preview| { - VecDeque::from([GitIndexPreviewPortResultV1 { - execution: OperationReceipt::completed( - preview.created_at, - preview.created_at, - Deadline::new(preview.expires_at).expect("preview deadline"), - OperationBudgetUsage::default(), - ) - .expect("preview execution"), - preview, - }]) - }) - .unwrap_or_default(); + let preview_results = preview.map_or_default(|preview| { + VecDeque::from([GitIndexPreviewPortResultV1 { + execution: OperationReceipt::completed( + preview.created_at, + preview.created_at, + Deadline::new(preview.expires_at).expect("preview deadline"), + OperationBudgetUsage::default(), + ) + .expect("preview execution"), + preview, + }]) + }); Self { preview_results: Mutex::new(preview_results), apply_modes: Mutex::new(apply_modes.into_iter().collect()), diff --git a/crates/tracedecay-code-index-runtime/src/git_watch.rs b/crates/tracedecay-code-index-runtime/src/git_watch.rs index 1fde5ed6a7..829d9f371e 100644 --- a/crates/tracedecay-code-index-runtime/src/git_watch.rs +++ b/crates/tracedecay-code-index-runtime/src/git_watch.rs @@ -667,7 +667,9 @@ async fn repository_task(inner: Arc, state: Arc) { state.health.set_status(ProjectWatchStatus::Active); state.health.beat(); - match debounce_loop(&inner, &state, &cancellation).await { + let debounced: BoxFuture<'_, DebounceExit> = + Box::pin(debounce_loop(&inner, &state, &cancellation)); + match debounced.await { DebounceExit::Cancelled | DebounceExit::TimerUnavailable => return, DebounceExit::Reconfigure => { // A canceled debounce future may already have consumed the @@ -834,13 +836,12 @@ async fn debounce_loop( let quiet_deadline = last.map(|l| l + quiet); let hard_deadline = first.map(|f| f + max_delay); - let operation_state = match observe_operation_state( + let observed: BoxFuture<'_, OperationObservation> = Box::pin(observe_operation_state( Arc::clone(state), cancellation.clone(), affected_roots, - ) - .await - { + )); + let operation_state = match observed.await { OperationObservation::State(state) => state, OperationObservation::Cancelled => return DebounceExit::Cancelled, }; @@ -1038,8 +1039,23 @@ async fn observe_operation_state( /// scheduler owns exact source revision resolution, gix status, /// changed-candidate evidence, generation assembly, and its short CAS /// publication; this watcher owns none of those authorities. +/// +/// Boxed so the watch task's `Send` proof does not nest the whole freshness +/// request (rust-lang/rust#159228). +fn request_freshness_for_repository<'a>( + inner: &'a GitWatcherInner, + state: &'a Arc, + affected_roots: Option>, +) -> BoxFuture<'a, ()> { + Box::pin(request_freshness_for_repository_inner( + inner, + state, + affected_roots, + )) +} + #[tracing::instrument(name = "daemon.git.watch.freshness", level = "trace", skip_all)] -async fn request_freshness_for_repository( +async fn request_freshness_for_repository_inner( inner: &GitWatcherInner, state: &Arc, affected_roots: Option>, diff --git a/crates/tracedecay-code-index-runtime/src/git_watch/admission.rs b/crates/tracedecay-code-index-runtime/src/git_watch/admission.rs index 550367cda9..854771dc4d 100644 --- a/crates/tracedecay-code-index-runtime/src/git_watch/admission.rs +++ b/crates/tracedecay-code-index-runtime/src/git_watch/admission.rs @@ -253,10 +253,13 @@ impl GitWatcher { { break; } - match self - .ensure_watching_with_config(&project_root, &config) - .await - { + // Erased so the retry task's `Send` proof does not nest the whole + // admission (rust-lang/rust#159228). + let admitted = tracedecay_runtime_core::erase_send!( + self.ensure_watching_with_config(&project_root, &config), + GitWatcherAdmission + ); + match admitted.await { GitWatcherAdmission::IdentityUnavailable => { backoff = (backoff * 2).min(RESTART_BACKOFF_MAX); } diff --git a/crates/tracedecay-code-index-runtime/src/git_watch/state.rs b/crates/tracedecay-code-index-runtime/src/git_watch/state.rs index 9ff7bc802e..27ea8490d6 100644 --- a/crates/tracedecay-code-index-runtime/src/git_watch/state.rs +++ b/crates/tracedecay-code-index-runtime/src/git_watch/state.rs @@ -160,6 +160,74 @@ pub struct WatchState { pub retirement_probe: RetirementRaceProbe, } +// Futures that capture a watcher prove this state through the maintenance +// coordinator. That walk exceeds the recursion limit (rust-lang/rust#159228); +// each field is checked on its own. +unsafe impl Send for WatchState {} +unsafe impl Sync for WatchState {} + +const _: fn(&WatchState) = |value| { + fn assert_send_sync(_: &T) {} + let WatchState { + common_dir, + ownership, + dirty, + reconciliation_pending, + wake, + reconfigure, + retry_not_before, + retry_backoff_ms, + maintenance, + health, + task, + retirement, + #[cfg(test)] + retained_tasks, + #[cfg(test)] + entered_debounce, + #[cfg(test)] + heartbeat, + #[cfg(test)] + drained_plans, + #[cfg(test)] + plan_drained, + #[cfg(test)] + operation_held, + #[cfg(test)] + operation_scan_probe, + #[cfg(test)] + retirement_probe, + } = value; + assert_send_sync(common_dir); + assert_send_sync(ownership); + assert_send_sync(dirty); + assert_send_sync(reconciliation_pending); + assert_send_sync(wake); + assert_send_sync(reconfigure); + assert_send_sync(retry_not_before); + assert_send_sync(retry_backoff_ms); + assert_send_sync(maintenance); + assert_send_sync(health); + assert_send_sync(task); + assert_send_sync(retirement); + #[cfg(test)] + assert_send_sync(retained_tasks); + #[cfg(test)] + assert_send_sync(entered_debounce); + #[cfg(test)] + assert_send_sync(heartbeat); + #[cfg(test)] + assert_send_sync(drained_plans); + #[cfg(test)] + assert_send_sync(plan_drained); + #[cfg(test)] + assert_send_sync(operation_held); + #[cfg(test)] + assert_send_sync(operation_scan_probe); + #[cfg(test)] + assert_send_sync(retirement_probe); +}; + impl WatchState { #[cfg(test)] pub fn new( diff --git a/crates/tracedecay-code-index-runtime/tests/code_index_scheduler/reconcile.rs b/crates/tracedecay-code-index-runtime/tests/code_index_scheduler/reconcile.rs index f0519d12cb..012bb3238d 100644 --- a/crates/tracedecay-code-index-runtime/tests/code_index_scheduler/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/tests/code_index_scheduler/reconcile.rs @@ -3758,10 +3758,7 @@ async fn a_memory_refused_graph_releases_the_outgoing_warm_graph() { .await; let held = || { second_text - .graph_predecessor - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .as_ref() + .held_graph_predecessor() .map(|held| held.metadata().manifest().generation_id.as_str().to_owned()) }; assert_eq!( diff --git a/crates/tracedecay-code-index/benches/partitioned_codec.rs b/crates/tracedecay-code-index/benches/partitioned_codec.rs index 5b54e375f9..d5fb2cf5c3 100644 --- a/crates/tracedecay-code-index/benches/partitioned_codec.rs +++ b/crates/tracedecay-code-index/benches/partitioned_codec.rs @@ -225,9 +225,8 @@ fn main() -> Result<(), Box> { } fn replicated_sources(replicas: usize) -> Result, Box> { - let root = std::env::var_os(CORPUS_ENV) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(CORPUS_ROOT)); + let root = + std::env::var_os(CORPUS_ENV).map_or_else(|| PathBuf::from(CORPUS_ROOT), PathBuf::from); let root = root.as_path(); let mut base = WalkBuilder::new(root) .hidden(false) diff --git a/crates/tracedecay-code-index/benches/retained_parse.rs b/crates/tracedecay-code-index/benches/retained_parse.rs index 320e789722..cd2a14c0e6 100644 --- a/crates/tracedecay-code-index/benches/retained_parse.rs +++ b/crates/tracedecay-code-index/benches/retained_parse.rs @@ -469,9 +469,7 @@ fn environment() -> Environment { }) }) .unwrap_or_else(|| "unavailable".to_owned()), - logical_cpus: std::thread::available_parallelism() - .map(usize::from) - .unwrap_or(0), + logical_cpus: std::thread::available_parallelism().map_or(0, usize::from), rustc: command_output("rustc", &["--version"]), cargo: command_output("cargo", &["--version"]), } @@ -484,8 +482,7 @@ fn command_output(program: &str, arguments: &[&str]) -> String { .ok() .filter(|output| output.status.success()) .and_then(|output| String::from_utf8(output.stdout).ok()) - .map(|output| output.trim().to_owned()) - .unwrap_or_default() + .map_or_default(|output| output.trim().to_owned()) } fn sha256(bytes: &[u8]) -> String { diff --git a/crates/tracedecay-code-index/benches/sealed_storage.rs b/crates/tracedecay-code-index/benches/sealed_storage.rs index 9bee93bff9..36cbe36db1 100644 --- a/crates/tracedecay-code-index/benches/sealed_storage.rs +++ b/crates/tracedecay-code-index/benches/sealed_storage.rs @@ -188,9 +188,10 @@ struct LinkedWorktreeReport { } fn main() -> Result<(), Box> { - let repository = std::env::var_os(REPO_ENV) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))); + let repository = std::env::var_os(REPO_ENV).map_or_else( + || PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")), + PathBuf::from, + ); let revision = std::env::var(REV_ENV).unwrap_or_else(|_| "HEAD".to_owned()); let revision = git_output(&repository, &["rev-parse", "--verify", &revision])?; let sources = git_sources(&repository, &revision)?; diff --git a/crates/tracedecay-code-index/src/chunks.rs b/crates/tracedecay-code-index/src/chunks.rs index 98ff6e8dfd..73c7f055fb 100644 --- a/crates/tracedecay-code-index/src/chunks.rs +++ b/crates/tracedecay-code-index/src/chunks.rs @@ -2390,36 +2390,34 @@ fn resolve_file_references( .copied() .flatten() .map(|from| from.span); - let compatible = candidates - .map(|candidates| { - candidates - .iter() - .copied() - .filter(|target| { - reference_target_kind_is_compatible(reference.reference_kind, &target.kind) - && (language != "rust" - || reference.reference_kind != EdgeKind::Calls - || target.kind != NodeKind::Struct.as_str() - || target.arity.is_some()) - && (language != "java" - || reference - .argument_count - .zip(target.arity) - .is_none_or(|(arguments, arity)| arity.accepts(arguments))) - && !(typescript - && target.kind == NodeKind::Function.as_str() - && target - .signature - .as_deref() - .is_some_and(is_test_framework_call_signature)) - && (!shadows_import - || from_span.is_some_and(|from| { - encloses_in_function_scope(symbols, target, from) - })) - }) - .collect::>() - }) - .unwrap_or_default(); + let compatible = candidates.map_or_default(|candidates| { + candidates + .iter() + .copied() + .filter(|target| { + reference_target_kind_is_compatible(reference.reference_kind, &target.kind) + && (language != "rust" + || reference.reference_kind != EdgeKind::Calls + || target.kind != NodeKind::Struct.as_str() + || target.arity.is_some()) + && (language != "java" + || reference + .argument_count + .zip(target.arity) + .is_none_or(|(arguments, arity)| arity.accepts(arguments))) + && !(typescript + && target.kind == NodeKind::Function.as_str() + && target + .signature + .as_deref() + .is_some_and(is_test_framework_call_signature)) + && (!shadows_import + || from_span.is_some_and(|from| { + encloses_in_function_scope(symbols, target, from) + })) + }) + .collect::>() + }); match compatible.as_slice() { // Rust admits one definition per name and scope, so same-named // same-kind definitions are `#[cfg]` variants of one identity; the @@ -2578,14 +2576,13 @@ fn cross_file_reference_candidate( ), kind, evidence_span: reference_evidence_span(source, offsets, references_by_site, reference) - .map(|span| match computed_member { + .map_or(from.span, |span| match computed_member { Some(member) => SourceSpan { start_byte: span.end_byte - member.len() as u64, ..span }, None => span, - }) - .unwrap_or(from.span), + }), unmodeled_import: reference.unmodeled_import, argument_count: reference.argument_count, ambiguous_local: false, @@ -4672,8 +4669,7 @@ pub fn real_symbol() {} .symbols .iter() .find(|symbol| &symbol.occurrence == occurrence) - .map(|symbol| symbol.qualified_name.as_str()) - .unwrap_or("") + .map_or("", |symbol| symbol.qualified_name.as_str()) }; let mut calls = artifacts .edges @@ -4791,8 +4787,7 @@ pub fn real_symbol() {} .symbols .iter() .find(|symbol| &symbol.occurrence == occurrence) - .map(|symbol| symbol.qualified_name.as_str()) - .unwrap_or("") + .map_or("", |symbol| symbol.qualified_name.as_str()) }; let mut calls = artifacts .edges @@ -4851,8 +4846,7 @@ pub fn real_symbol() {} .symbols .iter() .find(|symbol| &symbol.occurrence == occurrence) - .map(|symbol| symbol.qualified_name.as_str()) - .unwrap_or("") + .map_or("", |symbol| symbol.qualified_name.as_str()) }; let mut calls = artifacts .edges diff --git a/crates/tracedecay-code-index/src/generations.rs b/crates/tracedecay-code-index/src/generations.rs index 04a5446c1e..d7625bb4c1 100644 --- a/crates/tracedecay-code-index/src/generations.rs +++ b/crates/tracedecay-code-index/src/generations.rs @@ -286,9 +286,8 @@ impl GenerationPlanner { let (grammar_revisions, extractor_revisions) = self.language_revisions(&snapshot.snapshot)?; let registry_revision = self.registry.registry_revision(); - let mut rebuild_triggers = parent - .map(|parent| self.rebuild_triggers(parent, &snapshot.snapshot)) - .unwrap_or_default(); + let mut rebuild_triggers = + parent.map_or_default(|parent| self.rebuild_triggers(parent, &snapshot.snapshot)); rebuild_triggers.extend(invalidations.iter().cloned()); rebuild_triggers.sort(); rebuild_triggers.dedup(); diff --git a/crates/tracedecay-code-index/src/graph_projection/builder.rs b/crates/tracedecay-code-index/src/graph_projection/builder.rs index 9e9bcb1fc9..ff4f6a91c0 100644 --- a/crates/tracedecay-code-index/src/graph_projection/builder.rs +++ b/crates/tracedecay-code-index/src/graph_projection/builder.rs @@ -563,8 +563,7 @@ pub(super) fn build_projection( .collect::>(); let file_rows = files .as_ref() - .map(|files| files.values().copied().collect::>()) - .unwrap_or_default(); + .map_or_default(|files| files.values().copied().collect::>()); let context = CodeGraphRowContext { projection, generation, diff --git a/crates/tracedecay-code-index/src/graph_projection/interactive.rs b/crates/tracedecay-code-index/src/graph_projection/interactive.rs index b5236895b2..fda882fdcf 100644 --- a/crates/tracedecay-code-index/src/graph_projection/interactive.rs +++ b/crates/tracedecay-code-index/src/graph_projection/interactive.rs @@ -633,8 +633,7 @@ impl CodeGraphInteractiveReader { .first() .and_then(|id| catalog.symbols.get(id)) .and_then(|symbol| symbol.metadata.as_ref()) - .map(|metadata| metadata.simple_name.clone()) - .unwrap_or_else(|| key.clone()); + .map_or_else(|| key.clone(), |metadata| metadata.simple_name.clone()); hits.push((distance, display)); } hits.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1))); diff --git a/crates/tracedecay-code-index/src/lineage.rs b/crates/tracedecay-code-index/src/lineage.rs index 24bc290b33..727ee8a9bc 100644 --- a/crates/tracedecay-code-index/src/lineage.rs +++ b/crates/tracedecay-code-index/src/lineage.rs @@ -620,14 +620,13 @@ impl SymbolLineageResolver { let content_candidates: Vec = by_content .get(symbol.content_digest.as_str()) - .map(|indices| { + .map_or_default(|indices| { indices .iter() .copied() .filter(|index| !reserved(*index)) .collect() - }) - .unwrap_or_default(); + }); match content_candidates.len() { 0 => {} count => { @@ -797,14 +796,13 @@ impl SymbolLineageResolver { // never proves lineage. let content_candidates: Vec = by_content .get(symbol.content_digest.as_str()) - .map(|indices| { + .map_or_default(|indices| { indices .iter() .copied() .filter(|index| !consumed[*index]) .collect() - }) - .unwrap_or_default(); + }); match content_candidates.len() { 0 => {} count => { diff --git a/crates/tracedecay-code-index/src/production/graph_pages.rs b/crates/tracedecay-code-index/src/production/graph_pages.rs index cb7d68374e..7dbfb253f5 100644 --- a/crates/tracedecay-code-index/src/production/graph_pages.rs +++ b/crates/tracedecay-code-index/src/production/graph_pages.rs @@ -329,12 +329,8 @@ pub(super) fn seal_code_graph_page( .unwrap_or_default(); let page = PersistedCodeGraphPageV1 { file: snapshot_file.clone(), - imports: artifacts - .map(|file| file.artifacts.imports.clone()) - .unwrap_or_default(), - symbols: artifacts - .map(|file| file.artifacts.symbols.clone()) - .unwrap_or_default(), + imports: artifacts.map_or_default(|file| file.artifacts.imports.clone()), + symbols: artifacts.map_or_default(|file| file.artifacts.symbols.clone()), edges, bindings, unresolved_calls, diff --git a/crates/tracedecay-code-index/src/production/mod.rs b/crates/tracedecay-code-index/src/production/mod.rs index 57ab23ae68..98010b5779 100644 --- a/crates/tracedecay-code-index/src/production/mod.rs +++ b/crates/tracedecay-code-index/src/production/mod.rs @@ -2498,8 +2498,7 @@ fn materialize_present_file( .chunks .chunks .first() - .map(|chunk| chunk.sensitivity.level) - .unwrap_or(SensitivityLevelV1::Public); + .map_or(SensitivityLevelV1::Public, |chunk| chunk.sensitivity.level); let physical_reuse_key = physical_reuse_key(config, file, descriptor, sensitivity)?; let reused = u64::try_from(carried.artifacts.clone_bodies.len()).unwrap_or(u64::MAX); physical_artifacts.record_clone_payloads(reused, 0); diff --git a/crates/tracedecay-code-index/src/production/partitioned_codec.rs b/crates/tracedecay-code-index/src/production/partitioned_codec.rs index 1794bceb5f..235c950703 100644 --- a/crates/tracedecay-code-index/src/production/partitioned_codec.rs +++ b/crates/tracedecay-code-index/src/production/partitioned_codec.rs @@ -2391,42 +2391,33 @@ impl CodeIndexPublishedGenerationV1 { "sealed segment reuse parent does not match the generation manifest".to_owned(), )); } - let parent_segments = parent - .as_ref() - .map(|parent| { - parent - .file_segments - .iter() - .filter_map(|descriptor| { - parent - .snapshot - .files - .get(descriptor.file_key as usize) - .map(|file| (&file.file_occurrence_id, (file, descriptor))) - }) - .collect::>() - }) - .unwrap_or_default(); - let reusable_graph_page_digests = parent - .as_ref() - .map(|parent| { - parent - .code_graph_pages - .iter() - .map(|page| page.page_digest.clone()) - .collect::>() - }) - .unwrap_or_default(); - let reusable_index_digests = parent - .as_ref() - .map(|parent| { - parent - .resolution_index - .segments() - .map(|page| page.segment_digest.clone()) - .collect::>() - }) - .unwrap_or_default(); + let parent_segments = parent.as_ref().map_or_default(|parent| { + parent + .file_segments + .iter() + .filter_map(|descriptor| { + parent + .snapshot + .files + .get(descriptor.file_key as usize) + .map(|file| (&file.file_occurrence_id, (file, descriptor))) + }) + .collect::>() + }); + let reusable_graph_page_digests = parent.as_ref().map_or_default(|parent| { + parent + .code_graph_pages + .iter() + .map(|page| page.page_digest.clone()) + .collect::>() + }); + let reusable_index_digests = parent.as_ref().map_or_default(|parent| { + parent + .resolution_index + .segments() + .map(|page| page.segment_digest.clone()) + .collect::>() + }); let file_keys = snapshot_file_keys( self.snapshot .files @@ -2537,16 +2528,13 @@ impl CodeIndexPublishedGenerationV1 { } } file_segments.sort_by_key(|segment| segment.file_key); - let reusable_file_evidence_digests = parent - .as_ref() - .map(|parent| { - parent - .file_evidence - .iter() - .map(|evidence| evidence.segment_digest.clone()) - .collect::>() - }) - .unwrap_or_default(); + let reusable_file_evidence_digests = parent.as_ref().map_or_default(|parent| { + parent + .file_evidence + .iter() + .map(|evidence| evidence.segment_digest.clone()) + .collect::>() + }); let (lineage_prior_generation, file_evidence) = { let _span = tracing::trace_span!("code_index.sealed_encode.file_evidence").entered(); self.encode_file_evidence( diff --git a/crates/tracedecay-code-index/src/production/sparse_successor.rs b/crates/tracedecay-code-index/src/production/sparse_successor.rs index 25552564d5..6f9d8d708f 100644 --- a/crates/tracedecay-code-index/src/production/sparse_successor.rs +++ b/crates/tracedecay-code-index/src/production/sparse_successor.rs @@ -376,19 +376,17 @@ impl<'p> SealedSuccessorV1<'p> { (Some((artifacts, _)), None) => Arc::clone(artifacts), (None, None) => return Err(contract("a re-decided file has no artifacts")), }; - let edges = resolved - .map(|(_, resolved)| { - resolved - .edges - .iter() - .map(|(edge, path, identity)| (edge, path.as_str(), identity)) - .collect::>() - }) - .unwrap_or_default(); + let edges = resolved.map_or_default(|(_, resolved)| { + resolved + .edges + .iter() + .map(|(edge, path, identity)| (edge, path.as_str(), identity)) + .collect::>() + }); counts.after = counts.after.saturating_add(count(edges.len())?); - let calls = resolved - .map(|(_, resolved)| resolved.unresolved_calls.iter().collect::>()) - .unwrap_or_default(); + let calls = resolved.map_or_default(|(_, resolved)| { + resolved.unresolved_calls.iter().collect::>() + }); let identity; let file_lineage = match edited_file { Some(_) => lineage_by_file @@ -502,17 +500,14 @@ impl<'p> SealedSuccessorV1<'p> { }; let inputs = match artifacts.as_deref() { Some(artifacts) => { - let cross_file = resolved - .map(|(_, resolved)| resolved.edges.as_slice()) - .unwrap_or_default(); + let cross_file = + resolved.map_or_default(|(_, resolved)| resolved.edges.as_slice()); self.page_inputs( key, file, artifacts, cross_file, - resolved - .map(|(_, resolved)| resolved.unresolved_calls.clone()) - .unwrap_or_default(), + resolved.map_or_default(|(_, resolved)| resolved.unresolved_calls.clone()), )? } None => CodeGraphPageInputsV1 { diff --git a/crates/tracedecay-code-index/src/unmounted_files/rust.rs b/crates/tracedecay-code-index/src/unmounted_files/rust.rs index 3cb3885fe9..67eb426dcd 100644 --- a/crates/tracedecay-code-index/src/unmounted_files/rust.rs +++ b/crates/tracedecay-code-index/src/unmounted_files/rust.rs @@ -274,14 +274,13 @@ fn workspace_declarations(project_root: &Path) -> Result<(Vec, Vec>() - }) - .unwrap_or_default(); + }); // `exclude` names a directory, and cargo excludes everything under it, // comparing for equality would keep auditing every member below an @@ -290,14 +289,13 @@ fn workspace_declarations(project_root: &Path) -> Result<(Vec, Vec>() - }) - .unwrap_or_default(); + }); Ok((excluded_globs, excluded_dirs)) } @@ -620,7 +618,7 @@ fn parse_rust(source: &str) -> Option { /// The directory a non-root module file's own declarations resolve against: /// `a/b/mod.rs` declares into `a/b/`, `a/b.rs` declares into `a/b/`. fn module_child_directory(file: &Path) -> PathBuf { - let parent = file.parent().map(Path::to_path_buf).unwrap_or_default(); + let parent = file.parent().map_or_default(Path::to_path_buf); match file.file_stem().and_then(|stem| stem.to_str()) { Some("mod") | None => parent, Some(stem) => parent.join(stem), 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 1d5a688e59..00f25eb244 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 @@ -73,13 +73,16 @@ impl SharedPublicationStore { &self, published: &CodeIndexPublishedBuildV1, ) -> Arc { - published.decoded().map(Arc::clone).unwrap_or_else(|| { - Arc::new( - self.sealed - .decode(published.manifest_bytes()) - .expect("published generation decodes"), - ) - }) + published.decoded().map_or_else( + || { + Arc::new( + self.sealed + .decode(published.manifest_bytes()) + .expect("published generation decodes"), + ) + }, + Arc::clone, + ) } } diff --git a/crates/tracedecay-contracts/src/doctor/adapters.rs b/crates/tracedecay-contracts/src/doctor/adapters.rs index 0f0b6d10ec..b4c040bd0b 100644 --- a/crates/tracedecay-contracts/src/doctor/adapters.rs +++ b/crates/tracedecay-contracts/src/doctor/adapters.rs @@ -123,8 +123,7 @@ pub fn advisory_feedback_read_from_publication( .result .impact .as_ref() - .map(|impact| impact.evidence_anchors.as_slice()) - .unwrap_or_default(); + .map_or_default(|impact| impact.evidence_anchors.as_slice()); let findings = publication .result .findings diff --git a/crates/tracedecay-contracts/src/feedback/catalog.rs b/crates/tracedecay-contracts/src/feedback/catalog.rs index 29b8fd85d5..267507935a 100644 --- a/crates/tracedecay-contracts/src/feedback/catalog.rs +++ b/crates/tracedecay-contracts/src/feedback/catalog.rs @@ -500,8 +500,7 @@ mod tests { fn property_keys(body: &Value) -> Vec<&str> { let mut keys = body["properties"] .as_object() - .map(|properties| properties.keys().map(String::as_str).collect::>()) - .unwrap_or_default(); + .map_or_default(|properties| properties.keys().map(String::as_str).collect::>()); keys.sort_unstable(); keys } diff --git a/crates/tracedecay-contracts/src/feedback/service.rs b/crates/tracedecay-contracts/src/feedback/service.rs index 7509df3161..b5b65ea2e8 100644 --- a/crates/tracedecay-contracts/src/feedback/service.rs +++ b/crates/tracedecay-contracts/src/feedback/service.rs @@ -946,8 +946,7 @@ where let affected_tests_state = progress .impact .as_ref() - .map(|impact| impact.affected_tests_state) - .unwrap_or(impact_state); + .map_or(impact_state, |impact| impact.affected_tests_state); let termination = determine_termination( &progress.provider_states, &progress.baseline_states, @@ -1710,26 +1709,29 @@ fn collect_diagnostics( &request.input, ) && diagnostic.validate().is_ok() => { - let classification = baselines - .get(provider_index) - .map(|resolved| { + let classification = baselines.get(provider_index).map_or( + FeedbackDiagnosticClassificationV1::Unknown, + |resolved| { resolved .baseline .zip(resolved.expected.as_ref()) - .map(|(baseline, expected)| { - baseline.classify(expected, &diagnostic.diagnostic_anchor) - }) - .unwrap_or_else(|| { - if resolved.state - == FeedbackBaselineStateV1::NoPriorBaseline - { - FeedbackDiagnosticClassificationV1::New - } else { - FeedbackDiagnosticClassificationV1::Unknown - } - }) - }) - .unwrap_or(FeedbackDiagnosticClassificationV1::Unknown); + .map_or_else( + || { + if resolved.state + == FeedbackBaselineStateV1::NoPriorBaseline + { + FeedbackDiagnosticClassificationV1::New + } else { + FeedbackDiagnosticClassificationV1::Unknown + } + }, + |(baseline, expected)| { + baseline + .classify(expected, &diagnostic.diagnostic_anchor) + }, + ) + }, + ); provider_findings.push(FeedbackFindingV1 { finding_id: derive_feedback_finding_id( &diagnostic.diagnostic_anchor, diff --git a/crates/tracedecay-contracts/src/historical_query.rs b/crates/tracedecay-contracts/src/historical_query.rs index 13eab44f9b..b010855473 100644 --- a/crates/tracedecay-contracts/src/historical_query.rs +++ b/crates/tracedecay-contracts/src/historical_query.rs @@ -513,13 +513,12 @@ mod tests { let (blob_oid, bytes) = self .blobs .get(&(request.commit.clone(), request.path.clone())) - .map(|(oid, bytes)| { + .map_or((None, None), |(oid, bytes)| { ( Some(oid.clone()), request.include_bytes.then(|| bytes.clone()), ) - }) - .unwrap_or((None, None)); + }); Ok(GitHistoricalBlobV1 { repository: self.scope.repository_id.clone(), worktree: self.scope.worktree_id.clone(), diff --git a/crates/tracedecay-contracts/src/remote/recovery/service.rs b/crates/tracedecay-contracts/src/remote/recovery/service.rs index c7724997c3..209b482300 100644 --- a/crates/tracedecay-contracts/src/remote/recovery/service.rs +++ b/crates/tracedecay-contracts/src/remote/recovery/service.rs @@ -512,24 +512,23 @@ impl_recovery_protocol!( REMOTE_PROMOTION_USE_CASE_ID_V1, "remote.promotion.result", |request: &RemoteProtocolRequestV1| { - request - .expected_authority - .as_ref() - .map(|writer| RecoveryAuthorityExpectationV1 { - brain_id: writer.brain_id.as_str().to_owned(), - shard_id: writer.shard_id.as_str().to_owned(), - generation_id: writer.generation_id.as_str().to_owned(), - authority_node_id: writer.authority_node_id.as_str().to_owned(), - placement_revision: request.body.expected_placement_revision, - authority_epoch: request.body.expected_authority_epoch, - }) - .unwrap_or_else(|| RecoveryAuthorityExpectationV1 { + request.expected_authority.as_ref().map_or_else( + || RecoveryAuthorityExpectationV1 { brain_id: request.brain_id.as_str().to_owned(), shard_id: "unavailable".to_owned(), generation_id: "unavailable".to_owned(), authority_node_id: "unavailable".to_owned(), placement_revision: request.body.expected_placement_revision, authority_epoch: request.body.expected_authority_epoch, - }) + }, + |writer| RecoveryAuthorityExpectationV1 { + brain_id: writer.brain_id.as_str().to_owned(), + shard_id: writer.shard_id.as_str().to_owned(), + generation_id: writer.generation_id.as_str().to_owned(), + authority_node_id: writer.authority_node_id.as_str().to_owned(), + placement_revision: request.body.expected_placement_revision, + authority_epoch: request.body.expected_authority_epoch, + }, + ) } ); diff --git a/crates/tracedecay-contracts/src/result/envelope.rs b/crates/tracedecay-contracts/src/result/envelope.rs index b761e4c3a8..28c5b3b297 100644 --- a/crates/tracedecay-contracts/src/result/envelope.rs +++ b/crates/tracedecay-contracts/src/result/envelope.rs @@ -572,10 +572,10 @@ impl ApplicationProblemRecord { .as_ref() .and_then(ApplicationProblemDetailV1::retry_after_millis); let committed_receipt = source.committed_receipt().cloned(); - let code = diagnostic - .as_ref() - .map(|diagnostic| diagnostic.code.clone()) - .unwrap_or_else(|| source.canonical_code().to_owned()); + let code = diagnostic.as_ref().map_or_else( + || source.canonical_code().to_owned(), + |diagnostic| diagnostic.code.clone(), + ); let record = Self { revision: APPLICATION_PROBLEM_REVISION, kind, @@ -637,11 +637,10 @@ impl ApplicationProblemRecord { } if self.code - != self - .source - .diagnostic() - .map(|diagnostic| diagnostic.code.as_str()) - .unwrap_or_else(|| self.source.canonical_code()) + != self.source.diagnostic().map_or_else( + || self.source.canonical_code(), + |diagnostic| diagnostic.code.as_str(), + ) || self.message != self.source.safe_message() || self.diagnostic.as_ref() != self.source.diagnostic() || self.detail.as_ref() != self.source.detail() diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index ccd7dd26ef..70800bd586 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -823,9 +823,10 @@ impl ApplicationProblem { /// Stable client-facing reason: the diagnostic code when one exists, /// otherwise the problem kind's canonical code. pub fn reason_code(&self) -> &str { - self.diagnostic() - .map(|diagnostic| diagnostic.code.as_str()) - .unwrap_or_else(|| self.canonical_code()) + self.diagnostic().map_or_else( + || self.canonical_code(), + |diagnostic| diagnostic.code.as_str(), + ) } /// Preserve reason code and retryability when a root caller must surface diff --git a/crates/tracedecay-contracts/src/sdk_catalog.rs b/crates/tracedecay-contracts/src/sdk_catalog.rs index 0309b774c3..45a737550e 100644 --- a/crates/tracedecay-contracts/src/sdk_catalog.rs +++ b/crates/tracedecay-contracts/src/sdk_catalog.rs @@ -537,10 +537,10 @@ mod tests { .as_str() .strip_prefix("operation.") .expect("canonical operation ID"); - let expected_method = operation - .strip_prefix("application.code_") - .map(|suffix| format!("code_{suffix}")) - .unwrap_or_else(|| operation.replace('.', "_")); + let expected_method = operation.strip_prefix("application.code_").map_or_else( + || operation.replace('.', "_"), + |suffix| format!("code_{suffix}"), + ); assert_eq!( projected_binding.sdk_method().as_str(), expected_method, diff --git a/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs b/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs index 72d959bd0f..172f08b826 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/common/mod.rs @@ -818,8 +818,7 @@ impl WorkAttemptStoragePort for WorkProductAttemptStore { .filter(|attempt| { attempt .as_ref() - .map(|attempt| !attempt.is_terminal()) - .unwrap_or(true) + .map_or(true, |attempt| !attempt.is_terminal()) }) .collect() } diff --git a/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs b/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs index be50c6ce86..f36cffa6ea 100644 --- a/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs +++ b/crates/tracedecay-contracts/tests/contracts_suite/work_run_control_service.rs @@ -202,7 +202,7 @@ impl WorkRunControlStoragePort for TestStore { .lock() .unwrap() .get(&key) - .map(|admission| { + .map_or_default(|admission| { admission .live_attempts .iter() @@ -212,8 +212,7 @@ impl WorkRunControlStoragePort for TestStore { step_id: workflow_bound.then(|| id::("step.run-control")), }) .collect() - }) - .unwrap_or_default(); + }); if std::mem::take(&mut *self.settle_frontier_during_binding_read.lock().unwrap()) && let Some(admission) = self.admissions.lock().unwrap().get_mut(&key) { diff --git a/crates/tracedecay-daemon-control/src/service.rs b/crates/tracedecay-daemon-control/src/service.rs index 31836463d7..89274baaf2 100644 --- a/crates/tracedecay-daemon-control/src/service.rs +++ b/crates/tracedecay-daemon-control/src/service.rs @@ -1727,8 +1727,7 @@ pub fn service_status( .as_ref() .ok() .and_then(ServiceRunner::service_detail_hint) - .map(|hint| format!("service-detail: {hint}\n")) - .unwrap_or_default(); + .map_or_default(|hint| format!("service-detail: {hint}\n")); let logs = match &service_observation { Ok(Err(ServiceStateError::ManagerUnreachable(_))) if serving => { "foreground daemon output (systemd user journal is unreachable)".to_owned() diff --git a/crates/tracedecay-daemon-identity/src/authority.rs b/crates/tracedecay-daemon-identity/src/authority.rs index b4c717c596..e2a29750cc 100644 --- a/crates/tracedecay-daemon-identity/src/authority.rs +++ b/crates/tracedecay-daemon-identity/src/authority.rs @@ -99,13 +99,12 @@ impl DaemonAuthority { let record = read_record_if_present(&authority_root.join(RECORD_FILE)) .ok() .flatten() - .map(|record| { + .map_or_default(|record| { format!( " (pid {}, epoch {}, endpoint '{}')", record.pid, record.epoch, record.endpoint ) - }) - .unwrap_or_default(); + }); return Err(TraceDecayError::Config { message: format!( "daemon authority for profile '{}' is already held{record}: {error}", diff --git a/crates/tracedecay-daemon-service/src/automation_effect.rs b/crates/tracedecay-daemon-service/src/automation_effect.rs index 4adf7e88d4..f4999d5318 100644 --- a/crates/tracedecay-daemon-service/src/automation_effect.rs +++ b/crates/tracedecay-daemon-service/src/automation_effect.rs @@ -4,7 +4,9 @@ //! and resolves the project-memory owner. Settlement, retries, and journal //! recovery live in `tracedecay-automation-runtime`. +use std::future::Future; use std::path::Path; +use std::pin::Pin; use crate::{DaemonInvocationService, RegisteredRetainedRequestContextError}; use tracedecay_automation_runtime::automation::effect_runtime::contract_error; @@ -73,34 +75,35 @@ pub async fn prepare( }; // Runtime prepare retains journal and authority state; keep that frame // out of scheduler and dashboard callers. - Box::pin(AutomationEffectAuthority::prepare( - AdmittedAutomationEffectRequest { - context, - cancellation: cancellation.clone(), - observed_at, - configuration_digest, - request, - dashboard_root: dashboard_root.to_path_buf(), - }, - || memory.project_memory_owner(), - |run_id, read_control| async move { - memory - .project_memory_application() - .map_err(|error| { - contract_error(format!( - "canonical memory automation receipt recovery failed: {error}" - )) - })? - .project_memory_automation_run_receipts(run_id, &read_control) - .await - .map_err(|error| { - contract_error(format!( - "canonical memory automation receipt recovery failed: {error}" - )) - }) - }, - )) - .await + let prepared: Pin> + Send + '_>> = + Box::pin(AutomationEffectAuthority::prepare( + AdmittedAutomationEffectRequest { + context, + cancellation: cancellation.clone(), + observed_at, + configuration_digest, + request, + dashboard_root: dashboard_root.to_path_buf(), + }, + || memory.project_memory_owner(), + |run_id, read_control| async move { + memory + .project_memory_application() + .map_err(|error| { + contract_error(format!( + "canonical memory automation receipt recovery failed: {error}" + )) + })? + .project_memory_automation_run_receipts(run_id, &read_control) + .await + .map_err(|error| { + contract_error(format!( + "canonical memory automation receipt recovery failed: {error}" + )) + }) + }, + )); + prepared.await } /// The stable request identity of one automation-scheduler run: a requested diff --git a/crates/tracedecay-daemon-service/src/invocation/feedback.rs b/crates/tracedecay-daemon-service/src/invocation/feedback.rs index 07c8bdfeb5..181851f3bf 100644 --- a/crates/tracedecay-daemon-service/src/invocation/feedback.rs +++ b/crates/tracedecay-daemon-service/src/invocation/feedback.rs @@ -481,19 +481,17 @@ pub fn advisory_cycle_invocation_result( | OperationTermination::Failed | OperationTermination::EffectUnknown => None, }; - let omissions = omission_reason - .map(|reason| { - vec![Omission { - domain: EvidenceDomain::Diagnostic, - count: cycle - .execution - .cycle - .omitted_findings - .max(u64::from(returned == total)), - reason, - }] - }) - .unwrap_or_default(); + let omissions = omission_reason.map_or_default(|reason| { + vec![Omission { + domain: EvidenceDomain::Diagnostic, + count: cycle + .execution + .cycle + .omitted_findings + .max(u64::from(returned == total)), + reason, + }] + }); let finding_handles = cycle .finding_handles .iter() @@ -733,15 +731,13 @@ pub fn feedback_proximity_invocation_result( execution .validate() .map_err(|_| feedback_proximity_contract_problem())?; - let omissions = omission_reason - .map(|reason| { - vec![Omission { - domain: EvidenceDomain::Graph, - count: 1, - reason, - }] - }) - .unwrap_or_default(); + let omissions = omission_reason.map_or_default(|reason| { + vec![Omission { + domain: EvidenceDomain::Graph, + count: 1, + reason, + }] + }); let payload = serde_json::to_value(result).map_err(|_| feedback_proximity_contract_problem())?; Ok(DaemonFeedbackInvocationResult { diff --git a/crates/tracedecay-daemon-service/src/invocation/work.rs b/crates/tracedecay-daemon-service/src/invocation/work.rs index c2f8e24179..d3e8e0b186 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work.rs @@ -1,5 +1,8 @@ //! Work and workflow application daemon invocation handlers. +use std::future::Future; +use std::pin::Pin; + use super::*; mod attempt_operations; @@ -108,18 +111,19 @@ pub async fn execute_work_application( let activity_database = registered.database.clone(); let activity_root = project_root.clone(); let mutates = work_invocation_mutates(&request); - let response = request_dispatch::dispatch_work_application( - registered, - attempt_processes, - observability_producer, - project_root, - request_id, - request, - observed_at, - deadline, - cancellation, - ) - .await; + let dispatched: Pin + Send>> = + Box::pin(request_dispatch::dispatch_work_application( + registered, + attempt_processes, + observability_producer, + project_root, + request_id, + request, + observed_at, + deadline, + cancellation, + )); + let response = dispatched.await; if mutates && matches!( response.outcome, diff --git a/crates/tracedecay-daemon-service/src/invocation/work/request_dispatch.rs b/crates/tracedecay-daemon-service/src/invocation/work/request_dispatch.rs index f8ae6edc51..1f898690b8 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work/request_dispatch.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work/request_dispatch.rs @@ -1,6 +1,8 @@ //! Work application invocation dispatch. +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; use tracedecay_contracts::{CancellationContext, Deadline, WorkAttemptListOperationV1}; @@ -493,8 +495,25 @@ pub(super) async fn dispatch_work_application( operation_key, use_case.clone(), input_digest, - evidence_retrieval::retrieve(®istered, &context, capability, use_case, request) - .await, + { + let retrieved: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_contracts::WorkEvidenceRetrievalV1, + tracedecay_contracts::ApplicationProblem, + >, + > + Send, + >, + > = Box::pin(evidence_retrieval::retrieve( + ®istered, + &context, + capability, + use_case, + request, + )); + retrieved.await + }, observed_at, deadline, WorkApplicationOutcomeV1::RetrieveEvidence, @@ -537,13 +556,23 @@ pub(super) async fn dispatch_work_application( tracedecay_application::observability::RegisteredObservabilityPortV1::new( ®istered.database, ); + let metrics: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_contracts::ExecutionTopologyMetricsV1, + tracedecay_contracts::ApplicationProblem, + >, + > + Send, + >, + > = Box::pin(tracedecay_contracts::execution_topology_rollup_metrics( + &observations, + &observations, + &context, + &request, + )); let metrics = tracing::Instrument::instrument( - tracedecay_contracts::execution_topology_rollup_metrics( - &observations, - &observations, - &context, - &request, - ), + metrics, tracing::trace_span!("daemon.service.work.topology_metrics"), ) .await; @@ -644,7 +673,16 @@ pub(super) async fn dispatch_work_application( } } WorkApplicationInvocationV1::AdjudicateLeak(command) => { - let adjudicated = adjudicate_leak( + let adjudicated: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_contracts::WorkLeakAdjudicationOutcomeV1, + tracedecay_contracts::ApplicationProblem, + >, + > + Send, + >, + > = Box::pin(adjudicate_leak( ®istered, &services, attempt_processes.as_ref(), @@ -652,8 +690,8 @@ pub(super) async fn dispatch_work_application( command, observed_at, &deadline, - ) - .await; + )); + let adjudicated = adjudicated.await; if let Ok(outcome) = &adjudicated { let _ = tracedecay_application::observability::record_work_leak_observation( observability_producer.as_deref(), @@ -737,20 +775,21 @@ pub(super) async fn dispatch_work_application( } } WorkApplicationInvocationV1::Experience(request) => { - intelligence::experience( - ®istered, - request_id, - &context, - canonical_request_id, - operation_key, - use_case, - input_digest, - observed_at, - deadline, - capability, - request, - ) - .await + let experienced: Pin + Send>> = + Box::pin(intelligence::experience( + ®istered, + request_id, + &context, + canonical_request_id, + operation_key, + use_case, + input_digest, + observed_at, + deadline, + capability, + request, + )); + experienced.await } WorkApplicationInvocationV1::CompareProposal(request) => intelligence::compare_proposal( ®istered, @@ -845,8 +884,7 @@ pub(super) async fn dispatch_work_application( let transition = services.run_control().pause_with_receipt(&context, command); let open_receipts = transition .as_ref() - .map(|transition| transition.blocked_intervals.clone()) - .unwrap_or_default(); + .map_or_default(|transition| transition.blocked_intervals.clone()); let response = complete_work_effect( ®istered, request_id, @@ -877,8 +915,7 @@ pub(super) async fn dispatch_work_application( .resume_with_receipt(&context, command); let settled_receipts = transition .as_ref() - .map(|transition| transition.blocked_intervals.clone()) - .unwrap_or_default(); + .map_or_default(|transition| transition.blocked_intervals.clone()); let response = complete_work_effect( ®istered, request_id, @@ -1014,7 +1051,21 @@ pub(super) async fn dispatch_work_application( deadline: deadline.clone(), cancellation: job_cancellation, }; - job::advance_job(&host, &context, capability, &use_case, request).await + { + let advanced: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_contracts::AdvanceWorkJobResultV1, + tracedecay_contracts::ApplicationProblem, + >, + > + Send, + >, + > = Box::pin(job::advance_job( + &host, &context, capability, &use_case, request, + )); + advanced.await + } } Err(problem) => Err(problem), }; diff --git a/crates/tracedecay-daemon-service/src/invocation/work/workflow_census.rs b/crates/tracedecay-daemon-service/src/invocation/work/workflow_census.rs index 7f02937aae..77fa3b7ac9 100644 --- a/crates/tracedecay-daemon-service/src/invocation/work/workflow_census.rs +++ b/crates/tracedecay-daemon-service/src/invocation/work/workflow_census.rs @@ -1,3 +1,5 @@ +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; use tracedecay_contracts::{ApplicationProblem, RequestContext}; @@ -330,34 +332,26 @@ fn census_readiness( ®istered.work_topology_policy, ) .ok()?; - let mut global_remaining = capacities - .values() - .next() - .map(|capacity| { - u64::from( - registered - .work_topology_policy - .concurrency - .maximum_global_active - .get(), - ) - .saturating_sub(capacity.global_active()) - }) - .unwrap_or_default(); - let mut repository_remaining = capacities - .values() - .next() - .map(|capacity| { - u64::from( - registered - .work_topology_policy - .concurrency - .maximum_active_per_repository - .get(), - ) - .saturating_sub(capacity.repository_active()) - }) - .unwrap_or_default(); + let mut global_remaining = capacities.values().next().map_or_default(|capacity| { + u64::from( + registered + .work_topology_policy + .concurrency + .maximum_global_active + .get(), + ) + .saturating_sub(capacity.global_active()) + }); + let mut repository_remaining = capacities.values().next().map_or_default(|capacity| { + u64::from( + registered + .work_topology_policy + .concurrency + .maximum_active_per_repository + .get(), + ) + .saturating_sub(capacity.repository_active()) + }); if capacities.values().any(|capacity| { capacity.concurrency() != ®istered.work_topology_policy.concurrency || capacities.values().next().is_some_and(|first| { @@ -476,8 +470,14 @@ impl WorkflowFanOutCensusObservationRecoveryOwnerV1 { let producer = Arc::clone(&producer); let cancellation = task_cancellation.clone(); async move { - recover_pending_census_once(database, project_id, producer, cancellation) - .await + let recovered: Pin> + Send>> = + Box::pin(recover_pending_census_once( + database, + project_id, + producer, + cancellation, + )); + recovered.await } }, ) diff --git a/crates/tracedecay-daemon-service/src/mcp_project_registry.rs b/crates/tracedecay-daemon-service/src/mcp_project_registry.rs index 3b624d7649..3930ecc82d 100644 --- a/crates/tracedecay-daemon-service/src/mcp_project_registry.rs +++ b/crates/tracedecay-daemon-service/src/mcp_project_registry.rs @@ -7,7 +7,9 @@ //! unreadable registry still fails. use serde_json::Value; +use std::future::Future; use std::path::Path; +use std::pin::Pin; use tracedecay_contracts::{ ProjectRegistryContextCommand, ProjectRegistryContextFuture, ProjectRegistryContextOutcome, @@ -44,11 +46,14 @@ impl DaemonProjectRegistryReadService { return Ok(None); }; let git_common_dir = tracedecay_runtime_core::worktree::git_common_dir(active_project_root); - Ok(self - .registry - .project_registry_context_by_identity(active_project_root, git_common_dir.as_deref()) - .await? - .map(|context| context.project.project_id)) + let resolved: Pin< + Box>> + Send + '_>, + > = + Box::pin(self.registry.project_registry_context_by_identity( + active_project_root, + git_common_dir.as_deref(), + )); + Ok(resolved.await?.map(|context| context.project.project_id)) } async fn listing( @@ -57,13 +62,18 @@ impl DaemonProjectRegistryReadService { mut projects: Vec, limit: usize, ) -> Result { - let active_id = self.active_project_id(active_project_root).await?; + let active: Pin>> + Send + '_>> = + Box::pin(self.active_project_id(active_project_root)); + let active_id = active.await?; let truncated = projects.len() > limit; projects.truncate(limit); - let contexts = self - .registry - .project_registry_contexts_for_projects(&projects) - .await?; + let contexts_future: Pin< + Box>> + Send + '_>, + > = Box::pin( + self.registry + .project_registry_contexts_for_projects(&projects), + ); + let contexts = contexts_future.await?; let view = build_project_registry_view( &contexts, active_id.as_deref(), @@ -95,13 +105,23 @@ impl DaemonProjectRegistryReadService { // One extra row detects truncation without a second count query. let page = limit.saturating_add(1); let projects = match &scope { - ProjectRegistryListingScope::All => self.registry.list_code_projects(page).await?, + ProjectRegistryListingScope::All => { + let listed: Pin< + Box>> + Send + '_>, + > = Box::pin(self.registry.list_code_projects(page)); + listed.await? + } ProjectRegistryListingScope::Matching { query } => { - self.registry.try_search_code_projects(query, page).await? + let searched: Pin< + Box>> + Send + '_>, + > = Box::pin(self.registry.try_search_code_projects(query, page)); + searched.await? } }; - self.listing(active_project_root.as_deref(), projects, limit) - .await + let listed_view: Pin< + Box> + Send + '_>, + > = Box::pin(self.listing(active_project_root.as_deref(), projects, limit)); + listed_view.await } async fn resolve_context( diff --git a/crates/tracedecay-daemon-service/src/project_runtime.rs b/crates/tracedecay-daemon-service/src/project_runtime.rs index 1fc4988bd0..1c632cf3f9 100644 --- a/crates/tracedecay-daemon-service/src/project_runtime.rs +++ b/crates/tracedecay-daemon-service/src/project_runtime.rs @@ -210,6 +210,83 @@ pub struct ProjectRuntime { recovery_cancel_probe: Option, } +// A `BTreeMap` of these runtimes walks every component before `Send` bottoms +// out (rust-lang/rust#159228). Each field is proved on its own. +unsafe impl Send for ProjectRuntime {} +unsafe impl Sync for ProjectRuntime {} + +const _: fn(&ProjectRuntime) = |value| { + fn assert_send_sync(_: &T) {} + let ProjectRuntime { + publication, + publication_attempt, + callable_code, + feedback, + advisory_cycle, + advisory, + delivery_read, + feedback_cycle, + feedback_cycle_input, + primitive, + configuration, + work, + retained, + lsp_owner, + source_edit, + graph_tool, + #[cfg(any(test, feature = "test-helpers"))] + test_marker, + observability, + reservations, + registration_builds, + #[cfg(test)] + test_first, + #[cfg(test)] + test_second, + #[cfg(test)] + test_omitted, + #[cfg(test)] + recording_first, + #[cfg(test)] + recording_second, + #[cfg(test)] + recovery_cancel_probe, + } = value; + assert_send_sync(publication); + assert_send_sync(publication_attempt); + assert_send_sync(callable_code); + assert_send_sync(feedback); + assert_send_sync(advisory_cycle); + assert_send_sync(advisory); + assert_send_sync(delivery_read); + assert_send_sync(feedback_cycle); + assert_send_sync(feedback_cycle_input); + assert_send_sync(primitive); + assert_send_sync(configuration); + assert_send_sync(work); + assert_send_sync(retained); + assert_send_sync(lsp_owner); + assert_send_sync(source_edit); + assert_send_sync(graph_tool); + #[cfg(any(test, feature = "test-helpers"))] + assert_send_sync(test_marker); + assert_send_sync(observability); + assert_send_sync(reservations); + assert_send_sync(registration_builds); + #[cfg(test)] + assert_send_sync(test_first); + #[cfg(test)] + assert_send_sync(test_second); + #[cfg(test)] + assert_send_sync(test_omitted); + #[cfg(test)] + assert_send_sync(recording_first); + #[cfg(test)] + assert_send_sync(recording_second); + #[cfg(test)] + assert_send_sync(recovery_cancel_probe); +}; + impl ProjectRuntime { /// Stop this project's retained background recovery owners from starting /// another cycle, without awaiting anything. diff --git a/crates/tracedecay-daemon-service/src/project_runtime/shutdown.rs b/crates/tracedecay-daemon-service/src/project_runtime/shutdown.rs index 7f24afd666..c692e8d785 100644 --- a/crates/tracedecay-daemon-service/src/project_runtime/shutdown.rs +++ b/crates/tracedecay-daemon-service/src/project_runtime/shutdown.rs @@ -1,5 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::{Arc, atomic::Ordering}; use tracedecay_runtime_core::native_runtime::{NativeTask, NativeTaskOutcome}; @@ -44,7 +46,9 @@ impl ProjectRuntimeRegistryV1 { } fences.quiesced.extend(roots.iter().cloned()); } - if !self.drain_roots(roots).await { + let drained: Pin + Send + '_>> = + Box::pin(self.drain_roots(roots)); + if !drained.await { self.release_quiesced_roots(roots); return None; } @@ -376,7 +380,13 @@ async fn shut_down_observability(runtimes: &mut BTreeMap> + + Send, + >, + > = Box::pin(observability.shutdown()); + if let Err(error) = shutdown.await { tracing::warn!( project = %project_root.display(), %error, diff --git a/crates/tracedecay-daemon-service/src/retained_owner/retained_curator.rs b/crates/tracedecay-daemon-service/src/retained_owner/retained_curator.rs index e2d8e65dca..bf0bf8ed58 100644 --- a/crates/tracedecay-daemon-service/src/retained_owner/retained_curator.rs +++ b/crates/tracedecay-daemon-service/src/retained_owner/retained_curator.rs @@ -1,5 +1,7 @@ //! Retained application adapter for the canonical Memory Curator. +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; use crate::DaemonInvocationService; @@ -90,7 +92,16 @@ pub async fn execute_retained_memory_curator( "the automation project context could not be composed: {error}" )) })?; - let admission = crate::automation_effect::prepare( + let prepared: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + tracedecay_automation_runtime::automation::effect_runtime::AutomationEffectAdmission, + >, + > + Send + + '_, + >, + > = Box::pin(crate::automation_effect::prepare( invocation_service, cg, automation_context.project_root(), @@ -101,9 +112,8 @@ pub async fn execute_retained_memory_curator( context.observed_at, configuration_digest, automation_request, - ) - .await - .map_err(|error| { + )); + let admission = prepared.await.map_err(|error| { RetainedSurfaceExecutionErrorV1::unavailable(format!( "the automation effect authority could not be prepared: {error}" )) diff --git a/crates/tracedecay-daemon-service/src/session_refresh.rs b/crates/tracedecay-daemon-service/src/session_refresh.rs index de0fd0192b..b878505890 100644 --- a/crates/tracedecay-daemon-service/src/session_refresh.rs +++ b/crates/tracedecay-daemon-service/src/session_refresh.rs @@ -141,9 +141,13 @@ impl DaemonSessionRefreshService { ); let outcome = match command.action { SessionRefreshAction::Begin => { - service - .begin_or_join(&command.context, &command.binding, command.target) - .await + let began: Pin + Send + '_>> = + Box::pin(service.begin_or_join( + &command.context, + &command.binding, + command.target, + )); + began.await } SessionRefreshAction::Status => { let handle = match command.handle.as_deref().map(|token| self.handle(token)) { @@ -155,9 +159,9 @@ impl DaemonSessionRefreshService { return SessionRefreshServiceOutcome::NotFound; } }; - service - .status(&command.context, &command.binding, &handle) - .await + let read: Pin + Send + '_>> = + Box::pin(service.status(&command.context, &command.binding, &handle)); + read.await } SessionRefreshAction::Cancel => { let handle = match command.handle.as_deref().map(|token| self.handle(token)) { @@ -169,9 +173,9 @@ impl DaemonSessionRefreshService { return SessionRefreshServiceOutcome::NotFound; } }; - service - .cancel(&command.context, &command.binding, &handle) - .await + let cancelled: Pin + Send + '_>> = + Box::pin(service.cancel(&command.context, &command.binding, &handle)); + cancelled.await } }; self.public_outcome(outcome) @@ -378,8 +382,7 @@ fn refresh_progress_view( coverage: refresh_coverage_view(progress.coverage()), source_coverage: progress .source_coverage() - .map(|receipt| receipt.sources().to_vec()) - .unwrap_or_default(), + .map_or_default(|receipt| receipt.sources().to_vec()), committed_batches: progress.committed_batches(), committed_records: progress.committed_records(), updated_at: utc_micros_value(progress.updated_at()), @@ -396,8 +399,7 @@ fn refresh_receipt_view( coverage: refresh_coverage_view(receipt.coverage()), source_coverage: receipt .source_coverage() - .map(|coverage| coverage.sources().to_vec()) - .unwrap_or_default(), + .map_or_default(|coverage| coverage.sources().to_vec()), state: match receipt.state() { tracedecay_store::SessionRefreshTerminalStateV1::Complete => "complete", tracedecay_store::SessionRefreshTerminalStateV1::Failed => "failed", diff --git a/crates/tracedecay-dashboard-api/src/analytics_api.rs b/crates/tracedecay-dashboard-api/src/analytics_api.rs index 465f6bc891..0e82121b18 100644 --- a/crates/tracedecay-dashboard-api/src/analytics_api.rs +++ b/crates/tracedecay-dashboard-api/src/analytics_api.rs @@ -5,7 +5,9 @@ //! comes only from durable analytics events. use std::collections::BTreeMap; +use std::future::Future; use std::path::Path; +use std::pin::Pin; use axum::extract::State; use axum::response::Json; @@ -231,23 +233,38 @@ pub async fn overview( async move { // These reads are independent of one another; run them concurrently. The // hint/usage/diagnostics summaries then share the one durable-event fetch. - let (durable_events, observatory, agents, underused) = tokio::join!( - durable_analytics_rows_for_state(&state), - observatory_model(&state), - agent_usage_summary(&state.host_io, state.lcm_db.as_deref()), + let durable_events: Pin< + Box>> + Send>, + > = Box::pin(durable_analytics_rows_for_state(&state)); + let observatory: Pin + Send>> = + Box::pin(observatory_model(&state)); + let agents: Pin< + Box> + Send>, + > = Box::pin(agent_usage_summary(&state.host_io, state.lcm_db.as_deref())); + let underused = tracedecay_runtime_core::erase_send!( underused_tool_families(state.lcm_db.as_deref()), + Result>, String> ); + let (durable_events, observatory, agents, underused) = + tokio::join!(durable_events, observatory, agents, underused); let observatory = Some(observatory); let project_id = RegisteredGlobalDb::canonical_project_key(&state.project_root); - let (hints, usage, diagnostics) = tokio::join!( - hint_summary( + let hints: Pin + Send>> = + Box::pin(hint_summary( state.savings_db.as_deref().or(state.lcm_db.as_deref()), durable_events.as_deref(), Some(&project_id), - ), - usage_summary(state.lcm_db.as_deref(), durable_events.as_deref()), - typed_diagnostics_summary(&state, durable_events.as_deref()), - ); + )); + let usage: Pin< + Box> + Send>, + > = Box::pin(usage_summary( + state.lcm_db.as_deref(), + durable_events.as_deref(), + )); + let diagnostics: Pin< + Box> + Send>, + > = Box::pin(typed_diagnostics_summary(&state, durable_events.as_deref())); + let (hints, usage, diagnostics) = tokio::join!(hints, usage, diagnostics); let usage = match usage { Ok(usage) => usage, Err(error) => { diff --git a/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs b/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs index 68671b8b87..009a79c6ef 100644 --- a/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs +++ b/crates/tracedecay-dashboard-api/src/automation_jobs_api.rs @@ -327,8 +327,7 @@ async fn load_job_or_404( fn micros_now() -> u128 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_micros()) - .unwrap_or_default() + .map_or_default(|duration| duration.as_micros()) } fn generated_job_id(name: &str) -> String { diff --git a/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs b/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs index de1777ee29..874b979c4e 100644 --- a/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs +++ b/crates/tracedecay-dashboard-api/src/code_index_freshness_api.rs @@ -82,8 +82,7 @@ async fn project_code_index_freshness( let reason = worktree .parked .as_ref() - .map(|parked| parked.reason.clone()) - .unwrap_or_default(); + .map_or_default(|parked| parked.reason.clone()); DashboardEnvelopeV1::new( scope_from_state(state), DashboardDomainStateV1::Error, diff --git a/crates/tracedecay-dashboard-api/src/delivery_api.rs b/crates/tracedecay-dashboard-api/src/delivery_api.rs index 1284c73df4..604045029f 100644 --- a/crates/tracedecay-dashboard-api/src/delivery_api.rs +++ b/crates/tracedecay-dashboard-api/src/delivery_api.rs @@ -1017,8 +1017,7 @@ pub async fn inbox( let label = Path::new(&project.display_root) .file_name() .and_then(|name| name.to_str()) - .map(str::to_owned) - .unwrap_or_else(|| project.display_root.clone()); + .map_or_else(|| project.display_root.clone(), str::to_owned); let project_root = PathBuf::from(&project.canonical_root); let freshness = match state.code_index_freshness_reader.as_ref() { Some(reader) => reader(project_root.clone()).await, diff --git a/crates/tracedecay-dashboard-api/src/memory_service/facts.rs b/crates/tracedecay-dashboard-api/src/memory_service/facts.rs index 5b0be324ed..99b8fcd0b9 100644 --- a/crates/tracedecay-dashboard-api/src/memory_service/facts.rs +++ b/crates/tracedecay-dashboard-api/src/memory_service/facts.rs @@ -1,5 +1,8 @@ //! Memory fact, entity, and overview payloads for the dashboard memory API. +use std::future::Future; +use std::pin::Pin; + use schemars::JsonSchema; use serde::Serialize; use serde_json::{Map, Value, json}; @@ -13,7 +16,7 @@ use tracedecay_store::{ FactReadControl, ProjectMemoryDashboardEntityV1, ProjectMemoryDashboardFactSummaryV1, ProjectMemoryDashboardMemoryOverviewV1, ProjectMemoryFactProjectionV1, ProjectMemoryFactSearchGraphCoverageV1, ProjectMemoryFactSearchKindV1, - ProjectMemoryFactSearchQuery, + ProjectMemoryFactSearchPageV1, ProjectMemoryFactSearchQuery, }; pub const MEMORY_FACT_LIMIT_MINIMUM: i64 = 1; @@ -177,20 +180,26 @@ pub async fn fetch_facts( if !query.trim().is_empty() { let application = memory_application_for_db(state.memory_owner.clone(), &state.mem_db) .map_err(|error| error.to_string())?; - let page = application - .search_project_memory_facts( - ProjectMemoryFactSearchQuery::new( - state.memory_owner.clone(), - ProjectMemoryFactSearchKindV1::Search, - Some(query.trim().to_owned()), - None, - limit, - ) - .map_err(|error| error.to_string())?, - read_control, - ) - .await - .map_err(|error| error.to_string())?; + let search = ProjectMemoryFactSearchQuery::new( + state.memory_owner.clone(), + ProjectMemoryFactSearchKindV1::Search, + Some(query.trim().to_owned()), + None, + limit, + ) + .map_err(|error| error.to_string())?; + let page: Pin< + Box< + dyn Future< + Output = std::result::Result< + ProjectMemoryFactSearchPageV1, + tracedecay_session_memory::memory::MemoryApplicationError, + >, + > + Send + + '_, + >, + > = Box::pin(application.search_project_memory_facts(search, read_control)); + let page = page.await.map_err(|error| error.to_string())?; let rows = page .hits() .iter() diff --git a/crates/tracedecay-dashboard-api/src/project_gateway.rs b/crates/tracedecay-dashboard-api/src/project_gateway.rs index 06fa5d28f2..2d4eb25fcd 100644 --- a/crates/tracedecay-dashboard-api/src/project_gateway.rs +++ b/crates/tracedecay-dashboard-api/src/project_gateway.rs @@ -93,8 +93,7 @@ pub(super) async fn project_scoped_api_gateway( let query = req .uri() .query() - .map(|query| format!("?{query}")) - .unwrap_or_default(); + .map_or_default(|query| format!("?{query}")); if let Some(read) = application_read { let Some(project_graph) = selected.state.project_graph.as_deref() else { return gateway_problem( diff --git a/crates/tracedecay-dashboard-api/src/state.rs b/crates/tracedecay-dashboard-api/src/state.rs index 154a1936d5..d564c87ea7 100644 --- a/crates/tracedecay-dashboard-api/src/state.rs +++ b/crates/tracedecay-dashboard-api/src/state.rs @@ -479,10 +479,10 @@ pub(super) async fn build_state_inner( ), _ => None, }; - let savings_db_path = registered_savings_db - .as_ref() - .map(|db| db.db_path().display().to_string()) - .unwrap_or_else(|| cg.profile.global_db_path().display().to_string()); + let savings_db_path = registered_savings_db.as_ref().map_or_else( + || cg.profile.global_db_path().display().to_string(), + |db| db.db_path().display().to_string(), + ); let delivery_settlements = Arc::new( events_delivery::DashboardDeliverySettlementRegistryV1::new(delivery_settlement_authority), ); diff --git a/crates/tracedecay-domain/src/work_product_projection.rs b/crates/tracedecay-domain/src/work_product_projection.rs index 8be997279a..72b5a50764 100644 --- a/crates/tracedecay-domain/src/work_product_projection.rs +++ b/crates/tracedecay-domain/src/work_product_projection.rs @@ -436,8 +436,7 @@ impl WorkProductProjectionBundleV1 { &accepted, runtime_by_task .get(item.task_id()) - .map(Vec::as_slice) - .unwrap_or_default(), + .map_or_default(Vec::as_slice), unavailable_runtime_tasks.contains(item.task_id()), observed_at, ), diff --git a/crates/tracedecay-domain/tests/domain_suite/work_product_contract/critical_path.rs b/crates/tracedecay-domain/tests/domain_suite/work_product_contract/critical_path.rs index 1531cd2c33..eda44448da 100644 --- a/crates/tracedecay-domain/tests/domain_suite/work_product_contract/critical_path.rs +++ b/crates/tracedecay-domain/tests/domain_suite/work_product_contract/critical_path.rs @@ -141,8 +141,7 @@ fn a_dependency_chain_at_the_item_cap_projects_its_whole_path() { .map(|(ordinal, task_id)| { let dependencies = ordinal .checked_sub(1) - .map(|previous| vec![task_ids[previous].as_str()]) - .unwrap_or_default(); + .map_or_default(|previous| vec![task_ids[previous].as_str()]); item(task_id, &dependencies, 1) }) .collect(); diff --git a/crates/tracedecay-global-db/src/checkpoint_tests.rs b/crates/tracedecay-global-db/src/checkpoint_tests.rs index 1f773ce381..5b052254ef 100644 --- a/crates/tracedecay-global-db/src/checkpoint_tests.rs +++ b/crates/tracedecay-global-db/src/checkpoint_tests.rs @@ -14,9 +14,7 @@ const PRESSURED_WAL_LOAD_BATCHES: usize = 40; fn wal_file_bytes(database: &RegisteredGlobalDb) -> u64 { let mut wal = database.db_path().as_os_str().to_owned(); wal.push("-wal"); - std::fs::metadata(std::path::PathBuf::from(wal)) - .map(|metadata| metadata.len()) - .unwrap_or(0) + std::fs::metadata(std::path::PathBuf::from(wal)).map_or(0, |metadata| metadata.len()) } /// Creates the load table, pins a read snapshot on the empty WAL position, diff --git a/crates/tracedecay-global-db/src/configuration/store.rs b/crates/tracedecay-global-db/src/configuration/store.rs index 6d51a122b1..52fd67d000 100644 --- a/crates/tracedecay-global-db/src/configuration/store.rs +++ b/crates/tracedecay-global-db/src/configuration/store.rs @@ -1,5 +1,8 @@ //! `SQLite` persistence behind the final configuration control plane. +use std::future::Future; +use std::pin::Pin; + use super::contracts::{ ActivationDriftV1, AuthorizedActor, CONFIGURATION_AUDIT_PAGE_LIMIT, ComponentConfigurationState, ConfigurationAuditPage, ConfigurationAuditQuery, @@ -378,7 +381,13 @@ impl<'db> GlobalDbConfigurationControlStore<'db> { .begin_write_transaction() .await .map_err(|_| ConfigurationError::Unavailable)?; - let outcome = async { + let outcome: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(async { let current = current_state_from_transaction(&transaction).await?; if ¤t.revision_id != expected_revision_id { return Err(ConfigurationError::RevisionConflict); @@ -511,8 +520,8 @@ impl<'db> GlobalDbConfigurationControlStore<'db> { revision_id: revision.revision_id, snapshot: revision.snapshot, }) - } - .await; + }); + let outcome = outcome.await; match outcome { Ok(state) => { transaction diff --git a/crates/tracedecay-global-db/src/git_correlation_adapter.rs b/crates/tracedecay-global-db/src/git_correlation_adapter.rs index dab7dd4d8a..f1bde8c528 100644 --- a/crates/tracedecay-global-db/src/git_correlation_adapter.rs +++ b/crates/tracedecay-global-db/src/git_correlation_adapter.rs @@ -7,6 +7,10 @@ use std::borrow::Borrow; use std::collections::BTreeSet; +use std::future::Future; +use std::pin::Pin; + +use futures_util::future::BoxFuture; use tracedecay_runtime_core::db::DatabaseEngineReadSnapshot; use tracedecay_store::StoreShardScopeV1; @@ -196,11 +200,6 @@ where converge_git_evidence_pass(self, git, project_root).await } - #[tracing::instrument( - name = "global_db.git_correlation.bounded_history", - level = "trace", - skip_all - )] pub async fn run_bounded_history_index_page( &self, opts: &BackfillOptions, @@ -246,12 +245,39 @@ where ) -> Result<(Vec, CorrelationIndexPresence), GitCorrelationError> { self.require_project_sessions_authority()?; - let snapshot = self.read_snapshot().await?; + let opened: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(self.read_snapshot()); + let snapshot = opened.await?; let backfill_watermark = read_meta_value(&snapshot, GIT_HISTORY_SEQUENCE_FRONTIER_KEY).await?; - Ok(match open_git_evidence_view(&snapshot).await? { + let viewed: BoxFuture< + '_, + Result< + Option< + tracedecay_sessions::runtime::git_correlation::GitEvidenceView< + '_, + DatabaseEngineReadSnapshot, + >, + >, + GitCorrelationError, + >, + > = Box::pin(open_git_evidence_view(&snapshot)); + Ok(match viewed.await? { Some(view) => { - let results = view.sessions_for(query, relation).await?; + let matched: Pin< + Box< + dyn Future< + Output = Result, GitCorrelationError>, + > + Send + + '_, + >, + > = Box::pin(view.sessions_for(query, relation)); + let results = matched.await?; (results, view.presence(backfill_watermark)) } None => ( diff --git a/crates/tracedecay-global-db/src/observation/canonical_bodies.rs b/crates/tracedecay-global-db/src/observation/canonical_bodies.rs index 9c0d8f70fc..ba9baeaab4 100644 --- a/crates/tracedecay-global-db/src/observation/canonical_bodies.rs +++ b/crates/tracedecay-global-db/src/observation/canonical_bodies.rs @@ -369,11 +369,7 @@ mod tests { name.push(*suffix); std::path::PathBuf::from(name) }; - total.saturating_add( - std::fs::metadata(member) - .map(|meta| meta.len()) - .unwrap_or(0), - ) + total.saturating_add(std::fs::metadata(member).map_or(0, |meta| meta.len())) }) } diff --git a/crates/tracedecay-global-db/src/observation_adapter.rs b/crates/tracedecay-global-db/src/observation_adapter.rs index db461af911..592f71f7de 100644 --- a/crates/tracedecay-global-db/src/observation_adapter.rs +++ b/crates/tracedecay-global-db/src/observation_adapter.rs @@ -760,7 +760,24 @@ impl GlobalDbObservationStore { /// Persists the longest prefix of `writes` that settles in one runtime /// batch, returning its outcomes in input order and the writes that must /// be prepared again once that prefix is durable. - async fn persist_observation_segment( + /// + /// The segment future is erased here. A concrete child future would be + /// part of every caller's `Send` proof, and that chain exceeds the + /// recursion limit (rust-lang/rust#159228). + fn persist_observation_segment<'a>( + &'a self, + writes: Vec, + ) -> futures_util::future::BoxFuture< + 'a, + ObservationStoreResult<( + Vec, + Vec, + )>, + > { + Box::pin(async move { Self::persist_observation_segment_body(self, writes).await }) + } + + async fn persist_observation_segment_body( &self, writes: Vec, ) -> ObservationStoreResult<( diff --git a/crates/tracedecay-global-db/src/observation_projection.rs b/crates/tracedecay-global-db/src/observation_projection.rs index 603b0921c7..60ac79bee0 100644 --- a/crates/tracedecay-global-db/src/observation_projection.rs +++ b/crates/tracedecay-global-db/src/observation_projection.rs @@ -1,14 +1,17 @@ -mod apply; +pub(crate) mod apply; #[cfg(test)] mod rearm_tests; mod rebuild; mod schema; -mod source_transition; -mod state; +pub(crate) mod source_transition; +pub(crate) mod state; mod transition; +pub(crate) use apply::SendProjectionDerive; #[cfg(test)] pub(crate) use apply::apply_provider_usage_effects; +#[cfg(test)] +pub(crate) use apply::impl_send_projection_derive; pub(crate) use apply::provider_usage_scope; pub(super) use apply::{ ConvergedRendering, converge_released_output_rendering, derive_projection, @@ -23,7 +26,6 @@ pub(super) use schema::{ ensure_observation_projection_performance_indexes, ensure_observation_projection_schema, }; pub(crate) use source_transition::verify_native_source_supersession; -#[cfg(test)] pub(crate) use state::RearmedProjectionRetries; pub(super) use state::ensure_projection_output_state_cache; #[cfg(test)] diff --git a/crates/tracedecay-global-db/src/observation_projection/apply.rs b/crates/tracedecay-global-db/src/observation_projection/apply.rs index 1148679d45..0974601582 100644 --- a/crates/tracedecay-global-db/src/observation_projection/apply.rs +++ b/crates/tracedecay-global-db/src/observation_projection/apply.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; use serde::Deserialize; use tracedecay_domain::{ @@ -15,12 +17,16 @@ use tracedecay_store::{ use tracedecay_lcm::contracts::LcmError; use tracedecay_runtime_core::db::engine::{Executor, QueryExecutor, params}; +use tracedecay_runtime_core::db::{ + DatabaseEngineReadConnection, DatabaseEngineReadSnapshot, DatabaseWriteTransaction, +}; use tracedecay_sessions::runtime::store_access::find_preceding_codex_goal_response; use super::state::{ - canonicalize_session_project_paths, read_message, read_observation, read_output_state, - read_session, reaggregate_output_state_for_output, reconcile_session_rows_detailed, storage, - storage_message, stored_output_digest, verify_output_state, + canonicalize_session_project_paths, message_projection, read_message, read_observation, + read_output_state, read_session, reaggregate_output_state_for_output, + reconcile_session_rows_detailed, storage, storage_message, stored_output_digest, + verify_output_state, verify_projection_rows, }; use super::transition::{ MessageTransition, MessageTransitionState, WorkflowFactTarget, WorkflowFactTransition, @@ -42,9 +48,9 @@ pub(in super::super) fn derive_projection( } #[derive(Debug, Clone, PartialEq, Eq)] -struct ProjectionOutputAlias { - provider: String, - message_id: String, +pub(crate) struct ProjectionOutputAlias { + pub(crate) provider: String, + pub(crate) message_id: String, } const LIVE_PROJECTION_AUTHORITY_SQL: &str = "SELECT disposition.reason, @@ -70,12 +76,12 @@ const REBUILD_PROJECTION_AUTHORITY_SQL: &str = "SELECT disposition.reason, AND alias.generation = target.generation AND alias.observation_id = target.observation_id"; -struct ProjectionAuthority { - disposition: Option, - alias: Option, +pub(crate) struct ProjectionAuthority { + pub(crate) disposition: Option, + pub(crate) alias: Option, } -async fn read_projection_authority( +pub(crate) async fn read_projection_authority( conn: &impl QueryExecutor, observation_id: &CanonicalObservationIdV1, rebuild_generation: Option<&str>, @@ -140,7 +146,7 @@ async fn read_projection_authority( }) } -async fn read_projection_alias( +pub(crate) async fn read_projection_alias( conn: &impl QueryExecutor, observation_id: &CanonicalObservationIdV1, rebuild_generation: Option<&str>, @@ -184,96 +190,494 @@ async fn read_projection_alias( })) } -pub(in super::super) async fn derive_projection_with_alias( - conn: &impl QueryExecutor, - observation: &DurableObservationV1, -) -> ProjectionStoreResult { - derive_projection_with_alias_from_generation(conn, observation, None).await +/// Connection that can derive a projection as one erased `Send` future. +/// +/// Native-source verification calls derivation, and derivation calls +/// verification. A generic `async fn` makes that a structural `Send` cycle the +/// next trait solver walks until the recursion limit. Each connection type +/// builds the future here, so the cycle is a `dyn Future + Send` instead. +pub(crate) trait SendProjectionDerive: QueryExecutor + Sync { + fn derive_projection_alias<'a>( + &'a self, + observation: &'a DurableObservationV1, + rebuild_generation: Option<&'a str>, + ) -> Pin> + Send + 'a>>; + + fn verify_native_source_supersession<'a>( + &'a self, + predecessor: &'a DurableObservationV1, + ) -> Pin> + Send + 'a>>; } -pub(super) async fn derive_projection_for_rebuild( - conn: &impl QueryExecutor, - observation: &DurableObservationV1, - generation: &str, -) -> ProjectionStoreResult { - derive_projection_with_alias_from_generation(conn, observation, Some(generation)).await +pub(in super::super) fn derive_projection_with_alias<'a>( + conn: &'a impl SendProjectionDerive, + observation: &'a DurableObservationV1, +) -> Pin> + Send + 'a>> { + conn.derive_projection_alias(observation, None) } -#[tracing::instrument( - name = "global_db.observation_apply.derive.alias", - level = "trace", - skip_all -)] -async fn derive_projection_with_alias_from_generation( - conn: &impl QueryExecutor, - observation: &DurableObservationV1, - rebuild_generation: Option<&str>, -) -> ProjectionStoreResult { - let authority = - read_projection_authority(conn, observation.observation_id(), rebuild_generation).await?; - if authority.disposition == Some(ProjectionSkipReason::NativeSourceSuperseded) { - Box::pin(super::source_transition::verify_native_source_supersession( - conn, - observation, - )) - .await?; - return Ok(ObservationProjection::Skipped( - ProjectionSkipReason::NativeSourceSuperseded, - )); - } - if let Some(reason) = authority.disposition - && matches!( - reason, - ProjectionSkipReason::OutputCollision - | ProjectionSkipReason::InvalidContract - | ProjectionSkipReason::SanitizationRefused - | ProjectionSkipReason::SourceRecordRetired - ) - { - return Ok(ObservationProjection::Skipped(reason)); - } - let projection = derive_projection(observation)?; - // Collapse Codex-style token/time goal ticks at projection time so every - // raw observation stays durable while current goal state follows the - // established (thread, objective, status) transition semantics. - let projection = - collapse_consecutive_goal_ticks(conn, observation, projection, rebuild_generation).await?; - let mut alias = authority.alias; - if alias.is_none() - && let Some(predecessor) = - super::source_transition::read_native_source_predecessor(conn, observation).await? - { - alias = - read_projection_alias(conn, predecessor.observation_id(), rebuild_generation).await?; - } - let Some(alias) = alias else { - return Ok(projection); - }; - let (projection, workflow_facts) = match projection { - ObservationProjection::Message(projection) => (projection, Vec::new()), - ObservationProjection::Composite { - message: Some(projection), - workflow_facts, - } => (projection, workflow_facts), - ObservationProjection::Composite { message: None, .. } - | ObservationProjection::Skipped(_) => { - return Err(ProjectionStoreError::ProvenanceCollision); +pub(super) fn derive_projection_for_rebuild<'a>( + conn: &'a impl SendProjectionDerive, + observation: &'a DurableObservationV1, + generation: &'a str, +) -> Pin> + Send + 'a>> { + conn.derive_projection_alias(observation, Some(generation)) +} + +macro_rules! impl_send_projection_derive { + ($ty:ty) => { + impl $crate::observation_projection::SendProjectionDerive for $ty { + fn derive_projection_alias<'a>( + &'a self, + observation: &'a tracedecay_domain::DurableObservationV1, + rebuild_generation: Option<&'a str>, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult< + tracedecay_store::ObservationProjection, + >, + > + Send + + 'a, + >, + > { + Box::pin(tracing::Instrument::instrument( + async move { + let authority: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult< + $crate::observation_projection::apply::ProjectionAuthority, + >, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::apply::read_projection_authority( + self, + observation.observation_id(), + rebuild_generation, + ), + ); + let authority = authority.await?; + if authority.disposition + == Some(tracedecay_store::ProjectionSkipReason::NativeSourceSuperseded) + { + self.verify_native_source_supersession(observation).await?; + return Ok(tracedecay_store::ObservationProjection::Skipped( + tracedecay_store::ProjectionSkipReason::NativeSourceSuperseded, + )); + } + if let Some(reason) = authority.disposition + && matches!( + reason, + tracedecay_store::ProjectionSkipReason::OutputCollision + | tracedecay_store::ProjectionSkipReason::InvalidContract + | tracedecay_store::ProjectionSkipReason::SanitizationRefused + | tracedecay_store::ProjectionSkipReason::SourceRecordRetired + ) + { + return Ok(tracedecay_store::ObservationProjection::Skipped(reason)); + } + let projection = + $crate::observation_projection::derive_projection(observation)?; + // Collapse Codex-style token/time goal ticks at projection time so every + // raw observation stays durable while current goal state follows the + // established (thread, objective, status) transition semantics. + let collapsed: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult< + tracedecay_store::ObservationProjection, + >, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::apply::collapse_consecutive_goal_ticks( + self, + observation, + projection, + rebuild_generation, + ), + ); + let projection = collapsed.await?; + let mut alias = authority.alias; + if alias.is_none() { + let predecessor: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult< + Option, + >, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::source_transition::read_native_source_predecessor( + self, + observation, + ), + ); + if let Some(predecessor) = predecessor.await? { + let aliased: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult< + Option< + $crate::observation_projection::apply::ProjectionOutputAlias, + >, + >, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::apply::read_projection_alias( + self, + predecessor.observation_id(), + rebuild_generation, + ), + ); + alias = aliased.await?; + } + } + let Some(alias) = alias else { + return Ok(projection); + }; + let (projection, workflow_facts) = match projection { + tracedecay_store::ObservationProjection::Message(projection) => { + (projection, Vec::new()) + } + tracedecay_store::ObservationProjection::Composite { + message: Some(projection), + workflow_facts, + } => (projection, workflow_facts), + tracedecay_store::ObservationProjection::Composite { message: None, .. } + | tracedecay_store::ObservationProjection::Skipped(_) => { + return Err(tracedecay_store::ProjectionStoreError::ProvenanceCollision); + } + }; + let mut session = projection.session().clone(); + let mut message = projection.message().clone(); + session.provider.clone_from(&alias.provider); + message.provider = alias.provider; + message.message_id = alias.message_id; + tracedecay_store::ObservationProjection::for_outputs( + observation, + Some((session, message)), + workflow_facts + .into_iter() + .map(|projection| { + (projection.session().clone(), projection.fact().clone()) + }) + .collect(), + ) + }, + tracing::trace_span!("global_db.observation_apply.derive.alias"), + )) + } + + fn verify_native_source_supersession<'a>( + &'a self, + predecessor: &'a tracedecay_domain::DurableObservationV1, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult<()>, + > + Send + + 'a, + >, + > { + Box::pin(async move { + let successor: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult< + Option, + >, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::source_transition::read_native_source_successor( + self, + predecessor, + ), + ); + let successor = successor + .await? + .ok_or(tracedecay_store::ProjectionStoreError::ProvenanceCollision)?; + let effect = self.derive_projection_alias(&successor, None).await?; + if matches!(effect.skip_reason(), Some(reason) if reason != tracedecay_store::ProjectionSkipReason::NonConversationalRecord) + { + return Err(tracedecay_store::ProjectionStoreError::ProvenanceCollision); + } + if effect.skip_reason().is_some() { + let verified: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult<()>, + > + Send + + '_, + >, + > = Box::pin($crate::observation_projection::apply::verify_effect( + self, + &successor, + &effect, + )); + verified.await?; + } else { + let usage: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult<()>, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::apply::verify_observation_provider_usage( + self, + &successor, + ), + ); + usage.await?; + // Reopen audits use read-only connections without the live writer's + // temporary cache. Resolve retained ownership through the same bounded + // authority used by the canonical projection audit. + let outputs = effect + .message() + .into_iter() + .map(|projection| { + ( + projection.message().provider.clone(), + projection.message().message_id.clone(), + ) + }) + .collect::>(); + let authorities: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult< + std::collections::HashMap< + (String, String), + $crate::observation_projection::ProjectionOutputAuthority, + >, + >, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::read_output_authorities( + self, + &outputs, + ), + ); + let authorities = authorities.await?; + for projection in effect.message().into_iter() { + $crate::observation_projection::apply::verify_provenance( + self, + projection, + ) + .await?; + let message = projection.message(); + let authority = authorities + .get(&(message.provider.clone(), message.message_id.clone())) + .ok_or(tracedecay_store::ProjectionStoreError::ProvenanceCollision)?; + let owner: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult< + tracedecay_store::SessionMessageProjection, + >, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::resolve_output_projection( + self, + authority, + Some((successor.observation_id().as_str(), &effect)), + projection, + ), + ); + let owner = owner.await?; + $crate::observation_projection::apply::verify_provenance(self, &owner) + .await?; + $crate::observation_projection::state::verify_projection_rows( + self, + &owner, + ) + .await?; + } + let workflow: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult<()>, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::apply::verify_workflow_effects( + self, + effect.workflow_facts(), + ), + ); + workflow.await?; + } + let anchors: std::pin::Pin< + Box< + dyn std::future::Future< + Output = tracedecay_store::ProjectionStoreResult<( + tracedecay_domain::RetrievalAnchorRecord, + tracedecay_domain::RetrievalAnchorRecord, + )>, + > + Send + + '_, + >, + > = Box::pin( + $crate::observation_projection::source_transition::read_transition_anchors( + self, + predecessor, + &successor, + ), + ); + let (old_anchor, new_anchor) = anchors.await?; + let owner = old_anchor.owner_column_json().map_err(|error| { + $crate::observation_projection::state::storage( + "encode native source owner", + error, + ) + })?; + let mut disposition = self + .query( + "SELECT state, superseded_by, reason_class FROM retrieval_anchor_dispositions + WHERE anchor_id = ?1 AND owner_json = ?2 ORDER BY sequence DESC LIMIT 1", + tracedecay_runtime_core::db::engine::params![ + old_anchor.anchor_id().as_str(), + owner.as_str() + ], + ) + .await + .map_err(|error| { + $crate::observation_projection::state::storage( + "verify native source anchor supersession", + error, + ) + })?; + let current = disposition + .next() + .await + .map_err(|error| { + $crate::observation_projection::state::storage( + "verify native source anchor supersession", + error, + ) + })? + .ok_or(tracedecay_store::ProjectionStoreError::ProvenanceCollision)?; + if current.get::(0).map_err(|error| { + $crate::observation_projection::state::storage( + "verify native source anchor supersession", + error, + ) + })? != "superseded" + || current.get::>(1).map_err(|error| { + $crate::observation_projection::state::storage( + "verify native source anchor supersession", + error, + ) + })?.as_deref() + != Some(new_anchor.anchor_id().as_str()) + || current.get::(2).map_err(|error| { + $crate::observation_projection::state::storage( + "verify native source anchor supersession", + error, + ) + })? != "correction" + { + return Err(tracedecay_store::ProjectionStoreError::ProvenanceCollision); + } + drop(disposition); + let mut aliases = self + .query( + "SELECT COUNT(*) FROM retrieval_anchor_aliases WHERE owner_json = ?1 AND anchor_id = ?2", + tracedecay_runtime_core::db::engine::params![ + owner, + new_anchor.anchor_id().as_str() + ], + ) + .await + .map_err(|error| { + $crate::observation_projection::state::storage( + "verify native source current alias", + error, + ) + })?; + let count: i64 = aliases + .next() + .await + .map_err(|error| { + $crate::observation_projection::state::storage( + "verify native source current alias", + error, + ) + })? + .ok_or(tracedecay_store::ProjectionStoreError::ProvenanceCollision)? + .get(0) + .map_err(|error| { + $crate::observation_projection::state::storage( + "verify native source current alias", + error, + ) + })?; + if usize::try_from(count).ok() != Some(new_anchor.aliases().len()) { + return Err(tracedecay_store::ProjectionStoreError::ProvenanceCollision); + } + drop(aliases); + let mut rows = self + .query( + "SELECT 1 FROM observation_projection_provenance WHERE projector_version = ?1 AND observation_id = ?2 + UNION ALL SELECT 1 FROM observation_workflow_facts WHERE projector_version = ?1 AND observation_id = ?2 + UNION ALL SELECT 1 FROM observation_projection_aliases WHERE projector_version = ?1 AND observation_id = ?2 + LIMIT 1", + tracedecay_runtime_core::db::engine::params![ + tracedecay_store::SESSION_MESSAGE_PROJECTOR_VERSION, + predecessor.observation_id().as_str() + ], + ) + .await + .map_err(|error| { + $crate::observation_projection::state::storage( + "verify superseded native source effects", + error, + ) + })?; + if rows + .next() + .await + .map_err(|error| { + $crate::observation_projection::state::storage( + "verify superseded native source effects", + error, + ) + })? + .is_some() + { + return Err(tracedecay_store::ProjectionStoreError::ProvenanceCollision); + } + drop(rows); + Ok(()) + }) + } } }; - let mut session = projection.session().clone(); - let mut message = projection.message().clone(); - session.provider.clone_from(&alias.provider); - message.provider = alias.provider; - message.message_id = alias.message_id; - ObservationProjection::for_outputs( - observation, - Some((session, message)), - workflow_facts - .into_iter() - .map(|projection| (projection.session().clone(), projection.fact().clone())) - .collect(), - ) } +#[cfg(test)] +pub(crate) use impl_send_projection_derive; + +impl_send_projection_derive!(DatabaseWriteTransaction<'_>); +impl_send_projection_derive!(DatabaseEngineReadConnection); +impl_send_projection_derive!(DatabaseEngineReadSnapshot); +impl_send_projection_derive!(crate::registered::RegisteredGlobalDbWriteTransaction<'_>); /// Objective used for goal-state dedupe: prefer native `/objective` (Codex), /// else the already-extracted `content_text`. @@ -389,7 +793,7 @@ async fn read_latest_goal_dedupe_key( level = "trace", skip_all )] -async fn collapse_consecutive_goal_ticks( +pub(crate) async fn collapse_consecutive_goal_ticks( conn: &impl QueryExecutor, observation: &DurableObservationV1, projection: ObservationProjection, @@ -959,7 +1363,7 @@ async fn retire_quarantined_projection( skip_all )] async fn apply_rows( - conn: &impl Executor, + conn: &DatabaseWriteTransaction<'_>, sequence: u64, observation: &DurableObservationV1, projection: &SessionMessageProjection, @@ -974,7 +1378,37 @@ async fn apply_rows( if existing.is_some() && let Some(state) = state.as_ref() { - verify_output_state(conn, state, projection).await?; + // Erase the projection child here. `verify_output_state` stays generic + // over `QueryExecutor`, whose futures are not `Send`, so the write + // transaction is the boundary that can prove the child on its own. + let verified: Pin> + Send + '_>> = + Box::pin(async { + if state.owner_count == 0 { + return Err(ProjectionStoreError::ProvenanceCollision); + } + let message = projection.message(); + let owner: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(message_projection( + conn, + &state.canonical, + &message.provider, + &message.message_id, + )); + let owner_projection = owner.await?; + let provenance: Pin< + Box> + Send + '_>, + > = Box::pin(verify_provenance(conn, &owner_projection)); + provenance.await?; + let rows: Pin> + Send + '_>> = + Box::pin(verify_projection_rows(conn, &owner_projection)); + rows.await + }); + verified.await?; } let transition_state = state.as_ref().map(|state| { MessageTransitionState::new( @@ -984,14 +1418,16 @@ async fn apply_rows( state.projector_owned, ) }); - let transition = message_transition( + let decided: Pin< + Box> + Send + '_>, + > = Box::pin(message_transition( conn, sequence, projection, existing.as_ref(), transition_state, - ) - .await?; + )); + let transition = decided.await?; match transition { MessageTransition::Insert => upsert_projected_raw_message(conn, message).await?, MessageTransition::Supersede => supersede_projected_message(conn, message).await?, @@ -1194,7 +1630,7 @@ async fn apply_workflow_fact( level = "trace", skip_all )] -pub(super) async fn verify_provenance( +pub(crate) async fn verify_provenance( conn: &impl QueryExecutor, projection: &SessionMessageProjection, ) -> ProjectionStoreResult<()> { @@ -1493,7 +1929,7 @@ async fn apply_skip_disposition_for_receipt( } async fn verify_message_effect( - conn: &impl QueryExecutor, + conn: &impl SendProjectionDerive, projection: &SessionMessageProjection, ) -> ProjectionStoreResult<()> { verify_provenance(conn, projection).await?; @@ -1504,14 +1940,16 @@ async fn verify_message_effect( } async fn apply_message_effect( - conn: &impl Executor, + conn: &DatabaseWriteTransaction<'_>, sequence: u64, observation: &DurableObservationV1, projection: &SessionMessageProjection, ) -> ProjectionStoreResult<()> { let message = projection.message(); let previous_owner = read_canonical_owner(conn, &message.provider, &message.message_id).await?; - let message_created = apply_rows(conn, sequence, observation, projection).await?; + let rows: Pin> + Send + '_>> = + Box::pin(apply_rows(conn, sequence, observation, projection)); + let message_created = rows.await?; apply_provenance(conn, sequence, projection, message_created).await?; // The session-temporal store materialized the previous owner's // occurrence; once another observation owns the message, that occurrence @@ -1560,7 +1998,7 @@ async fn read_canonical_owner( /// was retired; an observation the store never held, or one already settled /// by another disposition, has no projection to retire. pub(in super::super) async fn retire_source_record( - conn: &impl Executor, + conn: &(impl Executor + SendProjectionDerive), observation_id: &str, ) -> ProjectionStoreResult { let mut rows = conn @@ -2040,7 +2478,7 @@ async fn provider_usage_observation_sequence( .map_err(|error| storage("read provider usage observation sequence", error)) } -pub(super) async fn verify_observation_provider_usage( +pub(crate) async fn verify_observation_provider_usage( conn: &impl QueryExecutor, observation: &DurableObservationV1, ) -> ProjectionStoreResult<()> { @@ -2050,7 +2488,7 @@ pub(super) async fn verify_observation_provider_usage( #[tracing::instrument(name = "global_db.observation_apply.query", level = "trace", skip_all)] pub async fn verify_effect( - conn: &impl QueryExecutor, + conn: &impl SendProjectionDerive, observation: &DurableObservationV1, effect: &ObservationProjection, ) -> ProjectionStoreResult<()> { @@ -2102,7 +2540,7 @@ pub async fn verify_workflow_effects( skip_all )] pub(super) async fn apply_effect( - conn: &impl Executor, + conn: &DatabaseWriteTransaction<'_>, sequence: u64, observation: &DurableObservationV1, effect: &ObservationProjection, @@ -2126,20 +2564,23 @@ pub(super) async fn apply_effect( // into that already-deep composition overflows the base-opt worker stack. match effect { ObservationProjection::Message(projection) => { - Box::pin(apply_message_effect( - conn, - sequence, - observation, - projection, - )) - .await + let applied: Pin> + Send + '_>> = + Box::pin(apply_message_effect( + conn, + sequence, + observation, + projection, + )); + applied.await } ObservationProjection::Composite { message, workflow_facts, } => { if let Some(message) = message { - Box::pin(apply_message_effect(conn, sequence, observation, message)).await?; + let applied: Pin> + Send + '_>> = + Box::pin(apply_message_effect(conn, sequence, observation, message)); + applied.await?; } for projection in workflow_facts { apply_workflow_fact(conn, sequence, projection).await?; diff --git a/crates/tracedecay-global-db/src/observation_projection/rebuild.rs b/crates/tracedecay-global-db/src/observation_projection/rebuild.rs index a57c1ed1c0..b4d705b248 100644 --- a/crates/tracedecay-global-db/src/observation_projection/rebuild.rs +++ b/crates/tracedecay-global-db/src/observation_projection/rebuild.rs @@ -1,9 +1,11 @@ +use std::future::Future; +use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use tracedecay_domain::{CanonicalObservationIdV1, DurableObservationV1}; use tracedecay_lcm::retrieval_content::projected_content_hash; use tracedecay_runtime_core::db::{ - Database, + Database, DatabaseWriteTransaction, engine::{Executor, QueryExecutor, Row, params}, }; use tracedecay_store::{ @@ -137,7 +139,15 @@ pub async fn project_observation( })?, }); } - match project_observation_in_transaction(&transaction, observation_id).await { + // The apply future is erased so this function's `Send` proof stops here + // (rust-lang/rust#159228). The concrete child is what overflowed. + let projected: Pin< + Box> + Send + '_>, + > = Box::pin(project_observation_in_transaction( + &transaction, + observation_id, + )); + match projected.await { Ok(outcome) => match transaction.commit().await { Ok(()) => Ok(outcome), Err(commit_error) => { @@ -564,7 +574,7 @@ async fn advance_projection_rebuild( } async fn project_observation_in_transaction( - transaction: &impl Executor, + transaction: &DatabaseWriteTransaction<'_>, observation_id: &CanonicalObservationIdV1, ) -> ProjectionStoreResult { project_observation_in_transaction_with_session(transaction, observation_id) @@ -576,7 +586,7 @@ async fn project_observation_in_transaction( /// observation's session identity so batched drains need no follow-up point /// read per projected item. async fn project_observation_in_transaction_with_session( - transaction: &impl Executor, + transaction: &DatabaseWriteTransaction<'_>, observation_id: &CanonicalObservationIdV1, ) -> ProjectionStoreResult<(ProjectionPersistOutcome, String)> { ensure_projection_output_state_cache(transaction).await?; @@ -794,7 +804,7 @@ async fn stage_projection_alias_batch_transaction( } async fn stage_projection_rebuild_batch_transaction( - transaction: &impl Executor, + transaction: &DatabaseWriteTransaction<'_>, cancelled: &AtomicBool, ) -> ProjectionStoreResult { let job = read_rebuild_job(transaction).await?; @@ -919,7 +929,7 @@ enum RebuildBatchStage { } async fn activate_projection_rebuild_transaction( - transaction: &impl Executor, + transaction: &(impl Executor + super::apply::SendProjectionDerive), frontier_sequence: u64, ) -> ProjectionStoreResult { validate_rebuild_frontier(transaction, frontier_sequence).await?; @@ -1003,15 +1013,27 @@ enum CollisionGuardedWrite<'a> { impl CollisionGuardedWrite<'_> { async fn run( &self, - conn: &impl Executor, + conn: &DatabaseWriteTransaction<'_>, sequence: u64, observation: &DurableObservationV1, effect: &ObservationProjection, ) -> ProjectionStoreResult<()> { match self { - Self::Drain => apply_effect(conn, sequence, observation, effect).await, + Self::Drain => { + let applied: Pin> + Send + '_>> = + Box::pin(apply_effect(conn, sequence, observation, effect)); + applied.await + } Self::Stage { generation } => { - stage_rebuild_effect(conn, generation, sequence, observation, effect).await + let staged: Pin> + Send + '_>> = + Box::pin(stage_rebuild_effect( + conn, + generation, + sequence, + observation, + effect, + )); + staged.await } } } @@ -1024,7 +1046,7 @@ impl CollisionGuardedWrite<'_> { /// wedging on the collided output. This is the write-time backstop for /// collisions not yet recorded as a disposition. async fn write_effect_converging_collisions( - conn: &impl Executor, + conn: &DatabaseWriteTransaction<'_>, write: &CollisionGuardedWrite<'_>, sequence: u64, observation: &DurableObservationV1, @@ -1033,7 +1055,11 @@ async fn write_effect_converging_collisions( conn.execute_batch(&format!("SAVEPOINT {PROJECTION_COLLISION_SAVEPOINT};")) .await .map_err(|error| storage("begin projection collision savepoint", error))?; - match write.run(conn, sequence, observation, effect).await { + // The guarded write is its own `Send` root. Inlining it here overflows + // the projection future (rust-lang/rust#159228). + let ran: Pin> + Send + '_>> = + Box::pin(write.run(conn, sequence, observation, effect)); + match ran.await { Ok(()) => { conn.execute_batch(&format!("RELEASE {PROJECTION_COLLISION_SAVEPOINT};")) .await @@ -1050,7 +1076,15 @@ async fn write_effect_converging_collisions( observation = observation.observation_id().as_str(), "projection output collided; recording a durable skip disposition" ); - converge_collided_effect(conn, write, sequence, observation, effect).await + let converged: Pin> + Send + '_>> = + Box::pin(converge_collided_effect( + conn, + write, + sequence, + observation, + effect, + )); + converged.await } Err(ProjectionStoreError::SessionOutputCollision { provider, @@ -1071,11 +1105,31 @@ async fn write_effect_converging_collisions( ); match write { CollisionGuardedWrite::Drain => { - isolate_session_collision(conn, observation, &collision, effect).await?; - write.run(conn, sequence, observation, effect).await + let isolated: Pin< + Box> + Send + '_>, + > = Box::pin(isolate_session_collision( + conn, + observation, + &collision, + effect, + )); + isolated.await?; + let rerun: Pin< + Box> + Send + '_>, + > = Box::pin(write.run(conn, sequence, observation, effect)); + rerun.await } CollisionGuardedWrite::Stage { .. } => { - converge_collided_effect(conn, write, sequence, observation, effect).await + let converged: Pin< + Box> + Send + '_>, + > = Box::pin(converge_collided_effect( + conn, + write, + sequence, + observation, + effect, + )); + converged.await } } } @@ -1095,7 +1149,7 @@ async fn write_effect_converging_collisions( /// The projected-output ownership cache is re-aggregated for every output the /// removed rows touched. async fn converge_collided_effect( - conn: &impl Executor, + conn: &DatabaseWriteTransaction<'_>, write: &CollisionGuardedWrite<'_>, sequence: u64, observation: &DurableObservationV1, @@ -1111,7 +1165,9 @@ async fn converge_collided_effect( reconcile_collided_observation_provenance(conn, observation).await?; } *effect = ObservationProjection::Skipped(ProjectionSkipReason::OutputCollision); - write.run(conn, sequence, observation, effect).await + let rerun: Pin> + Send + '_>> = + Box::pin(write.run(conn, sequence, observation, effect)); + rerun.await } /// Live-drain session collision: keep the queue row, record the error, and @@ -1843,7 +1899,7 @@ async fn stage_rebuild_disposition( } async fn stage_rebuild_effect( - conn: &impl Executor, + conn: &(impl Executor + super::apply::SendProjectionDerive), generation: &str, sequence: u64, observation: &DurableObservationV1, diff --git a/crates/tracedecay-global-db/src/observation_projection/source_transition.rs b/crates/tracedecay-global-db/src/observation_projection/source_transition.rs index 244d6f5300..97b75f2308 100644 --- a/crates/tracedecay-global-db/src/observation_projection/source_transition.rs +++ b/crates/tracedecay-global-db/src/observation_projection/source_transition.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeSet; - use tracedecay_domain::{ CanonicalObservationIdV1, ClineTranscriptStream, DurableObservationV1, FactOwnerV1, ObservationIdentityMaterialV1, RetrievalAnchorRecord, RetrievalAnchorTarget, @@ -14,14 +12,7 @@ use tracedecay_store::{ use crate::observation::decode_observation_json; -use super::apply::{ - derive_projection_with_alias, verify_effect, verify_observation_provider_usage, - verify_provenance, verify_workflow_effects, -}; -use super::state::{ - read_observation, read_output_authorities, reaggregate_output_state_for_output, - resolve_output_projection, storage, verify_projection_rows, -}; +use super::state::{read_observation, reaggregate_output_state_for_output, storage}; #[derive(Clone, Copy)] pub(super) enum SourceTransitionTarget<'a> { @@ -30,124 +21,14 @@ pub(super) enum SourceTransitionTarget<'a> { } /// Resolve immutable successor evidence; a disposition label alone is never authority. -pub(crate) async fn verify_native_source_supersession( - conn: &impl QueryExecutor, - predecessor: &DurableObservationV1, -) -> ProjectionStoreResult<()> { - let successor = read_native_source_successor(conn, predecessor) - .await? - .ok_or(ProjectionStoreError::ProvenanceCollision)?; - let effect = Box::pin(derive_projection_with_alias(conn, &successor)).await?; - if matches!(effect.skip_reason(), Some(reason) if reason != ProjectionSkipReason::NonConversationalRecord) - { - return Err(ProjectionStoreError::ProvenanceCollision); - } - if effect.skip_reason().is_some() { - Box::pin(verify_effect(conn, &successor, &effect)).await?; - } else { - verify_observation_provider_usage(conn, &successor).await?; - // Reopen audits use read-only connections without the live writer's - // temporary cache. Resolve retained ownership through the same bounded - // authority used by the canonical projection audit. - let outputs = effect - .message() - .into_iter() - .map(|projection| { - ( - projection.message().provider.clone(), - projection.message().message_id.clone(), - ) - }) - .collect::>(); - let authorities = read_output_authorities(conn, &outputs).await?; - for projection in effect.message().into_iter() { - verify_provenance(conn, projection).await?; - let message = projection.message(); - let authority = authorities - .get(&(message.provider.clone(), message.message_id.clone())) - .ok_or(ProjectionStoreError::ProvenanceCollision)?; - let owner = resolve_output_projection( - conn, - authority, - Some((successor.observation_id().as_str(), &effect)), - projection, - ) - .await?; - verify_provenance(conn, &owner).await?; - verify_projection_rows(conn, &owner).await?; - } - verify_workflow_effects(conn, effect.workflow_facts()).await?; - } - let (old_anchor, new_anchor) = read_transition_anchors(conn, predecessor, &successor).await?; - let owner = old_anchor - .owner_column_json() - .map_err(|error| storage("encode native source owner", error))?; - let mut disposition = conn - .query( - "SELECT state, superseded_by, reason_class FROM retrieval_anchor_dispositions - WHERE anchor_id = ?1 AND owner_json = ?2 ORDER BY sequence DESC LIMIT 1", - params![old_anchor.anchor_id().as_str(), owner.as_str()], - ) - .await - .map_err(|error| storage("verify native source anchor supersession", error))?; - let current = disposition - .next() - .await - .map_err(|error| storage("verify native source anchor supersession", error))? - .ok_or(ProjectionStoreError::ProvenanceCollision)?; - if current - .get::(0) - .map_err(|error| storage("verify native source anchor supersession", error))? - != "superseded" - || current - .get::>(1) - .map_err(|error| storage("verify native source anchor supersession", error))? - .as_deref() - != Some(new_anchor.anchor_id().as_str()) - || current - .get::(2) - .map_err(|error| storage("verify native source anchor supersession", error))? - != "correction" - { - return Err(ProjectionStoreError::ProvenanceCollision); - } - drop(disposition); - let mut aliases = conn.query( - "SELECT COUNT(*) FROM retrieval_anchor_aliases WHERE owner_json = ?1 AND anchor_id = ?2", - params![owner, new_anchor.anchor_id().as_str()], - ).await.map_err(|error| storage("verify native source current alias", error))?; - let count: i64 = aliases - .next() - .await - .map_err(|error| storage("verify native source current alias", error))? - .ok_or(ProjectionStoreError::ProvenanceCollision)? - .get(0) - .map_err(|error| storage("verify native source current alias", error))?; - if usize::try_from(count).ok() != Some(new_anchor.aliases().len()) { - return Err(ProjectionStoreError::ProvenanceCollision); - } - drop(aliases); - let mut rows = conn.query( - "SELECT 1 FROM observation_projection_provenance WHERE projector_version = ?1 AND observation_id = ?2 - UNION ALL SELECT 1 FROM observation_workflow_facts WHERE projector_version = ?1 AND observation_id = ?2 - UNION ALL SELECT 1 FROM observation_projection_aliases WHERE projector_version = ?1 AND observation_id = ?2 - LIMIT 1", - params![SESSION_MESSAGE_PROJECTOR_VERSION, predecessor.observation_id().as_str()], - ).await.map_err(|error| storage("verify superseded native source effects", error))?; - if rows - .next() - .await - .map_err(|error| storage("verify superseded native source effects", error))? - .is_some() - { - return Err(ProjectionStoreError::ProvenanceCollision); - } - drop(rows); - - Ok(()) +pub(crate) fn verify_native_source_supersession<'a>( + conn: &'a impl super::apply::SendProjectionDerive, + predecessor: &'a DurableObservationV1, +) -> std::pin::Pin> + Send + 'a>> { + conn.verify_native_source_supersession(predecessor) } -async fn read_native_source_successor( +pub(crate) async fn read_native_source_successor( conn: &impl QueryExecutor, predecessor: &DurableObservationV1, ) -> ProjectionStoreResult> { @@ -190,7 +71,7 @@ async fn read_native_source_successor( Ok(None) } -pub(super) async fn read_native_source_predecessor( +pub(crate) async fn read_native_source_predecessor( conn: &impl QueryExecutor, successor: &DurableObservationV1, ) -> ProjectionStoreResult> { @@ -211,7 +92,7 @@ pub(super) async fn read_native_source_predecessor( /// Runs after the successor effect succeeds, within its existing savepoint. /// Staging removes only staged effects; live authority changes at activation. pub(super) async fn settle_native_source_transition( - conn: &impl Executor, + conn: &(impl Executor + super::apply::SendProjectionDerive), successor: &DurableObservationV1, target: SourceTransitionTarget<'_>, ) -> ProjectionStoreResult<()> { @@ -418,7 +299,7 @@ async fn observation_anchor( Ok(anchor) } -async fn read_transition_anchors( +pub(crate) async fn read_transition_anchors( conn: &impl QueryExecutor, predecessor: &DurableObservationV1, successor: &DurableObservationV1, @@ -516,7 +397,7 @@ async fn promote_native_source_anchor( /// Activation has already replaced staged messages and provenance. Publish the /// predecessor disposition and current alias in that same activation transaction. pub(super) async fn activate_native_source_transitions( - conn: &impl Executor, + conn: &(impl Executor + super::apply::SendProjectionDerive), generation: &str, ) -> ProjectionStoreResult<()> { let mut after_sequence = 0_i64; diff --git a/crates/tracedecay-global-db/src/observation_projection/state.rs b/crates/tracedecay-global-db/src/observation_projection/state.rs index aa5e107775..114150e5f4 100644 --- a/crates/tracedecay-global-db/src/observation_projection/state.rs +++ b/crates/tracedecay-global-db/src/observation_projection/state.rs @@ -24,7 +24,7 @@ use crate::observation::decode_observation_json; use super::apply::{derive_projection_with_alias, verify_provenance}; -pub(super) fn storage( +pub(crate) fn storage( operation: &'static str, source: impl std::error::Error + Send + Sync + 'static, ) -> ProjectionStoreError { @@ -799,8 +799,8 @@ pub(super) async fn has_other_projector_output_owner( .is_some()) } -async fn message_projection( - conn: &impl QueryExecutor, +pub(super) async fn message_projection( + conn: &impl super::apply::SendProjectionDerive, observation: &DurableObservationV1, provider: &str, message_id: &str, @@ -915,7 +915,7 @@ pub(super) fn same_projection_lineage( } pub(super) async fn verify_output_state( - conn: &impl QueryExecutor, + conn: &impl super::apply::SendProjectionDerive, state: &ProjectionOutputState, projection: &SessionMessageProjection, ) -> ProjectionStoreResult<()> { @@ -1243,7 +1243,7 @@ pub(in super::super) async fn read_output_authorities( skip_all )] pub(in super::super) async fn resolve_output_projection( - conn: &impl QueryExecutor, + conn: &impl super::apply::SendProjectionDerive, authority: &ProjectionOutputAuthority, derived: Option<(&str, &ObservationProjection)>, projection: &SessionMessageProjection, diff --git a/crates/tracedecay-global-db/src/project_registry.rs b/crates/tracedecay-global-db/src/project_registry.rs index 30e472b313..8f38c6c102 100644 --- a/crates/tracedecay-global-db/src/project_registry.rs +++ b/crates/tracedecay-global-db/src/project_registry.rs @@ -1,5 +1,9 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; + +use tracing::Instrument; use std::fmt::Write as _; @@ -653,52 +657,53 @@ pub(super) async fn list_registered_lossless_paths( list_lossless_paths_from(ProjectRegistryDatabase(db), sql, operation).await } -#[tracing::instrument( - name = "global_db.registry.query.list_paths", - level = "trace", - skip_all -)] -async fn list_lossless_paths_from( - db: ProjectRegistryDatabase<'_>, - sql: &str, +fn list_lossless_paths_from<'a>( + db: ProjectRegistryDatabase<'a>, + sql: &'a str, operation: &'static str, -) -> tracedecay_domain::errors::Result> { - let read = db.read_snapshot(operation).await?; - let mut rows = read - .query(sql, ()) - .await - .map_err(|error| global_db_operation_error(operation, error))?; - let mut paths = Vec::new(); - while let Some(row) = rows - .next() - .await - .map_err(|error| global_db_operation_error(operation, error))? - { - let encoded = row - .get::(0) - .map_err(|error| global_db_operation_error(operation, error))?; - let path = match decode_native_project_path_alias(&encoded) { - Ok(Some(path)) => Some(path), - Ok(None) if Path::new(&encoded).is_absolute() => Some(PathBuf::from(encoded)), - Ok(None) => None, - Err(error) => { - return Err(global_db_operation_message( - operation, - format!("invalid native project path alias: {error}"), - )); - } - }; - if let Some(path) = path { - if !path.is_absolute() { - return Err(global_db_operation_message( - operation, - "native project path alias is not absolute", - )); +) -> Pin>> + Send + 'a>> { + let span = tracing::trace_span!("global_db.registry.query.list_paths"); + Box::pin( + async move { + let read = db.read_snapshot(operation).await?; + let mut rows = read + .query(sql, ()) + .await + .map_err(|error| global_db_operation_error(operation, error))?; + let mut paths = Vec::new(); + while let Some(row) = rows + .next() + .await + .map_err(|error| global_db_operation_error(operation, error))? + { + let encoded = row + .get::(0) + .map_err(|error| global_db_operation_error(operation, error))?; + let path = match decode_native_project_path_alias(&encoded) { + Ok(Some(path)) => Some(path), + Ok(None) if Path::new(&encoded).is_absolute() => Some(PathBuf::from(encoded)), + Ok(None) => None, + Err(error) => { + return Err(global_db_operation_message( + operation, + format!("invalid native project path alias: {error}"), + )); + } + }; + if let Some(path) = path { + if !path.is_absolute() { + return Err(global_db_operation_message( + operation, + "native project path alias is not absolute", + )); + } + paths.push(path); + } } - paths.push(path); + Ok(paths) } - } - Ok(paths) + .instrument(span), + ) } impl RegisteredGlobalDb { @@ -742,115 +747,118 @@ impl RegisteredGlobalDb { /// [`TraceDecayError::ProjectRoute`]: tracedecay_domain::errors::TraceDecayError::ProjectRoute /// [`TraceDecayError::ResetRequired`]: tracedecay_domain::errors::TraceDecayError::ResetRequired /// [`TraceDecayError::Database`]: tracedecay_domain::errors::TraceDecayError::Database - #[tracing::instrument(name = "global_db.registry.persist.upsert", level = "trace", skip_all)] - pub async fn upsert_code_project( - &self, - project_id: &str, - project_root: &Path, - git_common_dir: Option<&Path>, - git_remote_url: Option<&str>, - default_branch: Option<&str>, - ) -> tracedecay_domain::errors::Result { - const OPERATION: &str = "upsert code project"; - // The one door through which a project authority is minted. Enforcing - // admission here rather than at each caller means an ephemeral root - // cannot become a durable authority even from a call site that has - // never heard of the policy. - if let Some(reason) = self.ephemeral_root_rejection(project_root) { - return Err(tracedecay_domain::errors::TraceDecayError::project_route( - EPHEMERAL_PROJECT_ROOT_REASON_CODE, - false, - reason, - )); - } - let now = tracedecay_runtime_core::tracedecay::current_timestamp(); - let canonical_project_root = canonical_project_path(project_root); - let canonical_root = canonical_project_root.to_string_lossy().into_owned(); - let current_root_alias = project_path_alias_key(&canonical_project_root); - let canonical_git_common_dir = git_common_dir.map(canonical_project_path); - let git_common_dir_text = canonical_git_common_dir - .as_deref() - .map(|path| path.to_string_lossy().into_owned()); - let git_common_dir_alias = - super::repo_identity_aliases(canonical_git_common_dir.as_deref()) + pub fn upsert_code_project<'a>( + &'a self, + project_id: &'a str, + project_root: &'a Path, + git_common_dir: Option<&'a Path>, + git_remote_url: Option<&'a str>, + default_branch: Option<&'a str>, + ) -> Pin< + Box> + Send + 'a>, + > { + Box::pin( + async move { + const OPERATION: &str = "upsert code project"; + // The one door through which a project authority is minted. Enforcing + // admission here rather than at each caller means an ephemeral root + // cannot become a durable authority even from a call site that has + // never heard of the policy. + if let Some(reason) = self.ephemeral_root_rejection(project_root) { + return Err(tracedecay_domain::errors::TraceDecayError::project_route( + EPHEMERAL_PROJECT_ROOT_REASON_CODE, + false, + reason, + )); + } + let now = tracedecay_runtime_core::tracedecay::current_timestamp(); + let canonical_project_root = canonical_project_path(project_root); + let canonical_root = canonical_project_root.to_string_lossy().into_owned(); + let current_root_alias = project_path_alias_key(&canonical_project_root); + let canonical_git_common_dir = git_common_dir.map(canonical_project_path); + let git_common_dir_text = canonical_git_common_dir + .as_deref() + .map(|path| path.to_string_lossy().into_owned()); + let git_common_dir_alias = + super::repo_identity_aliases(canonical_git_common_dir.as_deref()) + .into_iter() + .next(); + let transaction = self + .begin_write_transaction() + .await + .map_err(|error| global_db_operation_error(OPERATION, error))?; + + let mut existing_authorities = BTreeSet::new(); + for alias in [ + Some(current_root_alias.as_str()), + git_common_dir_alias.as_deref(), + ] .into_iter() - .next(); - let transaction = self - .begin_write_transaction() - .await - .map_err(|error| global_db_operation_error(OPERATION, error))?; - - let mut existing_authorities = BTreeSet::new(); - for alias in [ - Some(current_root_alias.as_str()), - git_common_dir_alias.as_deref(), - ] - .into_iter() - .flatten() - { - let mut rows = transaction - .query( - "SELECT project_id FROM project_aliases WHERE alias_path = ?1", - params![alias], - ) - .await - .map_err(|error| global_db_operation_error(OPERATION, error))?; - if let Some(row) = rows - .next() - .await - .map_err(|error| global_db_operation_error(OPERATION, error))? - { - existing_authorities.insert( - row.get::(0) - .map_err(|error| global_db_operation_error(OPERATION, error))?, - ); - } - } - if let Some(git_common_dir_text) = git_common_dir_text.as_deref() { - let mut rows = transaction - .query( - "SELECT project_id FROM code_projects WHERE git_common_dir = ?1", - params![git_common_dir_text], - ) - .await - .map_err(|error| global_db_operation_error(OPERATION, error))?; - while let Some(row) = rows - .next() - .await - .map_err(|error| global_db_operation_error(OPERATION, error))? - { - existing_authorities.insert( - row.get::(0) - .map_err(|error| global_db_operation_error(OPERATION, error))?, - ); - } - } - if existing_authorities.len() > 1 { - // Two ids already claim this root or its repository. Picking one - // would fabricate a consolidation the caller never asked for, so - // the conflict is surfaced with the ids that produced it. - return Err(tracedecay_domain::errors::TraceDecayError::reset_required( - PROJECT_REGISTRY_AUTHORITY, - format!( - "conflicting project authorities [{}] already claim '{}'", - existing_authorities - .iter() - .map(String::as_str) - .collect::>() - .join(", "), - canonical_project_root.display() - ), - )); - } - let authority_project_id = existing_authorities - .into_iter() - .next() - .unwrap_or_else(|| project_id.to_string()); + .flatten() + { + let mut rows = transaction + .query( + "SELECT project_id FROM project_aliases WHERE alias_path = ?1", + params![alias], + ) + .await + .map_err(|error| global_db_operation_error(OPERATION, error))?; + if let Some(row) = rows + .next() + .await + .map_err(|error| global_db_operation_error(OPERATION, error))? + { + existing_authorities.insert( + row.get::(0) + .map_err(|error| global_db_operation_error(OPERATION, error))?, + ); + } + } + if let Some(git_common_dir_text) = git_common_dir_text.as_deref() { + let mut rows = transaction + .query( + "SELECT project_id FROM code_projects WHERE git_common_dir = ?1", + params![git_common_dir_text], + ) + .await + .map_err(|error| global_db_operation_error(OPERATION, error))?; + while let Some(row) = rows + .next() + .await + .map_err(|error| global_db_operation_error(OPERATION, error))? + { + existing_authorities.insert( + row.get::(0) + .map_err(|error| global_db_operation_error(OPERATION, error))?, + ); + } + } + if existing_authorities.len() > 1 { + // Two ids already claim this root or its repository. Picking one + // would fabricate a consolidation the caller never asked for, so + // the conflict is surfaced with the ids that produced it. + return Err(tracedecay_domain::errors::TraceDecayError::reset_required( + PROJECT_REGISTRY_AUTHORITY, + format!( + "conflicting project authorities [{}] already claim '{}'", + existing_authorities + .iter() + .map(String::as_str) + .collect::>() + .join(", "), + canonical_project_root.display() + ), + )); + } + let authority_project_id = existing_authorities + .into_iter() + .next() + .unwrap_or_else(|| project_id.to_string()); - if authority_project_id == project_id { - transaction - .execute( - "INSERT INTO code_projects + if authority_project_id == project_id { + transaction + .execute( + "INSERT INTO code_projects (project_id, canonical_root, display_root, primary_root_platform, primary_root_bytes, primary_root_last_seen_at, git_common_dir, git_remote_url, default_branch, created_at, last_seen_at) @@ -865,64 +873,75 @@ impl RegisteredGlobalDb { git_remote_url = excluded.git_remote_url, default_branch = excluded.default_branch, last_seen_at = excluded.last_seen_at", - params![ - authority_project_id.as_str(), - canonical_root, - native_project_path_platform(), - encode_native_project_path(&canonical_project_root), - now, - git_common_dir_text, - git_remote_url, - default_branch - ], - ) - .await - .map_err(|error| global_db_operation_error(OPERATION, error))?; - } else { - transaction - .execute( - "UPDATE code_projects SET last_seen_at = ?2 WHERE project_id = ?1", - params![authority_project_id.as_str(), now], - ) - .await - .map_err(|error| global_db_operation_error(OPERATION, error))?; - } - let mut aliases = vec![current_root_alias]; - aliases.extend(git_common_dir_alias); - if let Some(alias) = super::git_remote_search_alias(git_remote_url) { - aliases.push(alias); - } - for alias in aliases { - transaction - .execute( - "INSERT INTO project_aliases (alias_path, project_id, last_seen_at) + params![ + authority_project_id.as_str(), + canonical_root, + native_project_path_platform(), + encode_native_project_path(&canonical_project_root), + now, + git_common_dir_text, + git_remote_url, + default_branch + ], + ) + .await + .map_err(|error| global_db_operation_error(OPERATION, error))?; + } else { + transaction + .execute( + "UPDATE code_projects SET last_seen_at = ?2 WHERE project_id = ?1", + params![authority_project_id.as_str(), now], + ) + .await + .map_err(|error| global_db_operation_error(OPERATION, error))?; + } + let mut aliases = vec![current_root_alias]; + aliases.extend(git_common_dir_alias); + if let Some(alias) = super::git_remote_search_alias(git_remote_url) { + aliases.push(alias); + } + for alias in aliases { + transaction + .execute( + "INSERT INTO project_aliases (alias_path, project_id, last_seen_at) VALUES (?1, ?2, ?3) ON CONFLICT(alias_path) DO UPDATE SET project_id = excluded.project_id, last_seen_at = excluded.last_seen_at", - params![alias, authority_project_id.as_str(), now], - ) - .await - .map_err(|error| global_db_operation_error(OPERATION, error))?; - } - transaction - .commit() - .await - .map_err(|error| global_db_operation_error(OPERATION, error))?; - // A committed authority that cannot be read back is a registry - // inconsistency, not an "unregistered project", say so rather than - // handing the caller an absence it would read as a clean refusal. - self.get_code_project(&authority_project_id) - .await? - .ok_or_else(|| { - global_db_operation_message( - OPERATION, - format!( - "project '{authority_project_id}' is not readable after its \ - registration committed" - ), - ) - }) + params![alias, authority_project_id.as_str(), now], + ) + .await + .map_err(|error| global_db_operation_error(OPERATION, error))?; + } + transaction + .commit() + .await + .map_err(|error| global_db_operation_error(OPERATION, error))?; + // A committed authority that cannot be read back is a registry + // inconsistency, not an "unregistered project", say so rather than + // handing the caller an absence it would read as a clean refusal. + let recorded: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Option, + >, + > + Send + + '_, + >, + > = Box::pin(self.get_code_project(&authority_project_id)); + recorded.await?.ok_or_else(|| { + global_db_operation_message( + OPERATION, + format!( + "project '{authority_project_id}' is not readable after its \ + registration committed" + ), + ) + }) + } + .instrument(tracing::trace_span!("global_db.registry.persist.upsert")), + ) } /// Mints or refreshes a `project_aliases` row. @@ -1570,13 +1589,21 @@ impl RegisteredGlobalDb { project_root: &Path, git_common_dir: Option<&Path>, ) -> tracedecay_domain::errors::Result> { - let Some(project_id) = self - .project_id_by_identity(project_root, git_common_dir) - .await? - else { + let identified: Pin< + Box>> + Send + '_>, + > = Box::pin(self.project_id_by_identity(project_root, git_common_dir)); + let Some(project_id) = identified.await? else { return Ok(None); }; - let Some(context) = self.project_registry_context_by_id(&project_id).await? else { + let context_read: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result>, + > + Send + + '_, + >, + > = Box::pin(self.project_registry_context_by_id(&project_id)); + let Some(context) = context_read.await? else { return Ok(None); }; if context.stores.len() > 1 { @@ -1650,13 +1677,21 @@ impl RegisteredGlobalDb { project_root: &Path, git_common_dir: Option<&Path>, ) -> tracedecay_domain::errors::Result> { - let Some(project_id) = self - .project_id_by_identity(project_root, git_common_dir) - .await? - else { + let identified: Pin< + Box>> + Send + '_>, + > = Box::pin(self.project_id_by_identity(project_root, git_common_dir)); + let Some(project_id) = identified.await? else { return Ok(None); }; - self.project_registry_context_by_id(&project_id).await + let context: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result>, + > + Send + + '_, + >, + > = Box::pin(self.project_registry_context_by_id(&project_id)); + context.await } async fn project_id_by_identity( diff --git a/crates/tracedecay-global-db/src/registered.rs b/crates/tracedecay-global-db/src/registered.rs index b979be8d9c..4ee8c6166c 100644 --- a/crates/tracedecay-global-db/src/registered.rs +++ b/crates/tracedecay-global-db/src/registered.rs @@ -1,5 +1,6 @@ use std::future::Future; use std::path::Path; +use std::pin::Pin; use std::sync::{Arc, OnceLock, RwLock, Weak}; use crate::schema_stages::RegisteredSchemaAttachmentV1; @@ -159,16 +160,23 @@ impl RegisteredGlobalDbOwnerV1 { )> { let temporary = database.issue_lease().map_err(registered_owner_error)?; let registered = RegisteredGlobalDb::from_owned_database(temporary); - let (convergence, refused_authority) = - match super::schema_stages::ensure_attached_registered_schema( - ®istered.database, - cancellation, - ) - .await? - { - RegisteredSchemaAttachmentV1::Admitted(convergence) => (Some(convergence), None), - RegisteredSchemaAttachmentV1::SessionsRefused(refused) => (None, Some(refused)), - }; + let attached: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + super::schema_stages::RegisteredSchemaAttachmentV1, + >, + > + Send + + '_, + >, + > = Box::pin(super::schema_stages::ensure_attached_registered_schema( + ®istered.database, + cancellation, + )); + let (convergence, refused_authority) = match attached.await? { + RegisteredSchemaAttachmentV1::Admitted(convergence) => (Some(convergence), None), + RegisteredSchemaAttachmentV1::SessionsRefused(refused) => (None, Some(refused)), + }; drop(registered); Ok(( Self { @@ -352,6 +360,25 @@ pub struct RegisteredGlobalDb { refused_authority: Option, } +// Futures that capture this client were proving the database lease and the +// graph binding in one walk, past the recursion limit (rust-lang/rust#159228). +unsafe impl Send for RegisteredGlobalDb {} +unsafe impl Sync for RegisteredGlobalDb {} + +const _: fn(&RegisteredGlobalDb) = |value| { + fn assert_send_sync(_: &T) {} + let RegisteredGlobalDb { + database, + project_graph, + session_relation_graph, + refused_authority, + } = value; + assert_send_sync(database); + assert_send_sync(project_graph); + assert_send_sync(session_relation_graph); + assert_send_sync(refused_authority); +}; + impl RegisteredGlobalDb { #[tracing::instrument(name = "global_db.registered.schema", level = "trace", skip_all)] pub async fn converge_schema( diff --git a/crates/tracedecay-global-db/src/registered_analytics.rs b/crates/tracedecay-global-db/src/registered_analytics.rs index 0508952a1b..dcd9177f15 100644 --- a/crates/tracedecay-global-db/src/registered_analytics.rs +++ b/crates/tracedecay-global-db/src/registered_analytics.rs @@ -1,5 +1,7 @@ use std::collections::BTreeMap; use std::fmt::Write as _; +use std::future::Future; +use std::pin::Pin; use tracedecay_runtime_core::db::engine::{Value, opt_text}; @@ -254,24 +256,42 @@ impl RegisteredGlobalDb { project_id: &str, idempotency_key: &str, ) -> Result, String> { - let snapshot = self - .read_snapshot() + let opened: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + tracedecay_runtime_core::db::DatabaseEngineReadSnapshot, + >, + > + Send + + '_, + >, + > = Box::pin(self.read_snapshot()); + let snapshot = opened .await .map_err(|error| format!("failed to begin observability event snapshot: {error}"))?; - let mut rows = snapshot - .query( - "SELECT id, provider, project_id, session_id, timestamp, event_kind, + let queried: Pin< + Box< + dyn Future< + Output = tracedecay_runtime_core::db::engine::Result< + tracedecay_runtime_core::db::engine::Rows, + >, + > + Send + + '_, + >, + > = Box::pin(snapshot.query( + "SELECT id, provider, project_id, session_id, timestamp, event_kind, hook_name, tool_name, tool_category, skill_name, hint_category, hint_id, outcome, metadata_json FROM analytics_events WHERE provider = ?1 AND project_id = ?2 AND hint_id = ?3 LIMIT 1", - tracedecay_runtime_core::db::engine::params![ - "tracedecay-observability", - project_id, - idempotency_key, - ], - ) + tracedecay_runtime_core::db::engine::params![ + "tracedecay-observability", + project_id, + idempotency_key, + ], + )); + let mut rows = queried .await .map_err(|error| format!("failed to read observability event: {error}"))?; rows.next() diff --git a/crates/tracedecay-global-db/src/registered_lcm.rs b/crates/tracedecay-global-db/src/registered_lcm.rs index aad93b5281..2f532df629 100644 --- a/crates/tracedecay-global-db/src/registered_lcm.rs +++ b/crates/tracedecay-global-db/src/registered_lcm.rs @@ -1,5 +1,9 @@ +use std::future::Future; use std::path::Path; +use std::pin::Pin; +use super::RegisteredGlobalDb; +use tracedecay_domain::ObservationScopeV1; use tracedecay_domain::SessionId; use tracedecay_lcm::{ LcmCompressionRequest, LcmCompressionResponse, LcmDescribeRequest, LcmDescribeResponse, @@ -11,15 +15,94 @@ use tracedecay_lcm::{ payload, types::{LcmImmutableSummaryPublication, LcmSummaryPublicationReceipt}, }; -use tracedecay_sessions::runtime::{SessionMessageRecord, SessionStoreAccess}; -use tracedecay_temporal_query::execution::ExecutionControl; -use tracedecay_temporal_query::execution::TemporalPortError; - -use super::RegisteredGlobalDb; use tracedecay_session_temporal_store::SessionTemporalAccess; use tracedecay_session_temporal_store::operations as session_temporal_operations; use tracedecay_session_temporal_store::seed_session_relation_projection; use tracedecay_session_temporal_store::store::execution_control_graph_cancellation; +use tracedecay_sessions::runtime::{SessionMessageRecord, SessionStoreAccess}; +use tracedecay_temporal_query::execution::ExecutionControl; +use tracedecay_temporal_query::execution::TemporalPortError; + +fn drive_lcm_compression<'a>( + transaction: &'a crate::RegisteredGlobalDbWriteTransaction<'a>, + scope: &'a ObservationScopeV1, + publisher: &'a session_temporal_operations::GlobalDbLcmSummaryPublication< + 'a, + crate::RegisteredGlobalDbWriteTransaction<'a>, + >, + storage_root: &'a Path, + request: &'a LcmCompressionRequest, + payload_rollback: &'a mut payload::PayloadFileRollback, + retained: Option, +) -> Pin< + Box< + dyn Future< + Output = Result< + tracedecay_lcm::summary_convergence::LcmBoundedCompressionResponse, + LcmError, + >, + > + Send + + 'a, + >, +> { + Box::pin(async move { + let begun: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(compression::begin_compression_pass( + transaction, + scope, + storage_root, + request, + payload_rollback, + retained, + )); + let classified = match begun.await? { + compression::CompressionAdmission::Finished(response) => return Ok(response), + compression::CompressionAdmission::Classify(publish) => { + let classified: Pin< + Box< + dyn Future< + Output = Result, + > + Send + + '_, + >, + > = Box::pin(compression::classify_compression_pass( + transaction, + publisher, + request, + publish, + )); + classified.await? + } + }; + match classified { + compression::CompressionClassification::Finished(response) => Ok(response), + compression::CompressionClassification::Publish(publish) => { + let published: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_lcm::summary_convergence::LcmBoundedCompressionResponse, + LcmError, + >, + > + Send + + '_, + >, + > = Box::pin(compression::publish_compression_pass( + transaction, + publisher, + request, + publish, + )); + published.await + } + } + }) +} fn check_execution(control: &ExecutionControl) -> Result<(), LcmError> { control.checkpoint().map_err(|error| match error { @@ -129,9 +212,10 @@ impl RegisteredGlobalDb { deep: bool, gc_config: &LcmGcConfig, ) -> Result { - SessionStoreAccess::new(self) - .lcm_status_with_options(provider, session_id, deep, gc_config) - .await + let access = SessionStoreAccess::new(self); + let status: Pin> + Send + '_>> = + Box::pin(access.lcm_status_with_options(provider, session_id, deep, gc_config)); + status.await } /// Publishes one immutable summary and advances its native relation @@ -251,15 +335,19 @@ impl RegisteredGlobalDb { &transaction, relation_projection, ); - let mut response = compression::compress( - &transaction, - &scope, - &publisher, - storage_root, - request, - &mut payload_rollback, - ) - .await?; + let compressed = tracing::Instrument::instrument( + drive_lcm_compression( + &transaction, + &scope, + &publisher, + storage_root, + request, + &mut payload_rollback, + None, + ), + tracing::trace_span!("sessions.lcm.compress"), + ); + let mut response = compressed.await?.response; check_execution(control)?; before_commit()?; transaction.commit().await?; @@ -303,27 +391,47 @@ impl RegisteredGlobalDb { request.session_id )) })?; - let transaction = self - .begin_write_transaction() + let begun: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + crate::RegisteredGlobalDbWriteTransaction<'_>, + >, + > + Send + + '_, + >, + > = Box::pin(self.begin_write_transaction()); + let transaction = begun .await .map_err(|error| LcmError::Db(error.to_string()))?; let mut payload_rollback = payload::PayloadFileRollback::begin_cancellation_safe(storage_root); if let Some(candidate) = convergence_candidate { - tracedecay_lcm::summary_convergence::require_candidate_revision( - &transaction, - candidate, - ) - .await?; + let revision: Pin> + Send + '_>> = + Box::pin( + tracedecay_lcm::summary_convergence::require_candidate_revision( + &transaction, + candidate, + ), + ); + revision.await?; } - let relation_projection = seed_session_relation_projection( + let seeded: Pin< + Box< + dyn Future< + Output = tracedecay_store::SessionStoreResult< + tracedecay_session_temporal_store::relations::SessionRelationProjection, + >, + > + Send + + '_, + >, + > = Box::pin(seed_session_relation_projection( self, &transaction, &session_id, execution_control_graph_cancellation(control), - ) - .await - .map_err(|error| { + )); + let relation_projection = seeded.await.map_err(|error| { LcmError::Db(format!( "seed native retained LCM relation projection: {error}" )) @@ -333,16 +441,19 @@ impl RegisteredGlobalDb { &transaction, relation_projection, ); - let bounded = compression::compress_retained_page( - &transaction, - &scope, - &publisher, - storage_root, - request, - &mut payload_rollback, - guard, - ) - .await?; + let compressed = tracing::Instrument::instrument( + drive_lcm_compression( + &transaction, + &scope, + &publisher, + storage_root, + request, + &mut payload_rollback, + Some(guard), + ), + tracing::trace_span!("sessions.lcm.compress"), + ); + let bounded = compressed.await?; if bounded.response.status != "needs_summary" && let Some(candidate) = convergence_candidate { @@ -351,21 +462,25 @@ impl RegisteredGlobalDb { } else { tracedecay_lcm::summary_convergence::LcmSummaryConvergenceQueueState::Current }; - let settled = tracedecay_lcm::summary_convergence::record_outcome( - &transaction, - candidate, - state, - None, - 0, - 0, - ) - .await?; - if !settled { - tracedecay_lcm::summary_convergence::require_candidate_revision( + let settled_outcome: Pin> + Send + '_>> = + Box::pin(tracedecay_lcm::summary_convergence::record_outcome( &transaction, candidate, - ) - .await?; + state, + None, + 0, + 0, + )); + let settled = settled_outcome.await?; + if !settled { + let revision: Pin> + Send + '_>> = + Box::pin( + tracedecay_lcm::summary_convergence::require_candidate_revision( + &transaction, + candidate, + ), + ); + revision.await?; return Err(LcmError::Db( "retained convergence queue settlement affected no row".to_string(), )); @@ -373,7 +488,10 @@ impl RegisteredGlobalDb { } check_execution(control)?; before_commit()?; - transaction.commit().await?; + let committed: Pin< + Box> + Send + '_>, + > = Box::pin(transaction.commit()); + committed.await?; payload_rollback.disarm(); // Retained convergence publishes the durable relation effect journal // atomically with the summary, lifecycle frontier, and queue outcome. diff --git a/crates/tracedecay-global-db/src/registered_source_rewrite.rs b/crates/tracedecay-global-db/src/registered_source_rewrite.rs index c9316384e8..69db111bde 100644 --- a/crates/tracedecay-global-db/src/registered_source_rewrite.rs +++ b/crates/tracedecay-global-db/src/registered_source_rewrite.rs @@ -5,6 +5,9 @@ //! a new generation; once that generation has been read to the end of the //! file, every record it did not offer again is retired from projection. +use std::future::Future; +use std::pin::Pin; + use tracedecay_domain::{ CanonicalObservationIdV1, ObservationScopeV1, ObservationSourceGenerationV1, ObservationSourceIdentityV1, @@ -255,7 +258,10 @@ impl RegisteredGlobalDb { let page_len = page.len(); after_observation_id = page[page_len - 1].clone(); for observation_id in &page { - if retire_source_record(&transaction, observation_id).await? { + let retired_one: Pin< + Box> + Send + '_>, + > = Box::pin(retire_source_record(&transaction, observation_id)); + if retired_one.await? { retired += 1; } } diff --git a/crates/tracedecay-global-db/src/schema_contract/invariants.rs b/crates/tracedecay-global-db/src/schema_contract/invariants.rs index 01fc77374d..8cc22406d5 100644 --- a/crates/tracedecay-global-db/src/schema_contract/invariants.rs +++ b/crates/tracedecay-global-db/src/schema_contract/invariants.rs @@ -1,3 +1,11 @@ +use std::future::Future; +use std::pin::Pin; + +use futures_util::future::BoxFuture; + +/// Checkpoint, provenance, disposition, and alias counts, then completion. +type ProjectionPage = (AuditCheckpoint, i64, i64, i64, bool); + use tracedecay_store::SESSION_MESSAGE_PROJECTOR_VERSION; use super::super::global_db_operation_error; @@ -440,13 +448,23 @@ async fn converge_authority_invariants( }; authority_invariant_step(provider, "repair committed source cursors", async |conn| { - repair_committed_source_cursors(conn, audited_from_sequence).await + let repaired: Pin> + Send>> = + Box::pin(repair_committed_source_cursors(conn, audited_from_sequence)); + repaired.await }) .await?; authority_invariant_step( provider, "validate observation cursor coverage", - async |conn| validate_observation_cursor_coverage(conn, audited_from_sequence).await, + async |conn| { + let covered: Pin< + Box> + Send>, + > = Box::pin(validate_observation_cursor_coverage( + conn, + audited_from_sequence, + )); + covered.await + }, ) .await?; @@ -454,11 +472,24 @@ async fn converge_authority_invariants( provider, "repair observation projection frontier", async |conn| { - repair_projection_frontier(conn, checkpoint.projection_checkpoint).await?; + let repaired: Pin< + Box> + Send>, + > = Box::pin(repair_projection_frontier( + conn, + checkpoint.projection_checkpoint, + )); + repaired.await?; if exhaustive { - validate_invariant_rows(&ConvergenceReads(conn)).await + let reads = ConvergenceReads(conn); + let rows: Pin< + Box> + Send>, + > = Box::pin(validate_invariant_rows(&reads)); + rows.await } else { - validate_mutable_invariant_rows(conn).await + let rows: Pin< + Box> + Send>, + > = Box::pin(validate_mutable_invariant_rows(conn)); + rows.await } }, ) @@ -482,8 +513,13 @@ async fn converge_authority_invariants( "audit observation projection authority page", async |conn| { let released = ReleasedRenderingLedger::default(); - let result = - validate_projection_authority_chunk(conn, progress, &released).await?; + let projected: BoxFuture< + '_, + tracedecay_domain::errors::Result, + > = Box::pin(validate_projection_authority_chunk( + conn, progress, &released, + )); + let result = projected.await?; released.converge(conn).await?; write_audit_checkpoint( conn, @@ -514,8 +550,15 @@ async fn converge_authority_invariants( "audit observation projection authority suffix", async |conn| { let released = ReleasedRenderingLedger::default(); - let audited = - validate_projection_authority_suffix(conn, projection_start, &released).await?; + let projected: BoxFuture< + '_, + tracedecay_domain::errors::Result<(AuditCheckpoint, i64, i64, i64)>, + > = Box::pin(validate_projection_authority_suffix( + conn, + projection_start, + &released, + )); + let audited = projected.await?; released.converge(conn).await?; Ok(audited) }, @@ -783,7 +826,7 @@ async fn foreign_key_violation_exists_read_only( } pub async fn validate_authority_rows_exhaustive( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, ) -> tracedecay_domain::errors::Result<()> { validate_receipt_authority_rows(conn, 0).await?; validate_observation_authority_rows(conn, 0).await?; diff --git a/crates/tracedecay-global-db/src/schema_contract/invariants/audit.rs b/crates/tracedecay-global-db/src/schema_contract/invariants/audit.rs index 024b28d104..51c336d4d3 100644 --- a/crates/tracedecay-global-db/src/schema_contract/invariants/audit.rs +++ b/crates/tracedecay-global-db/src/schema_contract/invariants/audit.rs @@ -658,7 +658,7 @@ fn validate_alias_binding( } async fn validate_message_projection_row( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, observation_id: &str, effect: &ObservationProjection, resolved: &ResolvedOutputAuthority<'_>, @@ -921,7 +921,7 @@ fn owned_storage_columns_need_rewrite( #[allow(clippy::too_many_arguments)] async fn validate_message_projection( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, observation_id: &str, state: ProjectionAuthorityState, unaliased: &ObservationProjection, @@ -988,7 +988,7 @@ fn validate_skipped_projection_row( #[allow(clippy::too_many_arguments)] async fn validate_composite_projection( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, observation_id: &str, state: ProjectionAuthorityState, unaliased: &ObservationProjection, @@ -1071,7 +1071,7 @@ async fn validate_composite_projection( /// durable output-collision skip re-derives as `Skipped(OutputCollision)` and /// audits through the `Skipped` arm natively. async fn derive_projection_effect( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, observation: &DurableObservationV1, ) -> tracedecay_domain::errors::Result { crate::observation_projection::derive_projection_with_alias(conn, observation) @@ -1103,7 +1103,7 @@ fn requested_outputs(effects: &[ObservationProjection]) -> BTreeSet<(String, Str skip_all )] async fn derive_page_effects( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, observations: &[&DurableObservationV1], ) -> tracedecay_domain::errors::Result> { let mut effects = Vec::with_capacity(observations.len()); @@ -1170,7 +1170,7 @@ async fn resolve_output_authority<'a>( /// per-row invariant is unchanged; only the number of round trips spent /// re-reading the same stored authority is. async fn validate_projection_effects( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, observations: &[&DurableObservationV1], released: &ReleasedRenderingLedger, ) -> tracedecay_domain::errors::Result<()> { @@ -1190,7 +1190,7 @@ async fn validate_projection_effects( } async fn validate_projection_effect( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, observation: &DurableObservationV1, effect: &ObservationProjection, resolved: &ResolvedOutputAuthority<'_>, @@ -1351,7 +1351,7 @@ fn historical_projection_delta_required(checkpoint: AuditCheckpoint) -> bool { } async fn validate_projection_authority_suffix_pages( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, mut checkpoint: AuditCheckpoint, page_limit: Option, released: &ReleasedRenderingLedger, @@ -1633,7 +1633,7 @@ async fn validate_projection_authority_suffix_pages( } pub(super) async fn validate_projection_authority_suffix( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, checkpoint: AuditCheckpoint, released: &ReleasedRenderingLedger, ) -> tracedecay_domain::errors::Result<(AuditCheckpoint, i64, i64, i64)> { @@ -1643,7 +1643,7 @@ pub(super) async fn validate_projection_authority_suffix( } pub(super) async fn validate_projection_authority_chunk( - conn: &impl QueryExecutor, + conn: &impl crate::observation_projection::SendProjectionDerive, checkpoint: AuditCheckpoint, released: &ReleasedRenderingLedger, ) -> tracedecay_domain::errors::Result<(AuditCheckpoint, i64, i64, i64, bool)> { @@ -1778,6 +1778,8 @@ mod tests { } } + crate::observation_projection::impl_send_projection_derive!(CountingQuery<'_>); + #[asupersync::test] async fn audit_checkpoint_schema_tracks_source_cursor_progress() { let directory = TempDir::new().unwrap(); @@ -2426,6 +2428,10 @@ mod tests { } } + crate::observation_projection::impl_send_projection_derive!( + CountingSnapshot<'_, tracedecay_runtime_core::db::DatabaseEngineReadSnapshot> + ); + const CURSOR_COLLISION_MESSAGE_ID: &str = "fixture-session:0:generation:5552477573209801791"; async fn seed_projected_cursor_message( diff --git a/crates/tracedecay-global-db/src/schema_contract/validation.rs b/crates/tracedecay-global-db/src/schema_contract/validation.rs index 16031fba81..c873204bbb 100644 --- a/crates/tracedecay-global-db/src/schema_contract/validation.rs +++ b/crates/tracedecay-global-db/src/schema_contract/validation.rs @@ -1,7 +1,11 @@ use std::collections::BTreeMap; +use std::future::Future; +use std::pin::Pin; use std::sync::LazyLock; use tracedecay_runtime_core::db::engine::{QueryExecutor, params}; +use tracedecay_runtime_core::db::{DatabaseEngineReadConnection, DatabaseEngineReadSnapshot}; +use tracedecay_runtime_core::ports::registered_schema::RegisteredSchemaInstallationV1; use super::super::{global_db_operation_error, global_db_operation_message}; use super::definitions::{ @@ -613,17 +617,58 @@ pub async fn validate_remote_deletion_schema_contract( /// /// Transcript, LCM, git-correlation, and workflow-index tables are independently owned by their /// schema modules; this validator intentionally neither claims nor validates those domains. -pub async fn validate_authority_schema_contract( - conn: &impl QueryExecutor, -) -> tracedecay_domain::errors::Result<()> { - validate_tables_and_indexes(conn, TABLES).await?; - for invariant in super::invariants::INVARIANTS { - for trigger in invariant.triggers { - validate_trigger(conn, trigger).await?; +/// +/// Each concrete connection implements this instead of one generic async function: nesting +/// `validate_tables_and_indexes` under the trigger walk overflows the next solver's `Send` +/// proof (rust-lang/rust#159228). The returned future only awaits already-erased children. +pub(crate) trait AuthoritySchemaContract: Sync { + fn validate_authority_schema<'a>( + &'a self, + ) -> Pin> + Send + 'a>>; +} + +macro_rules! impl_authority_schema_contract { + ($ty:ty) => { + impl AuthoritySchemaContract for $ty { + fn validate_authority_schema<'a>( + &'a self, + ) -> Pin> + Send + 'a>> + { + Box::pin(async move { + let tables: Pin< + Box> + Send + '_>, + > = Box::pin(validate_tables_and_indexes(self, TABLES)); + tables.await?; + for invariant in super::invariants::INVARIANTS { + for trigger in invariant.triggers { + let checked: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(validate_trigger(self, trigger)); + checked.await?; + } + } + let autoincrement: Pin< + Box> + Send + '_>, + > = Box::pin(validate_observation_autoincrement(self)); + autoincrement.await + }) + } } - } - validate_observation_autoincrement(conn).await?; - Ok(()) + }; +} + +impl_authority_schema_contract!(DatabaseEngineReadConnection); +impl_authority_schema_contract!(DatabaseEngineReadSnapshot); +impl_authority_schema_contract!(RegisteredSchemaInstallationV1); + +pub(crate) fn validate_authority_schema_contract<'a>( + conn: &'a impl AuthoritySchemaContract, +) -> Pin> + Send + 'a>> { + conn.validate_authority_schema() } #[cfg(test)] diff --git a/crates/tracedecay-global-db/src/schema_stages.rs b/crates/tracedecay-global-db/src/schema_stages.rs index 150393815e..f7f02379e8 100644 --- a/crates/tracedecay-global-db/src/schema_stages.rs +++ b/crates/tracedecay-global-db/src/schema_stages.rs @@ -1,3 +1,5 @@ +use std::future::Future; +use std::pin::Pin; use std::time::{Duration, Instant}; use tracedecay_domain::errors::ProjectOpenFailureKind; @@ -18,7 +20,7 @@ use crate::registered::RefusedAuthorityV1; use tracedecay_runtime_core::{ cancellation::CancellationToken, db::{ - Database, DatabaseWriteTransaction, + Database, DatabaseEngineReadConnection, DatabaseWriteTransaction, engine::{self, Executor, IntoParams, QueryExecutor, Rows}, }, ports::registered_schema::{ @@ -438,99 +440,207 @@ enum RegisteredSchemaAdmission { /// authority surfaces its own typed reset state; nothing here mutates the /// store. /// -/// The classification and installation phases below each run behind a heap -/// boundary so the admission futures that await them embed only a pointer: -/// every authority's admission check descends through its own contract -/// validation, and with instrumentation each measured `async fn` on the -/// path adds three more wrapper layers, so nesting the phase state machines -/// inside the attach chain overflowed rustc's layout query depth limit. -#[tracing::instrument(name = "global_db.schema.query.classify", level = "trace", skip_all)] -async fn classify_registered_schema_admission( - connection: &(impl QueryExecutor + Sync), - scope: &StoreShardScopeV1, -) -> tracedecay_domain::errors::Result { - Box::pin(classify_registered_schema_authorities(connection, scope)).await +/// One generic `async fn` nests every authority check and overflows the next +/// solver's `Send` proof (rust-lang/rust#159228). Each concrete connection +/// returns an already-erased future whose body awaits erased children. +trait RegisteredSchemaAdmissionClassify: Sync { + fn classify_registered_schema<'a>( + &'a self, + scope: &'a StoreShardScopeV1, + ) -> Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >; } -async fn classify_registered_schema_authorities( - connection: &(impl QueryExecutor + Sync), - scope: &StoreShardScopeV1, -) -> tracedecay_domain::errors::Result { - let hosts_session_features = hosts_session_features(scope); - // The LCM authority classifies profile content first. Whenever a later - // authority also fails, the earliest session refusal is the store's hard - // verdict: a legacy or version-skewed session store surfaces its own - // reset identity instead of being masked by the coarser configuration - // schema resets, which would also flag a store those features were simply - // never installed in. - let refused_lcm = if hosts_session_features { - lcm_schema_refusal(connection).await? - } else { - None - }; - let surface = |refused: Option| { - move |error| refused.map_or(error, RefusedAuthorityV1::error) - }; - let configuration_fresh = configuration::fresh_configuration_store_evidence(connection) - .await - .map_err(configuration_schema_error( - "inspect configuration schema freshness", - )) - .map_err(surface(refused_lcm))?; - let temporal_admission = session_temporal_schema::require_admissible_session_temporal_schema( - connection, - configuration_fresh.as_ref(), - ) - .await - .map_err(surface(refused_lcm))?; - let refused = refused_lcm.or(temporal_admission.err()); - let session_features = if hosts_session_features || configuration_fresh.is_some() { - inspect_workflow_schema_for_admission(connection) - .await - .map_err(surface(refused))? - .map(|workflow| SessionFeatureSchema::Hosted { workflow }) - } else { - Ok(SessionFeatureSchema::NotHosted) - }; - let refused = refused.or(session_features.err()); - let refused_git_correlation = if hosts_session_features { - git_correlation_schema_refusal(connection) - .await - .map_err(surface(refused))? - } else { - None - }; - let refused = refused.or(refused_git_correlation); - configuration::admit_configuration_schema(connection, configuration_fresh.as_ref()) - .await - .map_err(configuration_schema_error("admit configuration schema")) - .map_err(surface(refused))?; - // An existing catalog whose remote-deletion tombstone table drifted from the - // contract cannot be trusted to gate replay or admission, so admission fails - // closed with the tip's typed reset authority rather than silently - // continuing on a shape that no longer proves deletion state. - if configuration_fresh.is_none() - && let Err(error) = validate_remote_deletion_schema_contract(connection).await - { - return Err(surface(refused)( - tracedecay_domain::errors::TraceDecayError::reset_required( - "remote deletion tombstones", - error.to_string(), - ), - )); - } - Ok(match (refused, temporal_admission, session_features) { - (None, Ok(temporal_admission), Ok(session_features)) => { - RegisteredSchemaAdmission::Admissible(RegisteredSchemaAdmissionClassification { - configuration_fresh, - temporal_admission, - session_features, - }) - } - (Some(refused), _, _) | (None, Err(refused), _) | (None, _, Err(refused)) => { - RegisteredSchemaAdmission::SessionAuthorityRefused(refused) +macro_rules! impl_registered_schema_admission_classify { + ($ty:ty) => { + impl RegisteredSchemaAdmissionClassify for $ty { + fn classify_registered_schema<'a>( + &'a self, + scope: &'a StoreShardScopeV1, + ) -> Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result, + > + Send + + 'a, + >, + > { + Box::pin(async move { + let hosts_session_features = hosts_session_features(scope); + // The LCM authority classifies profile content first. Whenever a later + // authority also fails, the earliest session refusal is the store's hard + // verdict: a legacy or version-skewed session store surfaces its own + // reset identity instead of being masked by the coarser configuration + // schema resets, which would also flag a store those features were simply + // never installed in. + let refused_lcm = if hosts_session_features { + let refused: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Option, + >, + > + Send + + '_, + >, + > = Box::pin(lcm_schema_refusal(self)); + refused.await? + } else { + None + }; + let surface = |refused: Option| { + move |error| refused.map_or(error, RefusedAuthorityV1::error) + }; + let configuration_fresh: Pin< + Box< + dyn Future< + Output = Result< + Option, + configuration::ConfigurationSchemaError, + >, + > + Send + + '_, + >, + > = Box::pin(configuration::fresh_configuration_store_evidence(self)); + let configuration_fresh = configuration_fresh + .await + .map_err(configuration_schema_error( + "inspect configuration schema freshness", + )) + .map_err(surface(refused_lcm))?; + let temporal_admission: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Result< + session_temporal_schema::SessionTemporalSchemaAdmission, + RefusedAuthorityV1, + >, + >, + > + Send + + '_, + >, + > = Box::pin( + session_temporal_schema::require_admissible_session_temporal_schema( + self, + configuration_fresh.as_ref(), + ), + ); + let temporal_admission = + temporal_admission.await.map_err(surface(refused_lcm))?; + let refused = refused_lcm.or(temporal_admission.err()); + let session_features = + if hosts_session_features || configuration_fresh.is_some() { + let workflow: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Result, + >, + > + Send + + '_, + >, + > = Box::pin(inspect_workflow_schema_for_admission(self)); + workflow + .await + .map_err(surface(refused))? + .map(|workflow| SessionFeatureSchema::Hosted { workflow }) + } else { + Ok(SessionFeatureSchema::NotHosted) + }; + let refused = refused.or(session_features.err()); + let refused_git_correlation = if hosts_session_features { + let refused_git: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Option, + >, + > + Send + + '_, + >, + > = Box::pin(git_correlation_schema_refusal(self)); + refused_git.await.map_err(surface(refused))? + } else { + None + }; + let refused = refused.or(refused_git_correlation); + let admitted: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(configuration::admit_configuration_schema( + self, + configuration_fresh.as_ref(), + )); + admitted + .await + .map_err(configuration_schema_error("admit configuration schema")) + .map_err(surface(refused))?; + // An existing catalog whose remote-deletion tombstone table drifted from the + // contract cannot be trusted to gate replay or admission, so admission fails + // closed with the tip's typed reset authority rather than silently + // continuing on a shape that no longer proves deletion state. + if configuration_fresh.is_none() { + let remote: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(validate_remote_deletion_schema_contract(self)); + if let Err(error) = remote.await { + return Err(surface(refused)( + tracedecay_domain::errors::TraceDecayError::reset_required( + "remote deletion tombstones", + error.to_string(), + ), + )); + } + } + Ok(match (refused, temporal_admission, session_features) { + (None, Ok(temporal_admission), Ok(session_features)) => { + RegisteredSchemaAdmission::Admissible( + RegisteredSchemaAdmissionClassification { + configuration_fresh, + temporal_admission, + session_features, + }, + ) + } + (Some(refused), _, _) + | (None, Err(refused), _) + | (None, _, Err(refused)) => { + RegisteredSchemaAdmission::SessionAuthorityRefused(refused) + } + }) + }) + } } - }) + }; +} + +impl_registered_schema_admission_classify!(RegisteredSchemaInstallationV1); +impl_registered_schema_admission_classify!(DatabaseEngineReadConnection); + +fn classify_registered_schema_admission<'a>( + connection: &'a impl RegisteredSchemaAdmissionClassify, + scope: &'a StoreShardScopeV1, +) -> Pin< + Box< + dyn Future> + + Send + + 'a, + >, +> { + connection.classify_registered_schema(scope) } fn configuration_schema_error( @@ -597,7 +707,7 @@ async fn git_correlation_schema_refusal( const AUTHORITY_SCHEMA_AUTHORITY: &str = "authority schema"; async fn validate_admitted_authority_schema( - conn: &impl QueryExecutor, + conn: &RegisteredSchemaInstallationV1, is_fresh: bool, ) -> tracedecay_domain::errors::Result<()> { validate_authority_schema_contract(conn) @@ -638,14 +748,29 @@ pub async fn ensure_registered_schema_for_admission( } }; let is_fresh = configuration_fresh.is_some(); - let force_exhaustive = !authority_invariant_triggers_intact(installation).await?; + let triggers_intact: Pin< + Box> + Send + '_>, + > = Box::pin(authority_invariant_triggers_intact(installation)); + let force_exhaustive = !triggers_intact.await?; schema_install_checkpoint(installation.cancellation(), OPERATION)?; - let transaction = installation - .begin_atomic_schema_transaction() + let begun: Pin< + Box< + dyn Future>> + + Send + + '_, + >, + > = Box::pin(installation.begin_atomic_schema_transaction()); + let transaction = begun .await .map_err(|error| global_db_operation_error(OPERATION, error))?; - if let Some(refused) = install_and_commit_registered_schema( + let committed: Pin< + Box< + dyn Future>> + + Send + + '_, + >, + > = Box::pin(install_and_commit_registered_schema( CancellableSchemaTransaction { transaction, cancellation: installation.cancellation(), @@ -656,32 +781,33 @@ pub async fn ensure_registered_schema_for_admission( force_exhaustive, "commit registered global schema", "roll back registered global schema", - ) - .await? - { + )); + if let Some(refused) = committed.await? { return Err(refused.error()); } - observation_projection::ensure_observation_projection_performance_indexes(installation) - .await - .map_err(|error| { - global_db_operation_error("initialize observation projection indexes", error) - })?; + let indexes: Pin> + Send + '_>> = Box::pin( + observation_projection::ensure_observation_projection_performance_indexes(installation), + ); + indexes.await.map_err(|error| { + global_db_operation_error("initialize observation projection indexes", error) + })?; // Fresh installations publish the final LCM index shape. Keep each build // independently durable outside the shared schema transaction so a // real-scale index build gets the long lease without holding every other // installation stage open. if session_features != SessionFeatureSchema::NotHosted { for sql in tracedecay_lcm::schema::LCM_STATUS_PERFORMANCE_INDEX_SQL { - installation - .execute_authority_revalidated_batch(sql) - .await - .map_err(|error| { - global_db_operation_error("initialize LCM status performance indexes", error) - })?; + let built: Pin> + Send + '_>> = + Box::pin(installation.execute_authority_revalidated_batch(sql)); + built.await.map_err(|error| { + global_db_operation_error("initialize LCM status performance indexes", error) + })?; } } - validate_admitted_authority_schema(installation, is_fresh).await?; + let admitted: Pin> + Send + '_>> = + Box::pin(validate_admitted_authority_schema(installation, is_fresh)); + admitted.await?; Ok(RegisteredSchemaConvergence { force_exhaustive, is_fresh, @@ -693,7 +819,7 @@ pub async fn ensure_registered_schema_for_admission( /// the caller's admission transaction. Callers classify admission first, so /// this stage runs only for stores classified fresh or exactly current. /// -/// Heap boundary: see [`classify_registered_schema_admission`]. +/// Heap boundary: see [`RegisteredSchemaAdmissionClassify`]. #[tracing::instrument(name = "global_db.schema.persist.install", level = "trace", skip_all)] async fn install_registered_schema_stages( transaction: &(impl Executor + Sync), @@ -1075,14 +1201,18 @@ pub async fn converge_registered_schema( database: &Database, convergence: RegisteredSchemaConvergence, ) -> tracedecay_domain::errors::Result<()> { - observation::converge_canonical_bodies(database).await?; + let bodies: Pin> + Send + '_>> = + Box::pin(observation::converge_canonical_bodies(database)); + bodies.await?; if convergence.lcm_status_performance_indexes { - converge_migration_batches( + let batches: Pin< + Box> + Send + '_>, + > = Box::pin(converge_migration_batches( database, "converge LCM status performance indexes", tracedecay_lcm::schema::LCM_STATUS_PERFORMANCE_INDEX_SQL, - ) - .await?; + )); + batches.await?; } // The invariant pass pages historical authority rows and can legitimately // outlive an ordinary open on a large store. The admission phase has @@ -1090,7 +1220,10 @@ pub async fn converge_registered_schema( // guarded writes may proceed while these idempotent repairs advance. // Completed repairs survive interruption, while the trusted checkpoint is // still written only after every audit succeeds. - converge_registered_schema_on(database, convergence).await + let invariants: Pin< + Box> + Send + '_>, + > = Box::pin(converge_registered_schema_on(database, convergence)); + invariants.await } /// Runs one store-sized migration as a sequence of independently durable @@ -1123,15 +1256,29 @@ async fn converge_registered_schema_on( database: &Database, convergence: RegisteredSchemaConvergence, ) -> tracedecay_domain::errors::Result<()> { - observation_projection::rearm_queued_projection_retries( + let rearmed: Pin< + Box< + dyn Future< + Output = tracedecay_store::ProjectionStoreResult< + observation_projection::RearmedProjectionRetries, + >, + > + Send + + '_, + >, + > = Box::pin(observation_projection::rearm_queued_projection_retries( database, observation_projection::REARM_PROJECTION_RETRY_BATCH_ROWS, - ) - .await - .map_err(|error| { + )); + rearmed.await.map_err(|error| { global_db_operation_message("rearm queued projection retries", error.durable_detail()) })?; - ensure_authority_invariants(database, convergence.force_exhaustive, convergence.is_fresh).await + let ensured: Pin> + Send + '_>> = + Box::pin(ensure_authority_invariants( + database, + convergence.force_exhaustive, + convergence.is_fresh, + )); + ensured.await } /// Synchronously converges an attached existing store's historical schema. @@ -1205,25 +1352,33 @@ pub(crate) async fn ensure_attached_registered_schema( const OPERATION: &str = "install attached registered global database schema"; schema_install_checkpoint(cancellation, OPERATION)?; let read_connection = database.read_connection(); + let classified = classify_registered_schema_admission( + &read_connection, + &database.registered_binding().shard_id.scope, + ); let RegisteredSchemaAdmissionClassification { configuration_fresh, temporal_admission, session_features, - } = match classify_registered_schema_admission( - &read_connection, - &database.registered_binding().shard_id.scope, - ) - .await? - { + } = match classified.await? { RegisteredSchemaAdmission::Admissible(classification) => classification, RegisteredSchemaAdmission::SessionAuthorityRefused(refused) => { return Ok(RegisteredSchemaAttachmentV1::SessionsRefused(refused)); } }; - let force_exhaustive = !authority_invariant_triggers_intact(&read_connection).await?; + let triggers_intact: Pin< + Box> + Send + '_>, + > = Box::pin(authority_invariant_triggers_intact(&read_connection)); + let force_exhaustive = !triggers_intact.await?; schema_install_checkpoint(cancellation, OPERATION)?; let transaction = database.begin_bulk_write_transaction(OPERATION).await?; - let refused_authority = install_and_commit_registered_schema( + let committed: Pin< + Box< + dyn Future>> + + Send + + '_, + >, + > = Box::pin(install_and_commit_registered_schema( CancellableSchemaTransaction { transaction, cancellation, @@ -1234,8 +1389,8 @@ pub(crate) async fn ensure_attached_registered_schema( force_exhaustive, "commit attached registered global schema", "roll back attached registered global schema", - ) - .await?; + )); + let refused_authority = committed.await?; // Mirror initialization's independently durable index builds: each // historical-data index commits on its own so an interrupted later build // never rolls back an earlier completed one. @@ -1248,7 +1403,18 @@ pub(crate) async fn ensure_attached_registered_schema( })?; transaction.commit().await?; } - validate_admitted_authority_schema(&read_connection, configuration_fresh.is_some()).await?; + validate_authority_schema_contract(&read_connection) + .await + .map_err(|error| { + if configuration_fresh.is_some() { + error + } else { + tracedecay_domain::errors::TraceDecayError::reset_required( + AUTHORITY_SCHEMA_AUTHORITY, + error.to_string(), + ) + } + })?; database .run_incremental_vacuum(CANONICAL_BODY_RECLAIM_PAGES) .await?; @@ -1297,16 +1463,25 @@ pub(crate) async fn attached_registered_schema_reset_refusal( } Err(error) => return reset_or_error(error), }; - let force_exhaustive = !authority_invariant_triggers_intact(&read_connection).await?; + let triggers_intact: Pin< + Box> + Send + '_>, + > = Box::pin(authority_invariant_triggers_intact(&read_connection)); + let force_exhaustive = !triggers_intact.await?; let transaction = database.begin_bulk_write_transaction(OPERATION).await?; - let admission = install_registered_schema_stages( + let staged: Pin< + Box< + dyn Future>> + + Send + + '_, + >, + > = Box::pin(install_registered_schema_stages( &transaction, configuration_fresh.as_ref(), temporal_admission, session_features, force_exhaustive, - ) - .await; + )); + let admission = staged.await; transaction .rollback() .await @@ -1559,7 +1734,9 @@ pub async fn audit_observation_authority( }; let audit = async { validate_authority_schema_contract(&snapshot).await?; - validate_authority_rows_exhaustive(&snapshot).await + let rows: Pin> + Send + '_>> = + Box::pin(validate_authority_rows_exhaustive(&snapshot)); + rows.await }; match audit.await { Ok(()) => ObservationAuthorityAuditV1::Held, diff --git a/crates/tracedecay-global-db/src/store_registration.rs b/crates/tracedecay-global-db/src/store_registration.rs index d8d357db1a..ce451d2705 100644 --- a/crates/tracedecay-global-db/src/store_registration.rs +++ b/crates/tracedecay-global-db/src/store_registration.rs @@ -125,8 +125,7 @@ pub async fn register_project_store( let tracked_branches: BTreeSet = meta .as_ref() - .map(|meta| meta.branches.keys().cloned().collect()) - .unwrap_or_default(); + .map_or_default(|meta| meta.branches.keys().cloned().collect()); let artifact_mtimes = vec![ artifact_mtime(&store_layout.graph_db_path), artifact_mtime(&store_layout.sessions_db_path), diff --git a/crates/tracedecay-global-db/src/tests/harness.rs b/crates/tracedecay-global-db/src/tests/harness.rs index 400033fe01..1cf4e188c7 100644 --- a/crates/tracedecay-global-db/src/tests/harness.rs +++ b/crates/tracedecay-global-db/src/tests/harness.rs @@ -1589,6 +1589,9 @@ impl Executor for RegisteredGlobalDbTestFixture { } } +#[cfg(test)] +crate::observation_projection::impl_send_projection_derive!(RegisteredGlobalDbTestFixture); + #[cfg(test)] pub(crate) async fn open_registered_test_fixture( path: &std::path::Path, diff --git a/crates/tracedecay-global-db/src/tests/lcm_privacy_rescan.rs b/crates/tracedecay-global-db/src/tests/lcm_privacy_rescan.rs index 0e2bcaaa3c..1162c75fce 100644 --- a/crates/tracedecay-global-db/src/tests/lcm_privacy_rescan.rs +++ b/crates/tracedecay-global-db/src/tests/lcm_privacy_rescan.rs @@ -222,8 +222,7 @@ fn payload_dir_holds(storage_root: &std::path::Path, needle: &str) -> bool { }; entries.filter_map(Result::ok).any(|entry| { std::fs::read(entry.path()) - .map(|bytes| String::from_utf8_lossy(&bytes).contains(needle)) - .unwrap_or(false) + .is_ok_and(|bytes| String::from_utf8_lossy(&bytes).contains(needle)) }) } diff --git a/crates/tracedecay-global-db/src/tests/lcm_schema/temporal_cursor.rs b/crates/tracedecay-global-db/src/tests/lcm_schema/temporal_cursor.rs index de513ebf25..6434d6227e 100644 --- a/crates/tracedecay-global-db/src/tests/lcm_schema/temporal_cursor.rs +++ b/crates/tracedecay-global-db/src/tests/lcm_schema/temporal_cursor.rs @@ -512,8 +512,7 @@ async fn temporal_schema_concurrent_cursor_rotations_serialize_safely(cx: &asupe let message = result .as_ref() .err() - .map(ToString::to_string) - .unwrap_or_default() + .map_or_default(ToString::to_string) .to_ascii_lowercase(); assert!( !message.contains("busy") && !message.contains("locked"), diff --git a/crates/tracedecay-graph-db/src/generation/recovered.rs b/crates/tracedecay-graph-db/src/generation/recovered.rs index f61474a0f5..e5f731ab72 100644 --- a/crates/tracedecay-graph-db/src/generation/recovered.rs +++ b/crates/tracedecay-graph-db/src/generation/recovered.rs @@ -314,8 +314,7 @@ fn digest_rows_parallel( .chain(relations.chunks(chunk_rows).map(ProofChunk::Relations)) .collect(); let workers = std::thread::available_parallelism() - .map(std::num::NonZeroUsize::get) - .unwrap_or(1) + .map_or(1, std::num::NonZeroUsize::get) .min(PROOF_MAX_WORKERS) .min(chunks.len()) .max(1); diff --git a/crates/tracedecay-graph-db/src/runtime.rs b/crates/tracedecay-graph-db/src/runtime.rs index 51a045bafe..588dcef967 100644 --- a/crates/tracedecay-graph-db/src/runtime.rs +++ b/crates/tracedecay-graph-db/src/runtime.rs @@ -228,11 +228,10 @@ impl Inner { /// Delete the engine's heap once the engine itself is dropped, returning /// every page it built on whichever thread freed it. fn release_engine_heap(&self) { - let heap = self - .engine_heap - .lock() - .map(|mut heap| heap.take()) - .unwrap_or_else(|poisoned| poisoned.into_inner().take()); + let heap = self.engine_heap.lock().map_or_else( + |poisoned| poisoned.into_inner().take(), + |mut heap| heap.take(), + ); drop(heap); } } @@ -1128,8 +1127,7 @@ impl GraphDb { .inner .sealed_generations .write() - .map(|mut sealed| std::mem::take(&mut *sealed)) - .unwrap_or_default(); + .map_or_default(|mut sealed| std::mem::take(&mut *sealed)); for store in sealed.into_values() { let _ = store.close(); } @@ -1283,8 +1281,7 @@ impl GraphDb { .inner .sealed_generations .read() - .map(|sealed| sealed.values().cloned().collect::>()) - .unwrap_or_default(); + .map_or_default(|sealed| sealed.values().cloned().collect::>()); for store in sealed { let _ = store.hibernate_if_lazy(); } @@ -1828,8 +1825,7 @@ pub(crate) enum GraphEngineOpenSite { /// In-memory engines have no container path; the label keeps the operator log /// field present so open/release events pair up per engine. fn container_label(path: Option<&std::path::Path>) -> String { - path.map(|path| path.display().to_string()) - .unwrap_or_else(|| "memory".to_owned()) + path.map_or_else(|| "memory".to_owned(), |path| path.display().to_string()) } impl GraphEngineOpenSite { diff --git a/crates/tracedecay-graph-db/src/sealed_store.rs b/crates/tracedecay-graph-db/src/sealed_store.rs index 3a35f1158c..57862ef3b8 100644 --- a/crates/tracedecay-graph-db/src/sealed_store.rs +++ b/crates/tracedecay-graph-db/src/sealed_store.rs @@ -2018,9 +2018,7 @@ fn push_manifest_rows( } let projection = &identity.projection.projection; let entities = &manifest.entities; - let workers = rayon::current_thread_index() - .map(|_| rayon::current_num_threads()) - .unwrap_or(1); + let workers = rayon::current_thread_index().map_or(1, |_| rayon::current_num_threads()); let row_window = workers.max(1).saturating_mul(512); { let _span = tracing::trace_span!("code_index.seal.encode.entities").entered(); @@ -2138,9 +2136,7 @@ fn push_spilled_rows( check: &dyn Fn() -> Result<(), GraphDbError>, ) -> Result<(usize, usize), GraphDbError> { let projection = &identity.projection.projection; - let workers = rayon::current_thread_index() - .map(|_| rayon::current_num_threads()) - .unwrap_or(1); + let workers = rayon::current_thread_index().map_or(1, |_| rayon::current_num_threads()); let row_window = workers.max(1).saturating_mul(512); let (entity_count, relation_count) = spilled.row_counts(); { @@ -3531,7 +3527,7 @@ mod cost_probe { fn directory_bytes(path: &std::path::Path) -> u64 { let Ok(entries) = std::fs::read_dir(path) else { - return std::fs::metadata(path).map(|meta| meta.len()).unwrap_or(0); + return std::fs::metadata(path).map_or(0, |meta| meta.len()); }; entries .filter_map(Result::ok) @@ -3540,7 +3536,7 @@ mod cost_probe { if path.is_dir() { directory_bytes(&path) } else { - std::fs::metadata(&path).map(|meta| meta.len()).unwrap_or(0) + std::fs::metadata(&path).map_or(0, |meta| meta.len()) } }) .sum() @@ -3567,9 +3563,8 @@ mod cost_probe { let entities = env_usize("TRACEDECAY_VERIFY_PROBE_ROWS", 50_000); let relations = entities.saturating_mul(8) / 7; let payload = env_usize("TRACEDECAY_VERIFY_PROBE_PAYLOAD", 700); - let bytes_payload = std::env::var("TRACEDECAY_VERIFY_PROBE_FORM") - .map(|form| form != "compact") - .unwrap_or(true); + let bytes_payload = + std::env::var("TRACEDECAY_VERIFY_PROBE_FORM").map_or(true, |form| form != "compact"); let check: &dyn Fn() -> Result<(), crate::GraphDbError> = &|| Ok(()); let temp = tempfile::tempdir().unwrap(); diff --git a/crates/tracedecay-graph-db/src/verified_marker.rs b/crates/tracedecay-graph-db/src/verified_marker.rs index e80110efb4..7f724b92e9 100644 --- a/crates/tracedecay-graph-db/src/verified_marker.rs +++ b/crates/tracedecay-graph-db/src/verified_marker.rs @@ -404,8 +404,7 @@ impl GenerationMarkers { admitted: self .container .as_deref() - .map(|container| load(container, identity)) - .unwrap_or_default(), + .map_or_default(|container| load(container, identity)), proven: BTreeMap::new(), pristine: true, }); diff --git a/crates/tracedecay-graph-db/tests/at_rest_grafeo_probe.rs b/crates/tracedecay-graph-db/tests/at_rest_grafeo_probe.rs index ccac19a477..d34bf0d1ab 100644 --- a/crates/tracedecay-graph-db/tests/at_rest_grafeo_probe.rs +++ b/crates/tracedecay-graph-db/tests/at_rest_grafeo_probe.rs @@ -191,7 +191,7 @@ fn grafeo_at_rest_probe() { let hwm_closed = peak_rss_kib().unwrap(); let rss_closed = live_rss_kib().unwrap(); - let on_disk = std::fs::metadata(&path).map(|meta| meta.len()).unwrap_or(0); + let on_disk = std::fs::metadata(&path).map_or(0, |meta| meta.len()); let open_started = Instant::now(); let reopened = GrafeoDB::open(&path).unwrap(); diff --git a/crates/tracedecay-graph-db/tests/at_rest_snapshot.rs b/crates/tracedecay-graph-db/tests/at_rest_snapshot.rs index 2cc692bdd0..cf435aeeef 100644 --- a/crates/tracedecay-graph-db/tests/at_rest_snapshot.rs +++ b/crates/tracedecay-graph-db/tests/at_rest_snapshot.rs @@ -326,9 +326,7 @@ fn at_rest_reopen_probe() { let hwm_closed = peak_rss_kib().unwrap(); let rss_closed = live_rss_kib().unwrap(); - let on_disk = std::fs::metadata(root.join("graph.grafeo")) - .map(|meta| meta.len()) - .unwrap_or(0); + let on_disk = std::fs::metadata(root.join("graph.grafeo")).map_or(0, |meta| meta.len()); // ---- activation path ---- let open_started = Instant::now(); diff --git a/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/code_graph_layout.rs b/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/code_graph_layout.rs index 3e0ad4a0b5..1793336021 100644 --- a/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/code_graph_layout.rs +++ b/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/code_graph_layout.rs @@ -386,14 +386,12 @@ fn sweep_keeps_the_shared_head_while_the_deleted_generation_still_serves() { /// Number of sealed generation artifacts currently on disk under the store. fn sealed_generation_count(root: &std::path::Path) -> usize { - std::fs::read_dir(support::graph_path(root).with_extension("sealed")) - .map(|entries| { - entries - .map(Result::unwrap) - .filter(|entry| entry.path().join("sealed.json").is_file()) - .count() - }) - .unwrap_or(0) + std::fs::read_dir(support::graph_path(root).with_extension("sealed")).map_or(0, |entries| { + entries + .map(Result::unwrap) + .filter(|entry| entry.path().join("sealed.json").is_file()) + .count() + }) } fn publish_generation( diff --git a/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/layered_store.rs b/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/layered_store.rs index 1c562ea785..35052ef189 100644 --- a/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/layered_store.rs +++ b/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/layered_store.rs @@ -980,8 +980,10 @@ fn measure_seal_peak(symbols: usize) -> u64 { let peak = stdout .lines() .find_map(|line| line.split_once(SEAL_PEAK_PROBE_LINE)) - .map(|(_, peak)| peak.trim().parse().unwrap()) - .unwrap_or_else(|| panic!("seal peak probe printed no peak: {stdout}")); + .map_or_else( + || panic!("seal peak probe printed no peak: {stdout}"), + |(_, peak)| peak.trim().parse().unwrap(), + ); println!("{symbols}-symbol base: layered seal +{peak} KiB"); peak } diff --git a/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/sealed_store.rs b/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/sealed_store.rs index 5b4e898b67..3bb165ee0f 100644 --- a/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/sealed_store.rs +++ b/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/sealed_store.rs @@ -1777,7 +1777,7 @@ fn probe_mib(kib: u64) -> f64 { fn directory_bytes(path: &Path) -> u64 { let Ok(entries) = std::fs::read_dir(path) else { - return std::fs::metadata(path).map(|meta| meta.len()).unwrap_or(0); + return std::fs::metadata(path).map_or(0, |meta| meta.len()); }; entries .map(Result::unwrap) @@ -1786,7 +1786,7 @@ fn directory_bytes(path: &Path) -> u64 { if path.is_dir() { directory_bytes(&path) } else { - std::fs::metadata(&path).map(|meta| meta.len()).unwrap_or(0) + std::fs::metadata(&path).map_or(0, |meta| meta.len()) } }) .sum() @@ -1968,9 +1968,8 @@ fn sealed_artifact_open_probe() { drop(commit); assert!(registered.close().unwrap()); - let staging_bytes = std::fs::metadata(support::graph_path(temp.path())) - .map(|meta| meta.len()) - .unwrap_or(0); + let staging_bytes = + std::fs::metadata(support::graph_path(temp.path())).map_or(0, |meta| meta.len()); let sealed_root = sealed_store_root(temp.path()); let artifact_dir = std::fs::read_dir(&sealed_root) .unwrap() @@ -2159,15 +2158,13 @@ fn sealed_container_sections(root: &Path) -> (u64, Vec) { } fn row_spills(root: &Path) -> Vec { - std::fs::read_dir(sealed_store_root(root)) - .map(|entries| { - entries - .map(Result::unwrap) - .filter_map(|entry| entry.file_name().to_str().map(str::to_owned)) - .filter(|name| name.starts_with(".rows-")) - .collect() - }) - .unwrap_or_default() + std::fs::read_dir(sealed_store_root(root)).map_or_default(|entries| { + entries + .map(Result::unwrap) + .filter_map(|entry| entry.file_name().to_str().map(str::to_owned)) + .filter(|name| name.starts_with(".rows-")) + .collect() + }) } /// Rows pushed through a spill in shuffled batches, repeats included, seal diff --git a/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/staging_footprint.rs b/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/staging_footprint.rs index 9145e03556..01c55d10f3 100644 --- a/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/staging_footprint.rs +++ b/crates/tracedecay-graph-db/tests/graph_db_suite/verified_generation_contract/staging_footprint.rs @@ -698,9 +698,7 @@ fn sealed_generations_release_staging_rows_without_a_resident_lease() { // too small for a byte bound to distinguish compaction from framing. println!( "TRACEDECAY_STAGING_CONTAINER_BYTES after_release_and_reopen={}", - std::fs::metadata(support::graph_path(temp.path())) - .map(|meta| meta.len()) - .unwrap_or(0) + std::fs::metadata(support::graph_path(temp.path())).map_or(0, |meta| meta.len()) ); } diff --git a/crates/tracedecay-graph-query/src/test_risk.rs b/crates/tracedecay-graph-query/src/test_risk.rs index 3f702b2378..0eabc3beb8 100644 --- a/crates/tracedecay-graph-query/src/test_risk.rs +++ b/crates/tracedecay-graph-query/src/test_risk.rs @@ -196,8 +196,7 @@ pub async fn analyze_test_risk( let top_risk_untested = risks .iter() .find(|risk| !risk.has_test()) - .map(|risk| risk.name.clone()) - .unwrap_or_default(); + .map_or_default(|risk| risk.name.clone()); let reachable_unattributed = source_fns .iter() .filter(|n| { diff --git a/crates/tracedecay-host-admission/src/projection_drain.rs b/crates/tracedecay-host-admission/src/projection_drain.rs index 32235c16e2..3d8bf6fae0 100644 --- a/crates/tracedecay-host-admission/src/projection_drain.rs +++ b/crates/tracedecay-host-admission/src/projection_drain.rs @@ -1,4 +1,6 @@ use std::collections::BTreeSet; +use std::future::Future; +use std::pin::Pin; use tracedecay_sessions::admission::{HostAdmissionOutcome, HostProjectionDrainOutcome}; use tracedecay_sessions::runtime::git_correlation::SystemGit; @@ -31,16 +33,23 @@ impl HostAdmissionFacade<'_> { .registered_database(host_scope(scope))? .ok_or_else(HostAdmissionOutcome::registered_authority_unavailable)?; let store = self.store(provider, scope)?; - let predecessor = store - .converge_projection_predecessor() - .await - .map_err(|error| { - tracing::warn!( - error = %error.durable_detail(), - "predecessor projection convergence failed during host drain" - ); - projection_error_outcome(&error) - })?; + let predecessor: Pin< + Box< + dyn Future< + Output = tracedecay_store::ProjectionStoreResult< + tracedecay_store::ProjectionPredecessorConvergence, + >, + > + Send + + '_, + >, + > = Box::pin(store.converge_projection_predecessor()); + let predecessor = predecessor.await.map_err(|error| { + tracing::warn!( + error = %error.durable_detail(), + "predecessor projection convergence failed during host drain" + ); + projection_error_outcome(&error) + })?; if cancellation.is_cancelled() { return Err(classify_error(&ObservationApplicationError::Cancelled)); } @@ -57,13 +66,21 @@ impl HostAdmissionFacade<'_> { tracedecay_session_memory::external_source_store::RuntimeExternalSourceStore::new( database.runtime_client(), ); - let external_replay = external_source - .drain_host_projection_replay(max, cancellation) - .await - .map_err(|error| { - tracing::warn!(%error, "external-source projection replay failed during host drain"); - HostAdmissionOutcome::retained_unavailable("external_source_projection_unavailable") - })?; + let replay: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_session_memory::external_source_store::RuntimeProjectionReplayOutcomeV1, + tracedecay_session_memory::external_source_store::RuntimeExternalSourceErrorV1, + >, + > + Send + + '_, + >, + > = Box::pin(external_source.drain_host_projection_replay(max, cancellation)); + let external_replay = replay.await.map_err(|error| { + tracing::warn!(%error, "external-source projection replay failed during host drain"); + HostAdmissionOutcome::retained_unavailable("external_source_projection_unavailable") + })?; if cancellation.is_cancelled() { return Err(classify_error(&ObservationApplicationError::Cancelled)); } @@ -85,7 +102,17 @@ impl HostAdmissionFacade<'_> { return Err(classify_error(&ObservationApplicationError::Cancelled)); } let window = PROJECTION_DRAIN_TXN_WINDOW.min(remaining); - match store.project_queued_observations(window).await { + let queued: Pin< + Box< + dyn Future< + Output = tracedecay_store::ProjectionStoreResult< + Option, + >, + > + Send + + '_, + >, + > = Box::pin(store.project_queued_observations(window)); + match queued.await { Ok(Some(batch)) => { let item_count = batch.items.len(); for item in batch.items { @@ -136,7 +163,17 @@ impl HostAdmissionFacade<'_> { if cancellation.is_cancelled() { return Err(classify_error(&ObservationApplicationError::Cancelled)); } - let Some(observation_id) = store.next_queued_observation().await.map_err(|error| { + let next_queued: Pin< + Box< + dyn Future< + Output = tracedecay_store::ProjectionStoreResult< + Option, + >, + > + Send + + '_, + >, + > = Box::pin(store.next_queued_observation()); + let Some(observation_id) = next_queued.await.map_err(|error| { warn_projection_drain_store_error( &error, "projection store operation failed during host drain", @@ -147,7 +184,17 @@ impl HostAdmissionFacade<'_> { observation_queue_exhausted = true; break; }; - let projected = match store.project_observation(&observation_id).await { + let projected_item: Pin< + Box< + dyn Future< + Output = tracedecay_store::ProjectionStoreResult< + tracedecay_store::ProjectionPersistOutcome, + >, + > + Send + + '_, + >, + > = Box::pin(store.project_observation(&observation_id)); + let projected = match projected_item.await { Ok(projected) => projected, Err(ProjectionStoreError::RetryDeferred { .. }) => { observation_deferred = true; @@ -191,17 +238,23 @@ impl HostAdmissionFacade<'_> { outcome.projected_outputs = outcome.projected_outputs.saturating_add( u64::try_from(projected.output_count()).unwrap_or(u64::MAX), ); - if let Some(observation) = store - .get_observation(&observation_id) - .await - .map_err(|error| { - warn_projection_drain_store_error( - &error, - "projection store operation failed during host drain", - ); - observation_store_drain_outcome(&error) - })? - { + let loaded: Pin< + Box< + dyn Future< + Output = tracedecay_store::ObservationStoreResult< + Option, + >, + > + Send + + '_, + >, + > = Box::pin(store.get_observation(&observation_id)); + if let Some(observation) = loaded.await.map_err(|error| { + warn_projection_drain_store_error( + &error, + "projection store operation failed during host drain", + ); + observation_store_drain_outcome(&error) + })? { session_ids.insert( observation .observation() @@ -224,8 +277,17 @@ impl HostAdmissionFacade<'_> { if cancellation.is_cancelled() { return Err(classify_error(&ObservationApplicationError::Cancelled)); } - observation_deferred = store - .next_queued_observation() + let suffix: Pin< + Box< + dyn Future< + Output = tracedecay_store::ProjectionStoreResult< + Option, + >, + > + Send + + '_, + >, + > = Box::pin(store.next_queued_observation()); + observation_deferred = suffix .await .map_err(|error| { warn_projection_drain_store_error( @@ -244,9 +306,21 @@ impl HostAdmissionFacade<'_> { if cancellation.is_cancelled() { return Err(classify_error(&ObservationApplicationError::Cancelled)); } - let convergence = database - .converge_session_git_evidence(&SystemGit, self.authorities.project_root.as_deref()) - .await; + let git_evidence: Pin< + Box< + dyn Future< + Output = std::result::Result< + tracedecay_sessions::runtime::git_correlation::GitEvidencePassOutcome, + tracedecay_sessions::runtime::git_correlation::GitCorrelationError, + >, + > + Send + + '_, + >, + > = Box::pin(database.converge_session_git_evidence( + &SystemGit, + self.authorities.project_root.as_deref(), + )); + let convergence = git_evidence.await; if cancellation.is_cancelled() { return Err(classify_error(&ObservationApplicationError::Cancelled)); } diff --git a/crates/tracedecay-host-admission/src/session_ingest_authority.rs b/crates/tracedecay-host-admission/src/session_ingest_authority.rs index 10708fa3ab..3f8b80ccfd 100644 --- a/crates/tracedecay-host-admission/src/session_ingest_authority.rs +++ b/crates/tracedecay-host-admission/src/session_ingest_authority.rs @@ -1,7 +1,9 @@ //! Registered-database composition adapter for bounded session ingest. use std::borrow::Borrow; +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; use tracedecay_sessions::admission::HostAdmission; @@ -143,14 +145,18 @@ where skip_all )] async fn registered_project_roots(&self) -> Option> { - let mut roots = self.db().try_list_project_paths().await.ok()?; - roots.extend( - self.db() - .try_list_code_project_paths(usize::MAX) - .await - .ok()?, - ); - roots.extend(self.db().try_list_project_alias_paths().await.ok()?); + let paths: Pin< + Box>> + Send + '_>, + > = Box::pin(self.db().try_list_project_paths()); + let mut roots = paths.await.ok()?; + let code_paths: Pin< + Box>> + Send + '_>, + > = Box::pin(self.db().try_list_code_project_paths(usize::MAX)); + roots.extend(code_paths.await.ok()?); + let aliases: Pin< + Box>> + Send + '_>, + > = Box::pin(self.db().try_list_project_alias_paths()); + roots.extend(aliases.await.ok()?); Some(roots) } } diff --git a/crates/tracedecay-lcm/src/compression.rs b/crates/tracedecay-lcm/src/compression.rs index 6950bb8f32..49bc92b8dc 100644 --- a/crates/tracedecay-lcm/src/compression.rs +++ b/crates/tracedecay-lcm/src/compression.rs @@ -361,69 +361,119 @@ fn canonical_replay_messages(raw_messages: &[LcmRawMessage]) -> Vec { replay_transactions::normalize_replay_tool_pairs(&replay) } -/// Runs canonical compression for one request. +/// Admission outcome for one compression request. /// -/// The request is borrowed: a daemon journey that needs an authoritative -/// summary runs a planning pass and a commit pass over the same message -/// corpus, changing only `summarizer` in between, so the corpus is owned -/// once by the caller rather than cloned per pass. -#[tracing::instrument(name = "sessions.lcm.compress", level = "trace", skip_all)] -pub async fn compress( +/// `Publish` is a separate future from admission so the summary-publication +/// transaction is its own `Send` root. One generic async function that awaits +/// both exceeds the trait solver's recursion limit. +pub enum CompressionAdmission { + Finished(super::summary_convergence::LcmBoundedCompressionResponse), + Classify(CompressionPublish), +} + +pub enum CompressionClassification { + Finished(super::summary_convergence::LcmBoundedCompressionResponse), + Publish(CompressionPublish), +} + +pub struct CompressionPublish { + summarizer: CompressionSummarizerAdapter, + context: CompressionTransactionContext, +} + +fn bounded_compression( + response: LcmCompressionResponse, + rows_scanned: usize, + bytes_scanned: u64, + has_more: bool, +) -> super::summary_convergence::LcmBoundedCompressionResponse { + super::summary_convergence::LcmBoundedCompressionResponse { + response, + rows_scanned, + bytes_scanned, + has_more, + } +} + +pub async fn classify_compression_pass( conn: &impl Executor, - scope: &ObservationScopeV1, publisher: &impl dag::LcmSummaryPublicationPort, - storage_root: &Path, request: &LcmCompressionRequest, - payload_rollback: &mut payload::PayloadFileRollback, -) -> Result { - compress_inner( - conn, - scope, - publisher, - storage_root, - request, - payload_rollback, - None, - ) - .await - .map(|bounded| bounded.response) + publish: CompressionPublish, +) -> Result { + let CompressionPublish { + summarizer, + context, + } = publish; + let scan = ( + context.raw_rows_scanned, + context.raw_bytes_scanned, + context.raw_has_more, + ); + if let Some(response) = + no_backlog_compression_response(conn, publisher, request, &summarizer, &context).await? + { + return Ok(CompressionClassification::Finished(bounded_compression( + response, scan.0, scan.1, scan.2, + ))); + } + if let Some(response) = backlog_below_threshold_response(conn, request, &context).await? { + return Ok(CompressionClassification::Finished(bounded_compression( + response, scan.0, scan.1, scan.2, + ))); + } + if let Some(response) = auxiliary_summary_response(request, &summarizer, &context) { + return Ok(CompressionClassification::Finished(bounded_compression( + response, scan.0, scan.1, scan.2, + ))); + } + Ok(CompressionClassification::Publish(CompressionPublish { + summarizer, + context, + })) } -/// Runs canonical compression over one bounded retained-session raw page. -/// -/// This is the background-convergence entry point. Interactive compression -/// keeps its complete replay contract, while retained convergence advances the -/// same lifecycle CAS without materializing a mega-session in one future. -pub async fn compress_retained_page( +pub async fn publish_compression_pass( conn: &impl Executor, - scope: &ObservationScopeV1, publisher: &impl dag::LcmSummaryPublicationPort, - storage_root: &Path, request: &LcmCompressionRequest, - payload_rollback: &mut payload::PayloadFileRollback, - guard: RetainedCompressionGuard, + publish: CompressionPublish, ) -> Result { - compress_inner( + let rows_scanned = publish.context.raw_rows_scanned; + let bytes_scanned = publish.context.raw_bytes_scanned; + let has_more = publish.context.raw_has_more; + let response = persist_and_replay_backlog_compression( conn, - scope, publisher, - storage_root, request, - payload_rollback, - Some(guard), + &publish.summarizer, + publish.context, ) - .await + .await?; + Ok(bounded_compression( + response, + rows_scanned, + bytes_scanned, + has_more, + )) } -async fn compress_inner( +/// Runs canonical compression until the summary-publication transaction. +/// +/// The request is borrowed: a daemon journey that needs an authoritative +/// summary runs a planning pass and a commit pass over the same message +/// corpus, changing only `summarizer` in between, so the corpus is owned +/// once by the caller rather than cloned per pass. Retained convergence +/// passes `retained_scan`; interactive compression passes `None` and keeps +/// the complete replay contract. +pub async fn begin_compression_pass( conn: &impl Executor, scope: &ObservationScopeV1, - publisher: &impl dag::LcmSummaryPublicationPort, storage_root: &Path, request: &LcmCompressionRequest, payload_rollback: &mut payload::PayloadFileRollback, retained_scan: Option, -) -> Result { +) -> Result { let assembly_token_cap = compression_decision::effective_assembly_token_cap(AssemblyCapInput { max_assembly_tokens: request.max_assembly_tokens, context_length: request.context_length, @@ -441,8 +491,8 @@ async fn compress_inner( &request.session_id, ) .await?; - return Ok(super::summary_convergence::LcmBoundedCompressionResponse { - response: compression_response( + return Ok(CompressionAdmission::Finished(bounded_compression( + compression_response( "ok", reason, Vec::new(), @@ -451,10 +501,10 @@ async fn compress_inner( None, assembly_token_cap, ), - rows_scanned: 0, - bytes_scanned: 0, - has_more: false, - }); + 0, + 0, + false, + ))); } ensure_session(conn, scope, &request.provider, &request.session_id).await?; @@ -488,39 +538,21 @@ async fn compress_inner( None, assembly_token_cap, ); - return Ok(super::summary_convergence::LcmBoundedCompressionResponse { - response, - rows_scanned: 0, - bytes_scanned: 0, - has_more: false, - }); + return Ok(CompressionAdmission::Finished(bounded_compression( + response, 0, 0, false, + ))); } - let (response, rows_scanned, bytes_scanned, has_more) = compress_in_transaction( - conn, - publisher, - request, - assembly_token_cap, - &summarizer, - retained_scan, - ) - .await?; - Ok(super::summary_convergence::LcmBoundedCompressionResponse { - response, - rows_scanned, - bytes_scanned, - has_more, - }) + open_compression_transaction(conn, request, assembly_token_cap, summarizer, retained_scan).await } -async fn compress_in_transaction( +async fn open_compression_transaction( conn: &impl Executor, - publisher: &impl dag::LcmSummaryPublicationPort, request: &LcmCompressionRequest, assembly_token_cap: Option, - summarizer: &CompressionSummarizerAdapter, + summarizer: CompressionSummarizerAdapter, retained_scan: Option, -) -> Result<(LcmCompressionResponse, usize, u64, bool), LcmError> { +) -> Result { let expected_summary_source_range = retained_scan .as_ref() .and_then(|guard| guard.expected_summary_source_range.as_ref()) @@ -546,30 +578,19 @@ async fn compress_in_transaction( context.raw_has_more, ); if let Some(response) = frontier_changed_response(request, &context) { - return Ok((response, scan.0, scan.1, scan.2)); - } - if let Some(response) = - no_backlog_compression_response(conn, publisher, request, summarizer, &context).await? - { - return Ok((response, scan.0, scan.1, scan.2)); - } - if let Some(response) = backlog_below_threshold_response(conn, request, &context).await? { - return Ok((response, scan.0, scan.1, scan.2)); - } - if let Some(response) = auxiliary_summary_response(request, summarizer, &context) { - return Ok((response, scan.0, scan.1, scan.2)); + return Ok(CompressionAdmission::Finished(bounded_compression( + response, scan.0, scan.1, scan.2, + ))); } - - let response = - persist_and_replay_backlog_compression(conn, publisher, request, summarizer, context) - .await?; - Ok((response, scan.0, scan.1, scan.2)) + Ok(CompressionAdmission::Classify(CompressionPublish { + summarizer, + context, + })) } // The read phase: whole-session raw load plus window/plan derivation. // Together with `ingest_active`, `persist`, and `assemble_replay` this splits // the inclusive `compress` span into its sequential phases. -#[tracing::instrument(name = "sessions.lcm.compress.prepare", level = "trace", skip_all)] async fn prepare_compression_context( conn: &impl QueryExecutor, request: &LcmCompressionRequest, @@ -954,7 +975,6 @@ async fn persist_and_replay_backlog_compression( // The summary-publication transaction: chunk selection, immutable summary // publication, and the lifecycle/debt writes that commit the new frontier. -#[tracing::instrument(name = "sessions.lcm.compress.persist", level = "trace", skip_all)] async fn persist_compression_transaction_writes<'a>( conn: &impl Executor, publisher: &impl dag::LcmSummaryPublicationPort, @@ -1261,11 +1281,6 @@ struct ReplayWindowParts<'a> { /// is trimmed under the effective assembly cap. // Replay assembly; its DAG-build share is the nested // `sessions.lcm.dag.load_uncondensed` span. -#[tracing::instrument( - name = "sessions.lcm.compress.assemble_replay", - level = "trace", - skip_all -)] async fn assemble_replay_context( conn: &impl QueryExecutor, provider: &str, @@ -1297,11 +1312,6 @@ async fn assemble_replay_context( /// Mirrors hermes-lcm `_assemble_overflow_recovery_context`: assemble under /// the cap; when nothing beyond the anchors fits, fall back to anchors plus /// the most recent message even if that stays over budget. -#[tracing::instrument( - name = "sessions.lcm.compress.assemble_overflow_replay", - level = "trace", - skip_all -)] async fn assemble_overflow_recovery_replay( conn: &impl QueryExecutor, provider: &str, @@ -1971,11 +1981,6 @@ async fn load_condensation_candidates( // Active-context ingest: sanitization, payload externalization, and raw // upserts. This is compression's write-side file I/O + CPU phase, distinct // from the summary-publication transaction in `persist`. -#[tracing::instrument( - name = "sessions.lcm.compress.ingest_active", - level = "trace", - skip_all -)] async fn ingest_active_messages( conn: &impl Executor, storage_root: &Path, diff --git a/crates/tracedecay-lcm/src/payload.rs b/crates/tracedecay-lcm/src/payload.rs index 2980c6a293..d61ba4a080 100644 --- a/crates/tracedecay-lcm/src/payload.rs +++ b/crates/tracedecay-lcm/src/payload.rs @@ -474,8 +474,7 @@ async fn ensure_current_raw_payload_ref( let value: Option = row.get(index).unwrap_or(None); if value .as_deref() - .map(extract_payload_refs_from_text) - .unwrap_or_default() + .map_or_default(extract_payload_refs_from_text) .iter() .any(|reference| reference == &payload.payload_ref) { diff --git a/crates/tracedecay-lcm/src/query/payload_health.rs b/crates/tracedecay-lcm/src/query/payload_health.rs index ac456927b8..0c42c102f3 100644 --- a/crates/tracedecay-lcm/src/query/payload_health.rs +++ b/crates/tracedecay-lcm/src/query/payload_health.rs @@ -443,8 +443,7 @@ async fn payload_ref_locations_for_scope( }; for payload_ref in value .as_deref() - .map(payload::extract_payload_refs_from_text) - .unwrap_or_default() + .map_or_default(payload::extract_payload_refs_from_text) { refs.entry(payload_ref.clone()) .or_insert_with(|| PayloadRefLocation { diff --git a/crates/tracedecay-lsp/src/analyzer/process.rs b/crates/tracedecay-lsp/src/analyzer/process.rs index 129b00e948..0e1dcafd0c 100644 --- a/crates/tracedecay-lsp/src/analyzer/process.rs +++ b/crates/tracedecay-lsp/src/analyzer/process.rs @@ -190,8 +190,7 @@ impl AnalyzerProcess { pub(super) fn stderr(&self) -> String { self.stderr .lock() - .map(|captured| String::from_utf8_lossy(&captured).trim().to_owned()) - .unwrap_or_default() + .map_or_default(|captured| String::from_utf8_lossy(&captured).trim().to_owned()) } } diff --git a/crates/tracedecay-lsp/src/compile_diagnostics/tsconfig.rs b/crates/tracedecay-lsp/src/compile_diagnostics/tsconfig.rs index ae1981cd41..e5bd5fffed 100644 --- a/crates/tracedecay-lsp/src/compile_diagnostics/tsconfig.rs +++ b/crates/tracedecay-lsp/src/compile_diagnostics/tsconfig.rs @@ -146,14 +146,11 @@ impl TypeScriptProjectConfigInputs { #[must_use] pub fn load(project_root: &Path, tsconfig: &Path) -> Self { let tsconfig = normalize(tsconfig); - let manifest_dirs = tsconfig - .parent() - .map(|dir| { - ancestors_within(project_root, dir) - .map(Path::to_path_buf) - .collect() - }) - .unwrap_or_default(); + let manifest_dirs = tsconfig.parent().map_or_default(|dir| { + ancestors_within(project_root, dir) + .map(Path::to_path_buf) + .collect() + }); Self { config: Tsconfig::load(&tsconfig), tsconfig, diff --git a/crates/tracedecay-lsp/src/protocol/lifecycle_controller.rs b/crates/tracedecay-lsp/src/protocol/lifecycle_controller.rs index a108482e5d..4949f4acde 100644 --- a/crates/tracedecay-lsp/src/protocol/lifecycle_controller.rs +++ b/crates/tracedecay-lsp/src/protocol/lifecycle_controller.rs @@ -590,23 +590,17 @@ where return; } }; - let mounted_context = self - .context - .port - .as_ref() - .map(|context| { - context - .registrations() - .into_iter() - .filter(|registration| { - is_supported_context_projection(®istration.kind) - && registration.revision > 0 - }) - .take(MAX_CONTEXT_PROJECTION_KINDS) - .map(|registration| (registration.kind, registration.revision)) - .collect::>() - }) - .unwrap_or_default(); + let mounted_context = self.context.port.as_ref().map_or_default(|context| { + context + .registrations() + .into_iter() + .filter(|registration| { + is_supported_context_projection(®istration.kind) && registration.revision > 0 + }) + .take(MAX_CONTEXT_PROJECTION_KINDS) + .map(|registration| (registration.kind, registration.revision)) + .collect::>() + }); let mut gateway_capabilities = self.lifecycle.gateway_capabilities.clone(); let dynamic_workspace_diagnostics = client.diagnostic_dynamic_registration && client.supports_document_diagnostics diff --git a/crates/tracedecay-maintenance/src/retention/diagnostics.rs b/crates/tracedecay-maintenance/src/retention/diagnostics.rs index a26767c54a..d26715b80c 100644 --- a/crates/tracedecay-maintenance/src/retention/diagnostics.rs +++ b/crates/tracedecay-maintenance/src/retention/diagnostics.rs @@ -1,7 +1,9 @@ //! Read-only retention diagnostics owned beside the retention kernels. use std::collections::BTreeSet; +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; use tracedecay_contracts::doctor::{ DoctorCoverageCompletenessV1, DoctorCoverageStatementV1, DoctorEvidenceRefV1, @@ -108,24 +110,37 @@ pub async fn collect_profile_storage_findings( observed_at_secs: i64, ) -> ProfileStorageFindingsV1 { let scan_root = profile_root.join("projects"); - let permitted = native_runtime::spawn_blocking(move || { - let _span = tracing::trace_span!("maintenance.diagnostics.profile_scan").entered(); - permits_synchronous_exhaustive_scan(&scan_root) - }) - .await - .is_ok_and(|permitted| permitted); + let permitted_scan: Pin> + Send>> = + Box::pin(native_runtime::spawn_blocking(move || { + let _span = tracing::trace_span!("maintenance.diagnostics.profile_scan").entered(); + permits_synchronous_exhaustive_scan(&scan_root) + })); + let permitted = permitted_scan.await.is_ok_and(|permitted| permitted); if !permitted { return ProfileStorageFindingsV1::unknown(); } - let (registered_census, unregistered) = futures_util::join!( - super::orphan_stores::build_store_census(global_db, profile_root), - collect_unregistered_store_findings( - global_db, - profile_root, - retention_secs, - observed_at_secs, - ), - ); + let census: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Vec, + >, + > + Send + + '_, + >, + > = Box::pin(super::orphan_stores::build_store_census( + global_db, + profile_root, + )); + let unregistered_findings: Pin< + Box + Send + '_>, + > = Box::pin(collect_unregistered_store_findings( + global_db, + profile_root, + retention_secs, + observed_at_secs, + )); + let (registered_census, unregistered) = futures_util::join!(census, unregistered_findings); let (orphan_stores, incident_debris) = registered_census.as_deref().map_or( ( DoctorStorageFamilyReadV1::Unknown, diff --git a/crates/tracedecay-maintenance/src/retention/orphan_stores/collection.rs b/crates/tracedecay-maintenance/src/retention/orphan_stores/collection.rs index fd89e35d8c..d7c2f6d8d7 100644 --- a/crates/tracedecay-maintenance/src/retention/orphan_stores/collection.rs +++ b/crates/tracedecay-maintenance/src/retention/orphan_stores/collection.rs @@ -2,7 +2,7 @@ use std::future::{Future, poll_fn}; use std::path::{Path, PathBuf}; -use std::pin::pin; +use std::pin::{Pin, pin}; use std::task::Poll; use std::time::Instant; @@ -717,7 +717,10 @@ pub(super) async fn check_store_durable_memory( match durable_database_inventory(manifest_bytes, store_relpath, control) { DurableDatabaseInventoryV1::Interrupted => DurableMemoryCheck::Interrupted, DurableDatabaseInventoryV1::Resolved(relpath) => { - check_durable_memory_rows(profile_root, &relpath, scratch_root, control).await + let checked: Pin + Send + '_>> = Box::pin( + check_durable_memory_rows(profile_root, &relpath, scratch_root, control), + ); + checked.await } DurableDatabaseInventoryV1::Unverifiable => DurableMemoryCheck::Unverifiable, } @@ -903,7 +906,15 @@ pub(crate) async fn execute_unregistered_collection_controlled( outcome.completion = completion; break; } - let step = collect_unregistered_finding(db, finding, profile_root, control).await?; + let stepped: Pin< + Box> + Send + '_>, + > = Box::pin(collect_unregistered_finding( + db, + finding, + profile_root, + control, + )); + let step = stepped.await?; let store = CollectedStore { project_id: finding.project_dir_name.clone(), store_id: finding.project_dir_name.clone(), @@ -939,10 +950,10 @@ pub(super) async fn collect_unregistered_finding( ) { return Ok(step); } - match control - .race(db.code_project_exists(&finding.project_dir_name)) - .await - { + let registered: Pin< + Box> + Send + '_>, + > = Box::pin(db.code_project_exists(&finding.project_dir_name)); + match control.race(registered).await { Ok(exists) => { if exists? { return Ok(FindingStep::Refused(CollectionFailureKind::RegistryChanged)); @@ -963,18 +974,21 @@ pub(super) async fn collect_unregistered_finding( .join(tracedecay_runtime_core::storage::STORE_MANIFEST_FILENAME); let check = match read_regular_file(&manifest_path) { RegularFileSnapshot::Bytes(manifest_bytes) => { - check_store_durable_memory( - profile_root, - &finding.data_root, - Some(&manifest_bytes), - &scratch_root, - control, - ) - .await + let checked: Pin + Send + '_>> = + Box::pin(check_store_durable_memory( + profile_root, + &finding.data_root, + Some(&manifest_bytes), + &scratch_root, + control, + )); + checked.await } RegularFileSnapshot::Missing => { - check_manifestless_store_durable_memory(&finding.data_root, &scratch_root, control) - .await + let checked: Pin + Send + '_>> = Box::pin( + check_manifestless_store_durable_memory(&finding.data_root, &scratch_root, control), + ); + checked.await } RegularFileSnapshot::Unverifiable => DurableMemoryCheck::Unverifiable, }; @@ -1004,7 +1018,17 @@ pub(super) async fn collect_unregistered_finding( // The writer transaction orders this final absence proof after any // in-flight registration commit. - let transaction = match control.race(db.begin_write_transaction()).await { + let begun: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + tracedecay_global_db::RegisteredGlobalDbWriteTransaction<'_>, + >, + > + Send + + '_, + >, + > = Box::pin(db.begin_write_transaction()); + let transaction = match control.race(begun).await { Ok(transaction) => transaction?, Err(completion) => return Ok(FindingStep::Interrupted(completion)), }; @@ -1063,7 +1087,10 @@ async fn check_manifestless_store_durable_memory( if control.completion().is_some() { return DurableMemoryCheck::Interrupted; } - match check_durable_memory_rows(data_root, &relpath, scratch_root, control).await { + let checked: Pin + Send + '_>> = Box::pin( + check_durable_memory_rows(data_root, &relpath, scratch_root, control), + ); + match checked.await { DurableMemoryCheck::Empty => {} protected => return protected, } diff --git a/crates/tracedecay-maintenance/src/retention/orphan_stores/pages.rs b/crates/tracedecay-maintenance/src/retention/orphan_stores/pages.rs index 12fbf88245..905f343ba1 100644 --- a/crates/tracedecay-maintenance/src/retention/orphan_stores/pages.rs +++ b/crates/tracedecay-maintenance/src/retention/orphan_stores/pages.rs @@ -1,6 +1,8 @@ //! Census paging, store walks, and sweep reports for orphan-store retention. +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; use tracedecay_global_db::RegisteredGlobalDb; use tracedecay_runtime_core::cancellation::{CancellationToken, MonotonicDeadline}; @@ -159,8 +161,22 @@ pub async fn build_store_census( db: &RegisteredGlobalDb, profile_root: &Path, ) -> tracedecay_domain::errors::Result> { - let projects = db.list_code_projects(usize::MAX).await?; - build_store_census_for_projects(db, profile_root, &projects, None) + let listed: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Vec, + >, + > + Send + + '_, + >, + > = Box::pin(db.list_code_projects(usize::MAX)); + let projects = listed.await?; + let census = tracedecay_runtime_core::erase_send!( + build_store_census_for_projects(db, profile_root, &projects, None,), + tracedecay_domain::errors::Result>> + ); + census .await? .ok_or_else(|| tracedecay_domain::errors::TraceDecayError::Config { message: "unbounded store census was unexpectedly interrupted".to_owned(), @@ -223,7 +239,19 @@ async fn build_store_census_for_projects( Ok(Err(error)) => return Err(error), Err(_) => return Ok(None), }, - None => db.project_registry_contexts_for_projects(projects).await?, + None => { + let contexts: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Vec, + >, + > + Send + + '_, + >, + > = Box::pin(db.project_registry_contexts_for_projects(projects)); + contexts.await? + } }; for context in contexts { if control.is_some_and(|control| control.completion().is_some()) { @@ -246,8 +274,17 @@ async fn build_store_census_for_projects( Err(_) => return Ok(None), }, None => { - db.try_list_store_instances_for_project(&project.project_id) - .await? + let stores: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Vec, + >, + > + Send + + '_, + >, + > = Box::pin(db.try_list_store_instances_for_project(&project.project_id)); + stores.await? } }; for store in stores { @@ -639,7 +676,13 @@ pub async fn sweep_unregistered_stores( apply: bool, ) -> tracedecay_domain::errors::Result { let cancellation = CancellationToken::new(); - let report = sweep_unregistered_store_page( + let swept: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(sweep_unregistered_store_page( db, profile_root, UnregisteredStoreSweepRequestV1 { @@ -653,8 +696,8 @@ pub async fn sweep_unregistered_stores( std::time::Instant::now() + std::time::Duration::from_secs(5), ), }, - ) - .await?; + )); + let report = swept.await?; let completion_is_terminal = report.completion == UnregisteredSweepCompletionV1::Complete; let receipt_is_consistent = (!report.applied || apply && completion_is_terminal) && (completion_is_terminal || report.next_cursor.is_none()) diff --git a/crates/tracedecay-maintenance/src/retention/orphan_stores/unregistered_page.rs b/crates/tracedecay-maintenance/src/retention/orphan_stores/unregistered_page.rs index c3ebd56815..eb19105d3e 100644 --- a/crates/tracedecay-maintenance/src/retention/orphan_stores/unregistered_page.rs +++ b/crates/tracedecay-maintenance/src/retention/orphan_stores/unregistered_page.rs @@ -1,7 +1,9 @@ //! Bounded, resumable census and collection of unregistered project leaves. use std::collections::{HashMap, HashSet}; +use std::future::Future; use std::path::Path; +use std::pin::Pin; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; use std::time::Instant; @@ -96,16 +98,19 @@ pub async fn sweep_unregistered_store_page( { return Ok(interrupted_report(completion, CollectionOutcome::default())); } - let census = census_unregistered_project_dirs_page( - db, - profile_root, - request.cursor.as_deref(), - limit, - request.now, - request.cancellation, - request.deadline, - ) - .await?; + let census_page = tracedecay_runtime_core::erase_send!( + census_unregistered_project_dirs_page( + db, + profile_root, + request.cursor.as_deref(), + limit, + request.now, + request.cancellation, + request.deadline, + ), + tracedecay_domain::errors::Result, Option)>> + ); + let census = census_page.await?; let Some((findings, next_cursor)) = census else { return Ok(interrupted_report( UnregisteredSweepCompletionV1::interrupted(request.cancellation, request.deadline) @@ -134,13 +139,15 @@ pub async fn sweep_unregistered_store_page( completion, }); } - let outcome = execute_unregistered_collection_controlled( + let collected: Pin< + Box> + Send + '_>, + > = Box::pin(execute_unregistered_collection_controlled( db, &plan, profile_root, CollectionControl::new(request.cancellation, request.deadline), - ) - .await?; + )); + let outcome = collected.await?; let completion = match outcome.completion { CollectionCompletionV1::Complete => UnregisteredSweepCompletionV1::Complete, CollectionCompletionV1::Cancelled => UnregisteredSweepCompletionV1::Cancelled, diff --git a/crates/tracedecay-mcp-catalog/src/definitions/application_schema.rs b/crates/tracedecay-mcp-catalog/src/definitions/application_schema.rs index 8f8dbe4144..3cb274c9fa 100644 --- a/crates/tracedecay-mcp-catalog/src/definitions/application_schema.rs +++ b/crates/tracedecay-mcp-catalog/src/definitions/application_schema.rs @@ -169,8 +169,7 @@ pub(super) fn bound_tagged_union( let mut description = union .get("description") .and_then(Value::as_str) - .map(str::to_owned) - .unwrap_or_default(); + .map_or_default(str::to_owned); if !description.is_empty() { description.push(' '); } diff --git a/crates/tracedecay-mcp/src/handlers/dashboard_git_correlation.rs b/crates/tracedecay-mcp/src/handlers/dashboard_git_correlation.rs index 2102ca817c..546a958490 100644 --- a/crates/tracedecay-mcp/src/handlers/dashboard_git_correlation.rs +++ b/crates/tracedecay-mcp/src/handlers/dashboard_git_correlation.rs @@ -7,6 +7,8 @@ //! recorded Git evidence is the typed empty start, never an error. use std::collections::BTreeSet; +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; use tracedecay_dashboard_api::{ @@ -34,8 +36,18 @@ async fn read_sessions( session_ids: &BTreeSet, ) -> Result { let read_error = |detail: String| DashboardGitCorrelationReadErrorV1 { detail }; - let Some(evidence) = store - .git_evidence_for_sessions(session_ids) + let evidence: Pin< + Box< + dyn Future< + Output = std::result::Result< + Option, + tracedecay_sessions::runtime::git_correlation::GitCorrelationError, + >, + > + Send + + '_, + >, + > = Box::pin(store.git_evidence_for_sessions(session_ids)); + let Some(evidence) = evidence .await .map_err(|error| read_error(error.to_string()))? else { diff --git a/crates/tracedecay-mcp/src/handlers/dashboard_lcm.rs b/crates/tracedecay-mcp/src/handlers/dashboard_lcm.rs index 06cc42b3e0..26543e4e77 100644 --- a/crates/tracedecay-mcp/src/handlers/dashboard_lcm.rs +++ b/crates/tracedecay-mcp/src/handlers/dashboard_lcm.rs @@ -412,16 +412,13 @@ impl DashboardLcmReadAdapter { // anchor to hydrate from: the raw-store stats carry the // session's transcript counts instead. (Some(_), Some(raw_stats)) => { - let session_stats = raw_stats - .first() - .map(|stats| { - ( - stats.raw_message_count, - stats.unpublished_message_count, - stats.summary_node_count, - ) - }) - .unwrap_or_default(); + let session_stats = raw_stats.first().map_or_default(|stats| { + ( + stats.raw_message_count, + stats.unpublished_message_count, + stats.summary_node_count, + ) + }); DashboardLcmCanonicalStatsV1 { message_count: session_stats.0, unpublished_message_count: session_stats.1, diff --git a/crates/tracedecay-mcp/src/handlers/graph/search.rs b/crates/tracedecay-mcp/src/handlers/graph/search.rs index 91dc90b0c3..e9a1fd262c 100644 --- a/crates/tracedecay-mcp/src/handlers/graph/search.rs +++ b/crates/tracedecay-mcp/src/handlers/graph/search.rs @@ -473,8 +473,7 @@ pub(super) fn append_coverage_md(md: &mut Md, value: &Value) { fn display_unavailable_suffix(row: &Value) -> String { row.get("display_unavailable") .and_then(Value::as_str) - .map(|reason| format!(" · display unavailable: {reason}")) - .unwrap_or_default() + .map_or_default(|reason| format!(" · display unavailable: {reason}")) } /// One result row with its rendered markdown. diff --git a/crates/tracedecay-mcp/src/lifecycle.rs b/crates/tracedecay-mcp/src/lifecycle.rs index c323d7e418..4cda8a2f11 100644 --- a/crates/tracedecay-mcp/src/lifecycle.rs +++ b/crates/tracedecay-mcp/src/lifecycle.rs @@ -195,7 +195,7 @@ impl StartupCatchUpStateV1 { } fn take_tasks(&mut self) -> StartupCatchUpTasksV1 { - self.tasks_mut().map(std::mem::take).unwrap_or_default() + self.tasks_mut().map_or_default(std::mem::take) } } diff --git a/crates/tracedecay-mcp/src/tools/mod.rs b/crates/tracedecay-mcp/src/tools/mod.rs index 2cb8cdc67a..e1355337b7 100644 --- a/crates/tracedecay-mcp/src/tools/mod.rs +++ b/crates/tracedecay-mcp/src/tools/mod.rs @@ -151,8 +151,7 @@ pub fn render_tool_cli_help(def: &ToolDefinition) -> String { .input_schema .get("required") .and_then(Value::as_array) - .map(|arr| arr.iter().filter_map(Value::as_str).collect()) - .unwrap_or_default(); + .map_or_default(|arr| arr.iter().filter_map(Value::as_str).collect()); let Some(props) = props else { let _ = writeln!(out, "(no parameters)"); @@ -343,8 +342,7 @@ fn variant_label(variant: &Value) -> Option { let required: Vec<&str> = variant .get("required") .and_then(Value::as_array) - .map(|arr| arr.iter().filter_map(Value::as_str).collect()) - .unwrap_or_default(); + .map_or_default(|arr| arr.iter().filter_map(Value::as_str).collect()); (!required.is_empty()).then(|| format!("{{{}}}", required.join(", "))) } @@ -375,13 +373,12 @@ fn param_shape_note(schema: &Value, ty: &str) -> Option { let required: Vec = schema .get("required") .and_then(Value::as_array) - .map(|arr| { + .map_or_default(|arr| { arr.iter() .filter_map(Value::as_str) .map(str::to_owned) .collect() - }) - .unwrap_or_default(); + }); if !required.is_empty() { return Some(format!( "object with required keys: {}", @@ -466,8 +463,7 @@ fn example_from_object_schema(root: &Value, schema: &Value, depth: usize) -> Val let required: Vec<&str> = schema .get("required") .and_then(Value::as_array) - .map(|arr| arr.iter().filter_map(Value::as_str).collect()) - .unwrap_or_default(); + .map_or_default(|arr| arr.iter().filter_map(Value::as_str).collect()); example_object_value(root, props, &required, depth + 1) } diff --git a/crates/tracedecay-mcp/src/tools/renderers.rs b/crates/tracedecay-mcp/src/tools/renderers.rs index 5e07d76b81..4ebb8f610e 100644 --- a/crates/tracedecay-mcp/src/tools/renderers.rs +++ b/crates/tracedecay-mcp/src/tools/renderers.rs @@ -95,8 +95,7 @@ pub fn skill_view_md(value: &Value) -> String { let bytes = file .get("bytes") .and_then(Value::as_array) - .map(Vec::len) - .unwrap_or_default(); + .map_or_default(Vec::len); md.bullet(&format!("**{path}** - {bytes} bytes")); } } @@ -215,16 +214,13 @@ fn value_i64(value: &Value, pointer: &str) -> Option { } fn string_array(value: Option<&Value>) -> String { - value - .and_then(Value::as_array) - .map(|items| { - items - .iter() - .filter_map(Value::as_str) - .collect::>() - .join(", ") - }) - .unwrap_or_default() + value.and_then(Value::as_array).map_or_default(|items| { + items + .iter() + .filter_map(Value::as_str) + .collect::>() + .join(", ") + }) } fn append_skill_item(md: &mut Md, skill: &Value) { diff --git a/crates/tracedecay-project/src/config.rs b/crates/tracedecay-project/src/config.rs index 94e9166ac2..792857f27a 100644 --- a/crates/tracedecay-project/src/config.rs +++ b/crates/tracedecay-project/src/config.rs @@ -1,6 +1,10 @@ use std::collections::BTreeMap; +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; + use std::sync::{Arc, LazyLock, OnceLock, RwLock}; +use tracing::Instrument; use tracedecay_contracts::clock::now_micros; use tracedecay_domain::ProjectId; @@ -757,26 +761,32 @@ fn config_error(message: impl Into) -> TraceDecayError { } } -#[tracing::instrument(name = "daemon.config.discover", level = "trace", skip_all)] -pub async fn discover_project_root_with_identity( - profile: &tracedecay_runtime_core::config::ProfileRoot, - start: &Path, -) -> Option { - if let Some(root) = profile.discover_project_root(start) { - return Some(root); - } - let candidate = tracedecay_runtime_core::worktree::git_worktree_root(start) - .unwrap_or_else(|| start.to_path_buf()); - if crate::project::TraceDecay::has_initialized_store_with_options( - &candidate, - &crate::project::TraceDecayOpenOptions::for_profile(profile), +pub fn discover_project_root_with_identity<'a>( + profile: &'a tracedecay_runtime_core::config::ProfileRoot, + start: &'a Path, +) -> Pin> + Send + 'a>> { + let span = tracing::trace_span!("daemon.config.discover"); + Box::pin( + async move { + if let Some(root) = profile.discover_project_root(start) { + return Some(root); + } + let candidate = tracedecay_runtime_core::worktree::git_worktree_root(start) + .unwrap_or_else(|| start.to_path_buf()); + let options = crate::project::TraceDecayOpenOptions::for_profile(profile); + let initialized: Pin + Send + '_>> = Box::pin( + crate::project::TraceDecay::has_initialized_store_with_options( + &candidate, &options, + ), + ); + if initialized.await { + Some(candidate) + } else { + None + } + } + .instrument(span), ) - .await - { - Some(candidate) - } else { - None - } } #[cfg(test)] diff --git a/crates/tracedecay-project/src/project/lifecycle/identity.rs b/crates/tracedecay-project/src/project/lifecycle/identity.rs index 8e654bb2b8..632a8212de 100644 --- a/crates/tracedecay-project/src/project/lifecycle/identity.rs +++ b/crates/tracedecay-project/src/project/lifecycle/identity.rs @@ -1,7 +1,9 @@ //! Store-layout identity resolution: mapping a project root to its //! authoritative store layout. +use std::future::Future; use std::path::Path; +use std::pin::Pin; use tracedecay_domain::errors::{ProjectOpenFailureKind, Result, TraceDecayError}; use tracedecay_global_db::RegisteredGlobalDb; @@ -145,17 +147,20 @@ impl TraceDecay { { return Err(TraceDecayError::Config { message }); } - if let Some(registry_database) = registry_database - && let Some(layout) = + if let Some(registry_database) = registry_database { + let adopted: Pin< + Box>> + Send + '_>, + > = Box::pin( tracedecay_application::project_adoption::adopt_moved_nongit_project( project_root, &profile_root, registry_database, adoption, - ) - .await? - { - return Ok(layout); + ), + ); + if let Some(layout) = adopted.await? { + return Ok(layout); + } } storage::default_profile_sharded_layout(project_root, &profile_root) } @@ -244,14 +249,16 @@ impl TraceDecay { project_root: &Path, open_options: &TraceDecayOpenOptions, ) -> Result { - Self::resolve_store_layout_for_authority( - project_root, - open_options, - None, - true, - &MovedStoreAdoption::Never, - ) - .await + let adoption = MovedStoreAdoption::Never; + let resolved: Pin> + Send + '_>> = + Box::pin(Self::resolve_store_layout_for_authority( + project_root, + open_options, + None, + true, + &adoption, + )); + resolved.await } } diff --git a/crates/tracedecay-project/src/runtime_ports.rs b/crates/tracedecay-project/src/runtime_ports.rs index 3f94cf113d..21505e0029 100644 --- a/crates/tracedecay-project/src/runtime_ports.rs +++ b/crates/tracedecay-project/src/runtime_ports.rs @@ -201,10 +201,7 @@ fn resolve_project_root_with_identity<'a>( profile: &'a ProfileRoot, start: &'a Path, ) -> Pin> + Send + 'a>> { - Box::pin(tracing::Instrument::instrument( - crate::config::discover_project_root_with_identity(profile, start), - tracing::trace_span!("runtime_ports.resolve_project_root"), - )) + crate::config::discover_project_root_with_identity(profile, start) } #[tracing::instrument(name = "runtime_ports.resolve_hook_scope", level = "trace", skip_all)] diff --git a/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs b/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs index bb831e954b..e29f414971 100644 --- a/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs +++ b/crates/tracedecay-query/src/bin/tracedecay_search_bench.rs @@ -796,8 +796,7 @@ impl Scratch { fn create() -> Result { let unique = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|elapsed| elapsed.as_nanos()) - .unwrap_or_default(); + .map_or_default(|elapsed| elapsed.as_nanos()); let path = std::env::temp_dir().join(format!( "tracedecay-search-bench-{}-{unique:x}", std::process::id() diff --git a/crates/tracedecay-query/src/retrieval/exact.rs b/crates/tracedecay-query/src/retrieval/exact.rs index d6102036b4..aa6e8cbc31 100644 --- a/crates/tracedecay-query/src/retrieval/exact.rs +++ b/crates/tracedecay-query/src/retrieval/exact.rs @@ -421,8 +421,7 @@ fn spelled_identifiers(words: &[ExactQueryAtom]) -> [String; 3] { let mut chars = word.chars(); chars .next() - .map(|first| first.to_ascii_uppercase().to_string() + chars.as_str()) - .unwrap_or_default() + .map_or_default(|first| first.to_ascii_uppercase().to_string() + chars.as_str()) }; let snake = words .iter() diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader/family_report.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader/family_report.rs index def69abf8b..e10db436b7 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader/family_report.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader/family_report.rs @@ -122,18 +122,10 @@ impl CodeLexicalArtifactReaderV1 { let fetch = limit.checked_add(1).ok_or_else(|| { CodeLexicalArtifactErrorV1::Contract("clone family page limit overflowed".to_owned()) })?; - let after_reviewable = after - .map(|position| position.reviewable_source_bytes) - .unwrap_or_default(); - let after_members = after - .map(|position| position.member_count) - .unwrap_or_default(); - let after_class = after - .map(|position| position.class as u8) - .unwrap_or_default(); - let after_revision = after - .map(|position| position.normalization_revision) - .unwrap_or_default(); + let after_reviewable = after.map_or_default(|position| position.reviewable_source_bytes); + let after_members = after.map_or_default(|position| position.member_count); + let after_class = after.map_or_default(|position| position.class as u8); + let after_revision = after.map_or_default(|position| position.normalization_revision); let after_digest = after .map(|position| digest_key(&position.digest)) .transpose()? diff --git a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader/query.rs b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader/query.rs index 8ae495521b..0ee1e6f516 100644 --- a/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader/query.rs +++ b/crates/tracedecay-query/src/retrieval/lexical/projection/artifact/reader/query.rs @@ -1535,21 +1535,19 @@ impl<'a> ArtifactQueryV1<'a> { field_texts .iter() .find(|(matched, _)| *matched == field) - .map(|(_, text)| substring_count(text, phrase)) - .unwrap_or(0) + .map_or(0, |(_, text)| substring_count(text, phrase)) }, |field, proximity| { field_tokens .iter() .find(|(matched, _)| *matched == field) - .map(|(_, tokens)| { + .map_or(0, |(_, tokens)| { proximity_count_tokens( tokens, &proximity.terms, proximity.original.maximum_gap, ) }) - .unwrap_or(0) }, !prepared.echo_query.is_empty() && prepared.echo_query == row.normalized_text().trim(), diff --git a/crates/tracedecay-runtime-core/src/cancellation.rs b/crates/tracedecay-runtime-core/src/cancellation.rs index 7813384982..1c3695f5be 100644 --- a/crates/tracedecay-runtime-core/src/cancellation.rs +++ b/crates/tracedecay-runtime-core/src/cancellation.rs @@ -50,6 +50,24 @@ struct CancellationScope { changed: Arc, } +// `parent` is `Arc`. Auto-trait proof does not cache that cycle and +// overflows (rust-lang/rust#159228). The parent field below stops at this +// impl; the context is checked on its own. +unsafe impl Send for CancellationScope {} +unsafe impl Sync for CancellationScope {} + +const _: fn(&CancellationScope) = |value| { + fn assert_send_sync(_: &T) {} + let CancellationScope { + context, + parent, + changed, + } = value; + assert_send_sync(context); + assert_send_sync(parent); + assert_send_sync(changed); +}; + impl Default for CancellationToken { fn default() -> Self { Self { diff --git a/crates/tracedecay-runtime-core/src/db/connection.rs b/crates/tracedecay-runtime-core/src/db/connection.rs index 48189e91b2..a6c0478c64 100644 --- a/crates/tracedecay-runtime-core/src/db/connection.rs +++ b/crates/tracedecay-runtime-core/src/db/connection.rs @@ -63,6 +63,18 @@ pub struct Database { client: Arc, } +// A combined `Send` proof of this client walks the registry attachment past +// the recursion limit (rust-lang/rust#159228). Each field is checked alone. +unsafe impl Send for Database {} +unsafe impl Sync for Database {} + +const _: fn(&Database) = |value| { + fn assert_send_sync(_: &T) {} + let Database { inner, client } = value; + assert_send_sync(inner); + assert_send_sync(client); +}; + #[derive(Clone)] pub struct WeakDatabase { inner: Weak, diff --git a/crates/tracedecay-runtime-core/src/db/connection/query_write.rs b/crates/tracedecay-runtime-core/src/db/connection/query_write.rs index 61423ef396..1fb101f95d 100644 --- a/crates/tracedecay-runtime-core/src/db/connection/query_write.rs +++ b/crates/tracedecay-runtime-core/src/db/connection/query_write.rs @@ -154,16 +154,29 @@ impl Database { /// /// Writable handles opened for the same database share one `DatabaseInner`, /// so this guard coordinates MCP, dashboard, and automation mutations. - pub async fn writer(&self) -> Result> { - let cx = crate::native_runtime::cx().ok_or_else(|| TraceDecayError::Config { - message: "database writer admission requires an Asupersync context".to_owned(), - })?; - asupersync::sync::OwnedMutexGuard::lock(std::sync::Arc::clone(&self.inner.writer), &cx) - .await - .map_err(|error| TraceDecayError::Database { - message: format!("database writer admission failed: {error}"), - operation: "acquire database writer lane".to_owned(), - }) + /// Boxed: the context and lock wait sit dozens of levels deep, and every + /// write future would otherwise repeat that `Send` proof + /// (rust-lang/rust#159228). + pub fn writer( + &self, + ) -> std::pin::Pin< + Box< + dyn std::future::Future>> + + Send + + '_, + >, + > { + Box::pin(async move { + let cx = crate::native_runtime::cx().ok_or_else(|| TraceDecayError::Config { + message: "database writer admission requires an Asupersync context".to_owned(), + })?; + asupersync::sync::OwnedMutexGuard::lock(std::sync::Arc::clone(&self.inner.writer), &cx) + .await + .map_err(|error| TraceDecayError::Database { + message: format!("database writer admission failed: {error}"), + operation: "acquire database writer lane".to_owned(), + }) + }) } pub(super) fn require_active_write_scope(&self, operation: &str) -> Result<()> { diff --git a/crates/tracedecay-runtime-core/src/db/connection/registry.rs b/crates/tracedecay-runtime-core/src/db/connection/registry.rs index a6faea1fe4..071f5d0b7b 100644 --- a/crates/tracedecay-runtime-core/src/db/connection/registry.rs +++ b/crates/tracedecay-runtime-core/src/db/connection/registry.rs @@ -278,6 +278,39 @@ pub(super) struct DatabaseInner { std::sync::Mutex>, } +// One `Send` proof of every attachment field exceeds the recursion limit +// (rust-lang/rust#159228). Each field below is a separate proof. +unsafe impl Send for DatabaseInner {} +unsafe impl Sync for DatabaseInner {} + +const _: fn(&DatabaseInner) = |value| { + fn assert_send_sync(_: &T) {} + let DatabaseInner { + conn, + write_conn, + runtime, + opened_file_identity, + writer, + memory_graph_reconciliation, + memory_graph_reconciliation_telemetry, + canonical_path, + _authority, + memory_graph_runtime, + memory_graph_source_watermark, + } = value; + assert_send_sync(conn); + assert_send_sync(write_conn); + assert_send_sync(runtime); + assert_send_sync(opened_file_identity); + assert_send_sync(writer); + assert_send_sync(memory_graph_reconciliation); + assert_send_sync(memory_graph_reconciliation_telemetry); + assert_send_sync(canonical_path); + assert_send_sync(_authority); + assert_send_sync(memory_graph_runtime); + assert_send_sync(memory_graph_source_watermark); +}; + impl DatabaseInner { /// Publishes an already-open canonical registry runtime without reopening /// the `SQLite` path. diff --git a/crates/tracedecay-runtime-core/src/db/engine/connection.rs b/crates/tracedecay-runtime-core/src/db/engine/connection.rs index 289d7fcc39..5d51adb1f4 100644 --- a/crates/tracedecay-runtime-core/src/db/engine/connection.rs +++ b/crates/tracedecay-runtime-core/src/db/engine/connection.rs @@ -178,15 +178,10 @@ impl Connection { let statement = statement(sql, params)?; let runtime = Arc::clone(&self.runtime); let priority = self.read_priority; - let cx = crate::native_runtime::cx().ok_or(Error::RuntimeContextUnavailable)?; - let rows = cx - .spawn_blocking(move |_cx| { - runtime.query_with_priority(statement, priority, READER_WAIT) - }) - .map_err(|error| Error::Runtime(format!("failed to admit SQL reader: {error}")))? - .join(&cx) - .await - .map_err(join_error)??; + let rows = run_blocking("SQL reader", join_error, move || { + runtime.query_with_priority(statement, priority, READER_WAIT) + }) + .await??; Ok(Rows::from_exact(rows)) } @@ -208,12 +203,12 @@ impl Connection { pub async fn release_connection_memory(&self) -> Result { let runtime = Arc::clone(&self.runtime); - let owner = crate::native_runtime::handle().ok_or(Error::RuntimeContextUnavailable)?; - let settlement = owner + let settlement = crate::native_runtime::handle() + .ok_or(Error::RuntimeContextUnavailable)? .try_request_cx_with_budget(Budget::INFINITE) .map_err(|error| Error::Runtime(format!("failed to admit memory release: {error}")))?; let mut task = settlement - .spawn(move |_cx| async move { runtime.release_connection_memory_async(&owner).await }) + .spawn(move |cx| async move { runtime.release_connection_memory_async(&cx).await }) .map_err(|error| Error::Runtime(format!("failed to spawn memory release: {error}")))?; std::future::poll_fn(|context| task.poll_join(context)) .await @@ -242,30 +237,22 @@ impl Connection { pub async fn read_snapshot(&self) -> Result { let runtime = Arc::clone(&self.runtime); let priority = self.read_priority; - let cx = crate::native_runtime::cx().ok_or(Error::RuntimeContextUnavailable)?; - cx.spawn_blocking(move |_cx| { + run_blocking("SQL snapshot", join_error, move || { runtime.begin_read_snapshot_with_priority(priority, READER_WAIT) }) - .map_err(|error| Error::Runtime(format!("failed to admit SQL snapshot: {error}")))? - .join(&cx) - .await - .map_err(join_error)? + .await? .map(ReadSnapshot::from_runtime) .map_err(Into::into) } pub(crate) async fn health_read_snapshot(&self) -> Result { let runtime = Arc::clone(&self.runtime); - let cx = crate::native_runtime::cx().ok_or(Error::RuntimeContextUnavailable)?; - cx.spawn_blocking(move |_cx| runtime.begin_health_read_snapshot(HEALTH_READER_WAIT)) - .map_err(|error| { - Error::Runtime(format!("failed to admit SQL health snapshot: {error}")) - })? - .join(&cx) - .await - .map_err(join_error)? - .map(ReadSnapshot::from_runtime) - .map_err(Into::into) + run_blocking("SQL health snapshot", join_error, move || { + runtime.begin_health_read_snapshot(HEALTH_READER_WAIT) + }) + .await? + .map(ReadSnapshot::from_runtime) + .map_err(Into::into) } #[cfg(any(test, feature = "test-helpers"))] @@ -324,6 +311,25 @@ fn join_error(error: asupersync::runtime::JoinError) -> super::Error { super::Error::Runtime(format!("exact SQL worker task failed: {error}")) } +/// Runs `work` on the native blocking pool and joins it through a request +/// context. Boxed: a future that holds the context across the join sits +/// dozens of levels deep, and every SQL caller would repeat that `Send` proof +/// (rust-lang/rust#159228). +pub(super) fn run_blocking( + admission: &'static str, + join_failure: fn(asupersync::runtime::JoinError) -> Error, + work: impl FnOnce() -> T + Send + 'static, +) -> std::pin::Pin> + Send>> { + Box::pin(async move { + let cx = crate::native_runtime::cx().ok_or(Error::RuntimeContextUnavailable)?; + cx.spawn_blocking(move |_cx| work()) + .map_err(|error| Error::Runtime(format!("failed to admit {admission}: {error}")))? + .join(&cx) + .await + .map_err(join_failure) + }) +} + pub(super) fn statement

(sql: &str, params: P) -> Result where P: IntoParams, diff --git a/crates/tracedecay-runtime-core/src/db/engine/snapshot.rs b/crates/tracedecay-runtime-core/src/db/engine/snapshot.rs index 1c9de01bea..d256839e75 100644 --- a/crates/tracedecay-runtime-core/src/db/engine/snapshot.rs +++ b/crates/tracedecay-runtime-core/src/db/engine/snapshot.rs @@ -35,19 +35,12 @@ impl ReadSnapshot { let runtime = Arc::clone(&self.runtime); let budget = self.budget; let statement = statement(sql, params)?; - let cx = crate::native_runtime::cx().ok_or(super::Error::RuntimeContextUnavailable)?; - let rows = cx - .spawn_blocking(move |_cx| { - runtime - .query_with_budget(statement, budget) - .map_err(super::Error::from) - }) - .map_err(|error| { - super::Error::Runtime(format!("failed to admit SQL snapshot query: {error}")) - })? - .join(&cx) - .await - .map_err(join_error)??; + let rows = super::connection::run_blocking("SQL snapshot query", join_error, move || { + runtime + .query_with_budget(statement, budget) + .map_err(super::Error::from) + }) + .await??; Ok(Rows::from_exact(rows)) } } diff --git a/crates/tracedecay-runtime-core/src/lib.rs b/crates/tracedecay-runtime-core/src/lib.rs index 7db20c7705..560bba33af 100644 --- a/crates/tracedecay-runtime-core/src/lib.rs +++ b/crates/tracedecay-runtime-core/src/lib.rs @@ -88,6 +88,18 @@ #![allow(rustdoc::broken_intra_doc_links)] #![allow(rustdoc::private_intra_doc_links)] +/// Coerce one concrete future to `dyn Future + Send` where this macro expands. +/// A generic function proves `Send` in the caller, so the overflow stays put. +#[macro_export] +macro_rules! erase_send { + ($future:expr, $output:ty) => {{ + let erased: ::std::pin::Pin< + ::std::boxed::Box + Send + '_>, + > = ::std::boxed::Box::pin($future); + erased + }}; +} + /// Upper bound for daemon-owned shutdown persistence work. pub const DAEMON_SHUTDOWN_DEADLINE: std::time::Duration = std::time::Duration::from_secs(45); /// Grace retained for forced task abort and join during daemon shutdown. diff --git a/crates/tracedecay-runtime-core/src/shard_runtime/registry/ports.rs b/crates/tracedecay-runtime-core/src/shard_runtime/registry/ports.rs index f94bbf6b15..78a668ae49 100644 --- a/crates/tracedecay-runtime-core/src/shard_runtime/registry/ports.rs +++ b/crates/tracedecay-runtime-core/src/shard_runtime/registry/ports.rs @@ -130,21 +130,24 @@ impl ShardRuntimePublisher for LifecycleShardRuntimePublisher { ) -> StoreRuntimeRegistryFuture<'_, Result> { Box::pin(async move { - let executor = crate::native_runtime::handle().ok_or( - StoreRuntimeRegistryFailure::RuntimeUnavailable { - operation: "publish physical registered runtime", - }, - )?; - let admission = AdmissionConfigV1::default(); let pinned_profile = matches!(request.binding.shard_id.scope, StoreShardScopeV1::Profile); let runtime = ShardRuntime::new(request.binding.clone(), pinned_profile); runtime .transition(RuntimeMaintenanceStateV1::Opening) .map_err(runtime_lifecycle_failure)?; + // A `RuntimeHandle` held across the await overflows the `Send` + // proof (rust-lang/rust#159228), so it drops before publication. let attachment = LifecycleShardRuntimeAttachment::new(RepositoryPhysicalAttachmentFactory); - let physical = attachment.attach(&executor, &request, admission)?; + let physical = { + let executor = crate::native_runtime::handle().ok_or( + StoreRuntimeRegistryFailure::RuntimeUnavailable { + operation: "publish physical registered runtime", + }, + )?; + attachment.attach(&executor, &request, AdmissionConfigV1::default())? + }; publish_lifecycle_runtime(request, runtime, physical).await }) } diff --git a/crates/tracedecay-runtime-core/src/worktree.rs b/crates/tracedecay-runtime-core/src/worktree.rs index 01fa62e816..3396d7e96f 100644 --- a/crates/tracedecay-runtime-core/src/worktree.rs +++ b/crates/tracedecay-runtime-core/src/worktree.rs @@ -286,9 +286,8 @@ fn realpath(p: &Path) -> Option { #[cfg(test)] fn git_command() -> std::process::Command { let mut command = std::process::Command::new("git"); - let paths: Vec = std::env::var_os("PATH") - .map(|path| std::env::split_paths(&path).collect()) - .unwrap_or_default(); + let paths: Vec = + std::env::var_os("PATH").map_or_default(|path| std::env::split_paths(&path).collect()); #[cfg(not(windows))] let paths = { let mut paths = paths; diff --git a/crates/tracedecay-runtime-core/tests/sqlite_foreign_snapshot_cost.rs b/crates/tracedecay-runtime-core/tests/sqlite_foreign_snapshot_cost.rs index 3e8d1c82b9..04e421ccbe 100644 --- a/crates/tracedecay-runtime-core/tests/sqlite_foreign_snapshot_cost.rs +++ b/crates/tracedecay-runtime-core/tests/sqlite_foreign_snapshot_cost.rs @@ -30,9 +30,7 @@ fn written_bytes() -> u64 { } fn file_bytes(path: &Path) -> u64 { - fs::metadata(path) - .map(|metadata| metadata.len()) - .unwrap_or(0) + fs::metadata(path).map_or(0, |metadata| metadata.len()) } fn sidecar(path: &Path, suffix: &str) -> std::path::PathBuf { diff --git a/crates/tracedecay-rusqlite-runtime/src/exact_sql/guard.rs b/crates/tracedecay-rusqlite-runtime/src/exact_sql/guard.rs index acf8e46e8a..b1c0037f38 100644 --- a/crates/tracedecay-rusqlite-runtime/src/exact_sql/guard.rs +++ b/crates/tracedecay-rusqlite-runtime/src/exact_sql/guard.rs @@ -211,11 +211,9 @@ where { let deadline = if enforce_statement_limit { let operation_deadline = clock::now() + EXACT_SQL_EXECUTION_LIMIT; - Some( - execution_deadline - .map(|deadline| deadline.min(operation_deadline)) - .unwrap_or(operation_deadline), - ) + Some(execution_deadline.map_or(operation_deadline, |deadline| { + deadline.min(operation_deadline) + })) } else { execution_deadline }; diff --git a/crates/tracedecay-rusqlite-runtime/src/exact_sql/mod.rs b/crates/tracedecay-rusqlite-runtime/src/exact_sql/mod.rs index fc14bc2c01..486440e3d2 100644 --- a/crates/tracedecay-rusqlite-runtime/src/exact_sql/mod.rs +++ b/crates/tracedecay-rusqlite-runtime/src/exact_sql/mod.rs @@ -8,7 +8,7 @@ use std::{ time::{Duration, Instant}, }; -use asupersync::{Budget, channel::mpsc as actor_mpsc, runtime::RuntimeHandle}; +use asupersync::{Cx, channel::mpsc as actor_mpsc}; use rusqlite::{Connection, TransactionBehavior, params_from_iter}; use tracedecay_store::{ OperationPriorityV1, StoreRuntimeBindingV1, UnavailableReasonV1, VerifiedStoreLocatorV1, @@ -500,17 +500,14 @@ impl ExactSqlHandle { /// awaits the same serialized command protocol as ordinary writes. pub async fn release_connection_memory_async( &self, - runtime: &RuntimeHandle, + cx: &Cx, ) -> Result { let release_readers = Arc::clone(&self.release_reader_memory); - let cx = runtime - .try_request_cx_with_budget(Budget::INFINITE) - .map_err(|error| ExactSqlError::ReaderUnavailable(error.to_string()))?; let mut task = cx .spawn_blocking(move |_| release_readers()) .map_err(|error| ExactSqlError::ReaderUnavailable(error.to_string()))?; let readers = task - .join(&cx) + .join(cx) .await .map_err(|error| ExactSqlError::ReaderUnavailable(error.to_string()))??; let writer = if self.writer.is_some() { diff --git a/crates/tracedecay-rusqlite-runtime/src/native_runtime.rs b/crates/tracedecay-rusqlite-runtime/src/native_runtime.rs index fb5fbef766..ea274ff2d2 100644 --- a/crates/tracedecay-rusqlite-runtime/src/native_runtime.rs +++ b/crates/tracedecay-rusqlite-runtime/src/native_runtime.rs @@ -10,8 +10,7 @@ //! executor never blocks on the native runtime. use std::collections::BTreeMap; -use std::future::poll_fn; -use std::pin::{Pin, pin}; +use std::pin::Pin; use std::sync::{Mutex, OnceLock, PoisonError}; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; @@ -75,19 +74,23 @@ pub fn block_on(future: F) -> Result { /// is never polled still runs. Work spawned through a request `Cx` can instead /// be cancelled with that region before it starts, which would strand anything /// the caller had already queued for it. -pub fn spawn_blocking(work: F) -> impl Future> + Send + 'static +/// +/// Boxed so callers prove the context and channel wait `Send` once, here, +/// instead of inside every future that awaits blocking work +/// (rust-lang/rust#159228). +pub fn spawn_blocking(work: F) -> BoxFuture<'static, Result> where T: Send + 'static, F: FnOnce() -> T + Send + 'static, { let queued = queue_blocking(work); - async move { + Box::pin(async move { let (cx, mut receiver) = queued?; receiver .recv(&cx) .await .map_err(|error| format!("native blocking work ended without a result: {error}")) - } + }) } fn queue_blocking(work: F) -> Result<(Cx, asupersync::channel::oneshot::Receiver), String> @@ -158,20 +161,25 @@ impl Drop for Settlement { /// Completes at `deadline` on a native timer task, so a foreign executor never /// polls an Asupersync timer: its wait wakes through the join's `Waker`. /// Dropping the returned future cancels the timer. -pub async fn sleep_until(deadline: Instant) -> Result<(), String> { - let cx = cx().ok_or("native execution context unavailable")?; - let mut timer = cx - .spawn(move |cx| async move { - // Measured before the timer clock is read, so the wake never lands - // before `deadline`. - let remaining = deadline.saturating_duration_since(Instant::now()); - asupersync::time::sleep(cx.now(), remaining).await; - }) - .map_err(|error| format!("native timer was not admitted: {error}"))?; - timer - .join(&cx) - .await - .map_err(|error| format!("native timer ended without firing: {error}")) +/// +/// Boxed so every timed caller proves this future `Send` once, here, instead +/// of nesting the timer task inside its own proof (rust-lang/rust#159228). +pub fn sleep_until(deadline: Instant) -> BoxFuture<'static, Result<(), String>> { + Box::pin(async move { + let cx = cx().ok_or("native execution context unavailable")?; + let mut timer = cx + .spawn(move |cx| async move { + // Measured before the timer clock is read, so the wake never + // lands before `deadline`. + let remaining = deadline.saturating_duration_since(Instant::now()); + asupersync::time::sleep(cx.now(), remaining).await; + }) + .map_err(|error| format!("native timer was not admitted: {error}"))?; + timer + .join(&cx) + .await + .map_err(|error| format!("native timer ended without firing: {error}")) + }) } /// Why [`timeout_at`] stopped waiting for its future. @@ -188,37 +196,53 @@ pub enum TimeoutError { /// Awaits `future` until `deadline`, timed by [`sleep_until`]. The future is /// polled first, so one that is already ready wins even past its deadline and /// never starts a timer. -pub async fn timeout_at( - deadline: Instant, - future: F, -) -> Result { - race_timer(future, sleep_until(deadline)).await +pub fn timeout_at(deadline: Instant, future: F) -> Timeout { + race_timer(future, Some(sleep_until(deadline))) } /// [`timeout_at`] `duration` from now. A duration past the clock's range waits /// without a deadline. -pub async fn timeout(duration: Duration, future: F) -> Result { - match Instant::now().checked_add(duration) { - Some(deadline) => timeout_at(deadline, future).await, - None => Ok(future.await), - } +pub fn timeout(duration: Duration, future: F) -> Timeout { + race_timer( + future, + Instant::now().checked_add(duration).map(sleep_until), + ) } -async fn race_timer( +fn race_timer( future: F, - timer: impl Future>, -) -> Result { - let mut future = pin!(future); - let mut timer = pin!(timer); - poll_fn(|task| { - if let Poll::Ready(output) = future.as_mut().poll(task) { + timer: Option>>, +) -> Timeout { + Timeout { + future: Box::pin(future), + timer, + } +} + +/// The future [`timeout_at`] and [`timeout`] return. +/// +/// A named future over boxed fields, not an `async fn`, so a timed caller's +/// `Send` proof crosses two pointers instead of three nested coroutines +/// (rust-lang/rust#159228). +pub struct Timeout { + future: Pin>, + timer: Option>>, +} + +impl Future for Timeout { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, task: &mut Context<'_>) -> Poll { + if let Poll::Ready(output) = self.future.as_mut().poll(task) { return Poll::Ready(Ok(output)); } + let Some(timer) = self.timer.as_mut() else { + return Poll::Pending; + }; timer.as_mut().poll(task).map(|fired| { Err(fired.map_or_else(TimeoutError::TimerUnavailable, |()| TimeoutError::Elapsed)) }) - }) - .await + } } /// How a [`NativeTask`] ended. @@ -251,6 +275,9 @@ impl NativeTask { F: Future + Send + 'static, { let runtime = handle().ok_or("native execution runtime unavailable")?; + // Boxed so the runtime's own spawn wrappers do not nest `work` in + // their `Send` proof (rust-lang/rust#159228). + let work: BoxFuture<'static, T> = Box::pin(work); let (work, abort) = abortable(work); let join = runtime .try_spawn_checked(work) @@ -457,11 +484,11 @@ mod tests { fn an_unavailable_timer_fails_closed_after_a_ready_value() { let unavailable = || async { Err::<(), _>("runtime stopped".to_owned()) }; assert_eq!( - race_timer(async { 7 }, unavailable()).now_or_never(), + race_timer(async { 7 }, Some(Box::pin(unavailable()))).now_or_never(), Some(Ok(7)) ); assert_eq!( - race_timer(std::future::pending::<()>(), unavailable()).now_or_never(), + race_timer(std::future::pending::<()>(), Some(Box::pin(unavailable()))).now_or_never(), Some(Err(TimeoutError::TimerUnavailable( "runtime stopped".to_owned() ))) diff --git a/crates/tracedecay-rusqlite-runtime/src/repository/attachment.rs b/crates/tracedecay-rusqlite-runtime/src/repository/attachment.rs index 906cc1d40a..9f37f67c3a 100644 --- a/crates/tracedecay-rusqlite-runtime/src/repository/attachment.rs +++ b/crates/tracedecay-rusqlite-runtime/src/repository/attachment.rs @@ -596,11 +596,9 @@ impl RepositoryRuntimePhysicalAttachment { break; } if started.elapsed() >= ATTACHMENT_DRAIN_TIMEOUT { - let queued = state - .writer - .as_ref() - .map(|writer| writer.telemetry_snapshot().queue.queued_operations) - .unwrap_or(0); + let queued = state.writer.as_ref().map_or(0, |writer| { + writer.telemetry_snapshot().queue.queued_operations + }); let leased = state.readers.as_ref().map_or(0, |readers| { let snapshot = readers.snapshot(); u32::from(snapshot.leased_general) + u32::from(snapshot.leased_health) diff --git a/crates/tracedecay-rusqlite-runtime/src/telemetry/store_size.rs b/crates/tracedecay-rusqlite-runtime/src/telemetry/store_size.rs index 7443e92c24..6b9fb50126 100644 --- a/crates/tracedecay-rusqlite-runtime/src/telemetry/store_size.rs +++ b/crates/tracedecay-rusqlite-runtime/src/telemetry/store_size.rs @@ -113,16 +113,22 @@ impl SqliteStoreSizeTelemetryPort { store: &'a StoreKeyV1, observation: TableGrowthObservation, ) -> StorageTelemetryFuture<'a, TableGrowthTelemetryReadV1> { + // A future that borrows `self` proves `RuntimeHandle: Sync`, which + // overflows the recursion limit (rust-lang/rust#159228). + let cx = self + .admits(context, store) + .then(|| self.runtime.try_request_cx_with_budget(Budget::INFINITE)); + let handle = self.handle.clone(); + let reader_wait = self.reader_wait; + let table_watermarks = &self.table_watermarks; Box::pin(async move { - if !self.admits(context, store) { + let Some(cx) = cx else { return TableGrowthTelemetryReadV1::Denied { store: store.clone(), }; - } - let handle = self.handle.clone(); - let reader_wait = self.reader_wait; + }; let context = context.clone(); - let Ok(cx) = self.runtime.try_request_cx_with_budget(Budget::INFINITE) else { + let Ok(cx) = cx else { return TableGrowthTelemetryReadV1::Unknown { store: store.clone(), }; @@ -149,8 +155,7 @@ impl SqliteStoreSizeTelemetryPort { }; current_tables.insert(table, StorageByteSizeV1(sample.bytes)); } - let mut watermarks = self - .table_watermarks + let mut watermarks = table_watermarks .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); compare_table_growth( @@ -170,16 +175,20 @@ impl StoreSizeTelemetryPort for SqliteStoreSizeTelemetryPort { context: &'a RequestContext, store: &'a StoreKeyV1, ) -> StorageTelemetryFuture<'a, StorageTelemetryReadV1> { + // Same `RuntimeHandle: Sync` overflow as `read_table_growth`. + let cx = self + .admits(context, store) + .then(|| self.runtime.try_request_cx_with_budget(Budget::INFINITE)); + let handle = self.handle.clone(); + let reader_wait = self.reader_wait; Box::pin(async move { - if !self.admits(context, store) { + let Some(cx) = cx else { return StorageTelemetryReadV1::Denied { store: store.clone(), }; - } - let handle = self.handle.clone(); - let reader_wait = self.reader_wait; + }; let context = context.clone(); - let Ok(cx) = self.runtime.try_request_cx_with_budget(Budget::INFINITE) else { + let Ok(cx) = cx else { return StorageTelemetryReadV1::Unknown { store: store.clone(), }; diff --git a/crates/tracedecay-rusqlite-runtime/src/work/owner_observation.rs b/crates/tracedecay-rusqlite-runtime/src/work/owner_observation.rs index 8eb308db53..187263ed1d 100644 --- a/crates/tracedecay-rusqlite-runtime/src/work/owner_observation.rs +++ b/crates/tracedecay-rusqlite-runtime/src/work/owner_observation.rs @@ -54,40 +54,30 @@ impl WorkOwnerObservationStoragePortV1 for WorkSqliteStorage { worktree_id, actor_id, policy_digest LIMIT ?9", vec![ - after - .map(|cursor| ExactSqlValue::Integer(cursor.ordered_at_micros)) - .unwrap_or(ExactSqlValue::Null), - after - .map(|cursor| ExactSqlValue::Text(kind_text(cursor.kind).to_owned())) - .unwrap_or(ExactSqlValue::Null), - after - .map(|cursor| ExactSqlValue::Text(cursor.command_id.as_str().to_owned())) - .unwrap_or(ExactSqlValue::Null), - after - .map(|cursor| { - ExactSqlValue::Text(cursor.authority.project_id().as_str().to_owned()) - }) - .unwrap_or(ExactSqlValue::Null), - after - .map(|cursor| { - ExactSqlValue::Text(cursor.authority.repository_id().as_str().to_owned()) - }) - .unwrap_or(ExactSqlValue::Null), - after - .map(|cursor| { - ExactSqlValue::Text(cursor.authority.worktree_id().as_str().to_owned()) - }) - .unwrap_or(ExactSqlValue::Null), - after - .map(|cursor| { - ExactSqlValue::Text(cursor.authority.actor_id().as_str().to_owned()) - }) - .unwrap_or(ExactSqlValue::Null), - after - .map(|cursor| { - ExactSqlValue::Text(cursor.authority.policy_digest().as_str().to_owned()) - }) - .unwrap_or(ExactSqlValue::Null), + after.map_or(ExactSqlValue::Null, |cursor| { + ExactSqlValue::Integer(cursor.ordered_at_micros) + }), + after.map_or(ExactSqlValue::Null, |cursor| { + ExactSqlValue::Text(kind_text(cursor.kind).to_owned()) + }), + after.map_or(ExactSqlValue::Null, |cursor| { + ExactSqlValue::Text(cursor.command_id.as_str().to_owned()) + }), + after.map_or(ExactSqlValue::Null, |cursor| { + ExactSqlValue::Text(cursor.authority.project_id().as_str().to_owned()) + }), + after.map_or(ExactSqlValue::Null, |cursor| { + ExactSqlValue::Text(cursor.authority.repository_id().as_str().to_owned()) + }), + after.map_or(ExactSqlValue::Null, |cursor| { + ExactSqlValue::Text(cursor.authority.worktree_id().as_str().to_owned()) + }), + after.map_or(ExactSqlValue::Null, |cursor| { + ExactSqlValue::Text(cursor.authority.actor_id().as_str().to_owned()) + }), + after.map_or(ExactSqlValue::Null, |cursor| { + ExactSqlValue::Text(cursor.authority.policy_digest().as_str().to_owned()) + }), ExactSqlValue::Integer(i64::from(limit.get())), ], ) diff --git a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs index e082a9ce17..125886f2ef 100644 --- a/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs +++ b/crates/tracedecay-rusqlite-runtime/src/work_attempt.rs @@ -242,8 +242,7 @@ impl WorkAttemptStoragePort for WorkSqliteStorage { ExactSqlValue::Integer(i64::from(next.is_terminal())), ExactSqlValue::Text(payload), evidence_payload - .map(ExactSqlValue::Text) - .unwrap_or(ExactSqlValue::Null), + .map_or(ExactSqlValue::Null, ExactSqlValue::Text), ExactSqlValue::Text( expected_fence.lease_id().as_str().to_owned(), ), diff --git a/crates/tracedecay-rusqlite-runtime/src/writer/transaction.rs b/crates/tracedecay-rusqlite-runtime/src/writer/transaction.rs index 534ad7a865..d3391acf42 100644 --- a/crates/tracedecay-rusqlite-runtime/src/writer/transaction.rs +++ b/crates/tracedecay-rusqlite-runtime/src/writer/transaction.rs @@ -260,14 +260,14 @@ pub(super) fn process_batch( let lock_held = lock_held_from.elapsed(); record_transaction( telemetry, - commit_failure - .as_ref() - .map(transaction_outcome) - .unwrap_or(if has_pending_writes { + commit_failure.as_ref().map_or( + if has_pending_writes { WriterTransactionOutcome::Committed } else { WriterTransactionOutcome::RolledBack - }), + }, + transaction_outcome, + ), command_count, connection.total_changes().saturating_sub(rows_before), started, @@ -456,11 +456,9 @@ fn rollback_or( fallback: StorageRuntimeErrorV1, operation: &'static str, ) -> StorageRuntimeErrorV1 { - savepoint - .rollback() - .err() - .map(|error| driver_failure(error, operation).storage_error()) - .unwrap_or(fallback) + savepoint.rollback().err().map_or(fallback, |error| { + driver_failure(error, operation).storage_error() + }) } fn settle_batch_failure( @@ -556,12 +554,14 @@ fn settle_commit_denied( item.probe.as_ref(), RuntimeCancellationStageV1::BeforeCommit, ) - .map(Ok) - .unwrap_or_else(|| { - Ok(RuntimeSubmitOutcomeV1::Unavailable { - reason: UnavailableReasonV1::Faulted, - }) - }) + .map_or_else( + || { + Ok(RuntimeSubmitOutcomeV1::Unavailable { + reason: UnavailableReasonV1::Faulted, + }) + }, + Ok, + ) } else { match result { PreparedResult::Final(result) => result, diff --git a/crates/tracedecay-rusqlite-runtime/src/writer/worker/mod.rs b/crates/tracedecay-rusqlite-runtime/src/writer/worker/mod.rs index b94fda5743..277749f9fb 100644 --- a/crates/tracedecay-rusqlite-runtime/src/writer/worker/mod.rs +++ b/crates/tracedecay-rusqlite-runtime/src/writer/worker/mod.rs @@ -1001,8 +1001,8 @@ fn checkpoint_sample(result: &CheckpointResult) -> WalCheckpointSample { snapshot_blockers, .. } => WalCheckpointSample { - wal_frames: sample.map(|sample| sample.frames).unwrap_or(0), - wal_bytes: sample.map(|sample| sample.bytes).unwrap_or(0), + wal_frames: sample.map_or(0, |sample| sample.frames), + wal_bytes: sample.map_or(0, |sample| sample.bytes), blocker_count: u64::try_from(snapshot_blockers.count()).unwrap_or(u64::MAX), ..WalCheckpointSample::default() }, diff --git a/crates/tracedecay-sdk/tests/sdk_suite/main.rs b/crates/tracedecay-sdk/tests/sdk_suite/main.rs index 1ce2739fd6..ffb57e3980 100644 --- a/crates/tracedecay-sdk/tests/sdk_suite/main.rs +++ b/crates/tracedecay-sdk/tests/sdk_suite/main.rs @@ -20,9 +20,10 @@ mod remote_journey; mod semantic_replay; fn production_binary() -> std::path::PathBuf { - let path = std::env::var_os("TRACEDECAY_TEST_BIN") - .map(std::path::PathBuf::from) - .unwrap_or_else(|| std::path::PathBuf::from("../../target/debug/tracedecay")); + let path = std::env::var_os("TRACEDECAY_TEST_BIN").map_or_else( + || std::path::PathBuf::from("../../target/debug/tracedecay"), + std::path::PathBuf::from, + ); std::fs::canonicalize(&path) .unwrap_or_else(|error| panic!("missing production daemon {}: {error}", path.display())) } diff --git a/crates/tracedecay-search-eval/src/report_tests.rs b/crates/tracedecay-search-eval/src/report_tests.rs index ef2d6aead7..3814a65551 100644 --- a/crates/tracedecay-search-eval/src/report_tests.rs +++ b/crates/tracedecay-search-eval/src/report_tests.rs @@ -36,13 +36,15 @@ fn assert_baseline_report_retains_raw_fallback_current_and_exact_ten_x_samples( .find(|output| { output.profile_id == profile.profile_id && output.partition == profile.partition }) - .map(|output| { - format!( - "observed {} vs pinned {}", - output.query_fallback_digest, output.expected_query_fallback_digest - ) - }) - .unwrap_or_else(|| "no generated output for this profile".to_owned()); + .map_or_else( + || "no generated output for this profile".to_owned(), + |output| { + format!( + "observed {} vs pinned {}", + output.query_fallback_digest, output.expected_query_fallback_digest + ) + }, + ); assert!( profile.fallback_matches_expected, "{}:{} ranking receipt drifted from \ diff --git a/crates/tracedecay-session-memory/src/context/mod.rs b/crates/tracedecay-session-memory/src/context/mod.rs index a95846697d..19de3c8ca7 100644 --- a/crates/tracedecay-session-memory/src/context/mod.rs +++ b/crates/tracedecay-session-memory/src/context/mod.rs @@ -9,8 +9,11 @@ mod registered_scope; use std::fmt; use std::future::Future; +use std::pin::Pin; use std::task::Poll; +use tracing::Instrument; + use tracedecay_contracts::now_micros; use tracedecay_domain::{ AccessPolicyDigest, ProjectId, RepositoryId, WorktreeId, sha256_hex_suffix, @@ -411,35 +414,45 @@ pub fn application_request_interruption( /// Runs one awaitable application step against the exact immutable deadline /// and the live cancellation token owned by the transport/runtime boundary. -#[tracing::instrument(name = "usecases.context.interruptible", level = "trace", skip_all)] -pub async fn run_application_request_interruptible( - context: &tracedecay_contracts::RequestContext, - cancellation: &CancellationToken, - future: impl std::future::Future, +pub fn run_application_request_interruptible<'a, T, F>( + context: &'a tracedecay_contracts::RequestContext, + cancellation: &'a CancellationToken, + future: impl Future + Send + 'a, on_interruption: F, -) -> Result +) -> Pin> + Send + 'a>> where - F: FnOnce(), + T: Send + 'a, + F: FnOnce() + Send + 'a, { - if let Some(interruption) = application_request_interruption(context, cancellation) { - on_interruption(); - return Err(interruption); - } + // The awaited step is erased before this wait is built. Leaving the + // concrete step in the wait makes every caller re-prove `Send` through + // that step's whole state (rust-lang/rust#159228). + let future: Pin + Send + 'a>> = Box::pin(future); + let span = tracing::trace_span!("usecases.context.interruptible"); + Box::pin( + async move { + if let Some(interruption) = application_request_interruption(context, cancellation) { + on_interruption(); + return Err(interruption); + } - let terminal_at = context - .deadline() - .expires_at - .0 - .min(context.grant().expires_at.0); - let remaining_micros = terminal_at.saturating_sub(application_observed_at().0); - let remaining_micros = u64::try_from(remaining_micros).unwrap_or(0); - race_application_request_wait( - remaining_micros, - cancellation.cancelled(), - future, - on_interruption, + let terminal_at = context + .deadline() + .expires_at + .0 + .min(context.grant().expires_at.0); + let remaining_micros = terminal_at.saturating_sub(application_observed_at().0); + let remaining_micros = u64::try_from(remaining_micros).unwrap_or(0); + race_application_request_wait( + remaining_micros, + cancellation.cancelled(), + future, + on_interruption, + ) + .await + } + .instrument(span), ) - .await } /// Races one awaitable step against a wall-clock deadline and a live diff --git a/crates/tracedecay-session-memory/src/fact_store/crud/commands.rs b/crates/tracedecay-session-memory/src/fact_store/crud/commands.rs index cb5783bf17..8011fe7fa9 100644 --- a/crates/tracedecay-session-memory/src/fact_store/crud/commands.rs +++ b/crates/tracedecay-session-memory/src/fact_store/crud/commands.rs @@ -1,5 +1,8 @@ //! Canonical project-memory add, update, and remove commands. +use std::future::Future; +use std::pin::Pin; + use super::super::envelope::{ ProjectMemoryOperationReceiptV1, project_memory_digest, project_memory_lookup_operation_receipt_tx, project_memory_receipt_u64, @@ -645,11 +648,15 @@ pub(in crate::fact_store) async fn update_project_memory_fact_tx( update_project_memory_fact_with_cas_tx(transaction, request, false).await } -pub(in crate::fact_store) async fn update_project_memory_fact_after_curation_review_tx( - transaction: &Transaction<'_>, - request: &ProjectMemoryFactUpdateCommandV1, -) -> FactStoreResult { - update_project_memory_fact_with_cas_tx(transaction, request, true).await +pub(in crate::fact_store) fn update_project_memory_fact_after_curation_review_tx<'a>( + transaction: &'a Transaction<'_>, + request: &'a ProjectMemoryFactUpdateCommandV1, +) -> Pin> + Send + 'a>> { + Box::pin(update_project_memory_fact_with_cas_tx( + transaction, + request, + true, + )) } async fn update_project_memory_fact_with_cas_tx( @@ -831,11 +838,15 @@ pub(in crate::fact_store) async fn remove_project_memory_fact_tx( remove_project_memory_fact_with_cas_tx(transaction, request, false).await } -pub(in crate::fact_store) async fn remove_project_memory_fact_after_curation_review_tx( - transaction: &Transaction<'_>, - request: &ProjectMemoryFactRemoveCommandV1, -) -> FactStoreResult { - remove_project_memory_fact_with_cas_tx(transaction, request, true).await +pub(in crate::fact_store) fn remove_project_memory_fact_after_curation_review_tx<'a>( + transaction: &'a Transaction<'_>, + request: &'a ProjectMemoryFactRemoveCommandV1, +) -> Pin> + Send + 'a>> { + Box::pin(remove_project_memory_fact_with_cas_tx( + transaction, + request, + true, + )) } async fn remove_project_memory_fact_with_cas_tx( diff --git a/crates/tracedecay-session-memory/src/fact_store/curation/apply.rs b/crates/tracedecay-session-memory/src/fact_store/curation/apply.rs index 2899794122..54fec951e8 100644 --- a/crates/tracedecay-session-memory/src/fact_store/curation/apply.rs +++ b/crates/tracedecay-session-memory/src/fact_store/curation/apply.rs @@ -1,6 +1,8 @@ //! Canonical tag curation and fact merge commands. use std::collections::{BTreeSet, HashMap}; +use std::future::Future; +use std::pin::Pin; use serde_json::Value; use tracedecay_domain::{ @@ -76,27 +78,34 @@ pub(super) fn curation_receipt_from_value( .map_err(|error| storage_error(PROJECT_MEMORY_WRITE_OPERATION, error)) } -async fn replay_curation( - transaction: &Transaction<'_>, - request: &ProjectMemoryFactCurationBatchV1, - envelope: &ProjectMemoryOperationReceiptV1, - input_digest: &str, -) -> FactStoreResult { - let receipt = curation_receipt_from_value(&envelope.receipt)?; - if receipt.owner() != request.owner() - || receipt.operation_id() != request.operation_id() - || receipt.input_digest() != input_digest - || receipt.automation_run_id() != request.automation_run_id() - || envelope.fact_id.as_ref() != receipt.replay_fact_id() - || envelope.event_id.as_ref() != receipt.replay_event_id() - { - return Err(storage_message( - PROJECT_MEMORY_WRITE_OPERATION, - "curation operation receipt material does not match the request", - )); - } - verify_curation_replay_events_tx(transaction, request, &receipt).await?; - Ok(receipt.into_replayed()) +fn replay_curation<'a>( + transaction: &'a Transaction<'_>, + request: &'a ProjectMemoryFactCurationBatchV1, + envelope: &'a ProjectMemoryOperationReceiptV1, + input_digest: &'a str, +) -> Pin> + Send + 'a>> +{ + let receipt = curation_receipt_from_value(&envelope.receipt); + Box::pin(async move { + let receipt = receipt?; + if receipt.owner() != request.owner() + || receipt.operation_id() != request.operation_id() + || receipt.input_digest() != input_digest + || receipt.automation_run_id() != request.automation_run_id() + || envelope.fact_id.as_ref() != receipt.replay_fact_id() + || envelope.event_id.as_ref() != receipt.replay_event_id() + { + return Err(storage_message( + PROJECT_MEMORY_WRITE_OPERATION, + "curation operation receipt material does not match the request", + )); + } + let verified: Pin> + Send + '_>> = Box::pin( + verify_curation_replay_events_tx(transaction, request, &receipt), + ); + verified.await?; + Ok(receipt.into_replayed()) + }) } async fn verify_curation_replay_events_tx( @@ -922,11 +931,15 @@ pub(in crate::fact_store) async fn merge_project_memory_facts_tx( merge_project_memory_facts_with_cas_tx(transaction, request, false).await } -async fn merge_project_memory_facts_after_curation_review_tx( - transaction: &Transaction<'_>, - request: &ProjectMemoryFactMergeCommandV1, -) -> FactStoreResult { - merge_project_memory_facts_with_cas_tx(transaction, request, true).await +fn merge_project_memory_facts_after_curation_review_tx<'a>( + transaction: &'a Transaction<'_>, + request: &'a ProjectMemoryFactMergeCommandV1, +) -> Pin> + Send + 'a>> { + Box::pin(merge_project_memory_facts_with_cas_tx( + transaction, + request, + true, + )) } async fn merge_project_memory_facts_with_cas_tx( diff --git a/crates/tracedecay-session-memory/src/fact_store/envelope.rs b/crates/tracedecay-session-memory/src/fact_store/envelope.rs index c66d77db6a..a65ed124cb 100644 --- a/crates/tracedecay-session-memory/src/fact_store/envelope.rs +++ b/crates/tracedecay-session-memory/src/fact_store/envelope.rs @@ -22,10 +22,37 @@ use super::primitives::{ /// The runtime retains both the settlement task and its join while the caller /// holds only a receipt. Dropping that receipt cannot interrupt an admitted /// commit or its graph publication. -pub(in crate::fact_store) async fn settle_fact_write( +struct FactSettlementReceipt { + cx: Cx, + receipt: oneshot::Receiver>, +} + +pub(in crate::fact_store) fn settle_fact_write( + operation: &'static str, + work: F, +) -> Pin> + Send>> +where + T: Send + 'static, + F: FnOnce(Cx) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, +{ + // `RuntimeHandle` stays in this synchronous admission. Holding it across + // the caller's await makes every `Send` proof enter `BrowserWorkerPump`'s + // self-reference (rust-lang/rust#159228). + let admitted = admit_fact_settlement(operation, work); + Box::pin(async move { + let FactSettlementReceipt { cx, mut receipt } = admitted?; + receipt + .recv(&cx) + .await + .map_err(|error| storage_error(operation, error))? + }) +} + +fn admit_fact_settlement( operation: &'static str, work: F, -) -> FactStoreResult +) -> FactStoreResult> where T: Send + 'static, F: FnOnce(Cx) -> Fut + Send + 'static, @@ -37,7 +64,7 @@ where .try_request_cx_with_budget(Budget::INFINITE) .map_err(|error| storage_error(operation, error))?; let receipt_cx = settlement_cx.clone(); - let (completed, mut receipt) = oneshot::channel(); + let (completed, receipt) = oneshot::channel(); runtime .try_spawn_with_cx(move |reaper_cx| async move { let outcome = match settlement_cx.spawn(work) { @@ -56,10 +83,10 @@ where } }) .map_err(|error| storage_error(operation, error))?; - receipt - .recv(&receipt_cx) - .await - .map_err(|error| storage_error(operation, error))? + Ok(FactSettlementReceipt { + cx: receipt_cx, + receipt, + }) } pub(super) async fn finish_read_snapshot( diff --git a/crates/tracedecay-session-memory/src/fact_store/graph.rs b/crates/tracedecay-session-memory/src/fact_store/graph.rs index 9f879f7c79..905b4924fe 100644 --- a/crates/tracedecay-session-memory/src/fact_store/graph.rs +++ b/crates/tracedecay-session-memory/src/fact_store/graph.rs @@ -1,7 +1,10 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; use asupersync::Cx; +use tracing::Instrument; use tracedecay_domain::canonical_text::sha256_hex; use tracedecay_domain::{ @@ -419,90 +422,125 @@ pub(super) fn schedule_project_memory_graph_reconciliation( /// cannot release the shutdown join while a task still owns a graph /// operation or snapshot lease. The scheduled worker and the post-write /// task pass `None`: their own join/admission already spans the whole pass. -#[tracing::instrument( - name = "runtime_core.memory_graph.reconcile_pass", - level = "trace", - skip_all -)] -async fn reconcile_project_memory_graph_pass( - cx: &Cx, - db: &Database, - read_control: Option<&FactReadControl>, - inline_pass: Option<&Arc>, -) -> FactStoreResult { - let _pass = db - .begin_project_memory_reconciliation_pass() - .map_err(|counter| { - storage_message( - OPERATION, - format!("project memory reconciliation telemetry overflowed: {counter}"), - ) - })?; - let owner = bound_owner(db)?; - let fact_runtime = - super::runtime::retained_fact_runtime(db)?.ok_or(FactStoreError::GraphUnavailable)?; - super::runtime::validate_owner_binding(fact_runtime.binding(), &owner, OPERATION)?; - let projection = GraphProjectionIdentity::new( - namespace(&owner)?, - GraphProjectionId::new(PROJECTION).map_err(|error| graph_error(&owner, error))?, - ); - let loaded = load_source(db, &owner, read_control, Some(db)).await?; - let watermark = source_watermark(&owner, &loaded.source, loaded.lineage_stamp, read_control)?; - if let Some(stamp) = loaded.lineage_stamp { - db.record_memory_graph_source_watermark(stamp, watermark.clone()); - } - let manifest = { - let _span = tracing::trace_span!("runtime_core.memory_graph.manifest_build").entered(); - build_manifest( - &owner, - projection.clone(), - &loaded.source, - watermark.clone(), - None, - ) - }?; - let source_stamp = loaded.lineage_stamp; - let expected_generation = manifest.generation.clone(); - let idempotency_key = - GraphIdempotencyKey::new(format!("publish:{}", expected_generation.as_str())) - .map_err(|error| graph_error(&owner, error))?; - db.project_memory_reconciliation_telemetry() - .record_publication_attempt() - .map_err(|counter| { - storage_message( - OPERATION, - format!("project memory reconciliation telemetry overflowed: {counter}"), - ) - })?; - let head_read_control = read_control - .cloned() - .unwrap_or_else(|| FactReadControl::new(Arc::new(|| false))); - ensure_source_read_active(read_control)?; - let runtime = issue_memory_graph_operation(db)?; - let publish_admission = inline_pass.cloned(); - let snapshot = match cx - .spawn_blocking(move |_cx| { - let _span = tracing::trace_span!("runtime_core.memory_graph.publish_apply").entered(); - let _inline_pass = publish_admission; - runtime - .runtime() - .reconcile_verified_manifest(&manifest, idempotency_key) - }) - .map_err(|error| storage_error(OPERATION, error))? - .join(cx) - .await - .map_err(|error| storage_error(OPERATION, error))? - { - Ok(snapshot) => snapshot, - Err(error) => { - // The typed mapping below collapses the graph error into unit - // variants for wire states; record the full message first so a - // persistently failing reconciliation names its cause in - // operator logs instead of a bare kind. - tracing::warn!(%error, "memory graph verified-manifest publication failed"); - let mapped = graph_error(&owner, error); - if matches!(mapped, FactStoreError::GraphConflict) - && let Some(snapshot) = verified_head_matching( +fn reconcile_project_memory_graph_pass<'a>( + cx: &'a Cx, + db: &'a Database, + read_control: Option<&'a FactReadControl>, + inline_pass: Option<&'a Arc>, +) -> Pin< + Box< + dyn Future> + + Send + + 'a, + >, +> { + // One span for the sweep. The concrete pass future stays behind this box + // so a caller's `Send` proof does not walk the sweep (rust-lang/rust#159228). + let span = tracing::trace_span!("runtime_core.memory_graph.reconcile_pass"); + Box::pin( + async move { + let _pass = db + .begin_project_memory_reconciliation_pass() + .map_err(|counter| { + storage_message( + OPERATION, + format!("project memory reconciliation telemetry overflowed: {counter}"), + ) + })?; + let owner = bound_owner(db)?; + let fact_runtime = super::runtime::retained_fact_runtime(db)? + .ok_or(FactStoreError::GraphUnavailable)?; + super::runtime::validate_owner_binding(fact_runtime.binding(), &owner, OPERATION)?; + let projection = GraphProjectionIdentity::new( + namespace(&owner)?, + GraphProjectionId::new(PROJECTION).map_err(|error| graph_error(&owner, error))?, + ); + let loaded = load_source(db, &owner, read_control, Some(db)).await?; + let watermark = + source_watermark(&owner, &loaded.source, loaded.lineage_stamp, read_control)?; + if let Some(stamp) = loaded.lineage_stamp { + db.record_memory_graph_source_watermark(stamp, watermark.clone()); + } + let manifest = { + let _span = + tracing::trace_span!("runtime_core.memory_graph.manifest_build").entered(); + build_manifest( + &owner, + projection.clone(), + &loaded.source, + watermark.clone(), + None, + ) + }?; + let source_stamp = loaded.lineage_stamp; + let expected_generation = manifest.generation.clone(); + let idempotency_key = + GraphIdempotencyKey::new(format!("publish:{}", expected_generation.as_str())) + .map_err(|error| graph_error(&owner, error))?; + db.project_memory_reconciliation_telemetry() + .record_publication_attempt() + .map_err(|counter| { + storage_message( + OPERATION, + format!("project memory reconciliation telemetry overflowed: {counter}"), + ) + })?; + let head_read_control = read_control + .cloned() + .unwrap_or_else(|| FactReadControl::new(Arc::new(|| false))); + ensure_source_read_active(read_control)?; + let runtime = issue_memory_graph_operation(db)?; + let publish_admission = inline_pass.cloned(); + let snapshot = match cx + .spawn_blocking(move |_cx| { + let _span = + tracing::trace_span!("runtime_core.memory_graph.publish_apply").entered(); + let _inline_pass = publish_admission; + runtime + .runtime() + .reconcile_verified_manifest(&manifest, idempotency_key) + }) + .map_err(|error| storage_error(OPERATION, error))? + .join(cx) + .await + .map_err(|error| storage_error(OPERATION, error))? + { + Ok(snapshot) => snapshot, + Err(error) => { + // The typed mapping below collapses the graph error into unit + // variants for wire states; record the full message first so a + // persistently failing reconciliation names its cause in + // operator logs instead of a bare kind. + tracing::warn!(%error, "memory graph verified-manifest publication failed"); + let mapped = graph_error(&owner, error); + if matches!(mapped, FactStoreError::GraphConflict) + && let Some(snapshot) = verified_head_matching( + cx, + db, + &projection, + &expected_generation, + &owner, + head_read_control.clone(), + inline_pass.cloned(), + ) + .await? + { + finish_reconciliation_watermark( + db, + &owner, + watermark, + source_stamp, + read_control, + ) + .await?; + return Ok(snapshot); + } + return Err(mapped); + } + }; + if snapshot.projection() != &projection || snapshot.generation() != &expected_generation + { + if let Some(snapshot) = verified_head_matching( cx, db, &projection, @@ -512,34 +550,25 @@ async fn reconcile_project_memory_graph_pass( inline_pass.cloned(), ) .await? - { - finish_reconciliation_watermark(db, &owner, watermark, source_stamp, read_control) + { + finish_reconciliation_watermark( + db, + &owner, + watermark, + source_stamp, + read_control, + ) .await?; - return Ok(snapshot); + return Ok(snapshot); + } + return Err(FactStoreError::GraphConflict); } - return Err(mapped); - } - }; - if snapshot.projection() != &projection || snapshot.generation() != &expected_generation { - if let Some(snapshot) = verified_head_matching( - cx, - db, - &projection, - &expected_generation, - &owner, - head_read_control.clone(), - inline_pass.cloned(), - ) - .await? - { finish_reconciliation_watermark(db, &owner, watermark, source_stamp, read_control) .await?; - return Ok(snapshot); + Ok(snapshot) } - return Err(FactStoreError::GraphConflict); - } - finish_reconciliation_watermark(db, &owner, watermark, source_stamp, read_control).await?; - Ok(snapshot) + .instrument(span), + ) } async fn finish_reconciliation_watermark( diff --git a/crates/tracedecay-session-memory/src/fact_store/search.rs b/crates/tracedecay-session-memory/src/fact_store/search.rs index 0abaff1754..f43d404d63 100644 --- a/crates/tracedecay-session-memory/src/fact_store/search.rs +++ b/crates/tracedecay-session-memory/src/fact_store/search.rs @@ -1,6 +1,10 @@ //! Canonical fact search, ranking, cursors, and retrieval recording. use std::collections::BTreeSet; +use std::future::Future; +use std::pin::Pin; + +use tracing::Instrument; use crate::memory::entities::normalize_entity; @@ -259,8 +263,7 @@ async fn project_memory_rank_facts_tx( .unwrap_or(Confidence::new(0.3).map_err(FactStoreError::from)?); let fts_scores = candidates .as_mut() - .map(|candidates| std::mem::take(&mut candidates.fts_scores)) - .unwrap_or_default(); + .map_or_default(|candidates| std::mem::take(&mut candidates.fts_scores)); let mut facts = if let Some(candidates) = candidates.take() { ensure_project_memory_read_active(read_control)?; let projections = load_project_memory_projections_controlled_tx( @@ -490,55 +493,63 @@ async fn project_memory_candidates_snapshot( finish_read_snapshot(transaction, result).await } -#[tracing::instrument(name = "runtime_core.memory.graph_assist", level = "trace", skip_all)] -async fn project_memory_graph_assist( - db: &Database, - query: &ProjectMemoryFactSearchQuery, - candidates: &mut SearchCandidates, - read_control: &FactReadControl, -) -> FactStoreResult { - if db.issue_memory_graph_runtime_operation().is_err() { - return Ok(ProjectMemoryFactSearchGraphCoverageV1::NotMounted); - } - let roots = candidates - .relation_roots - .iter() - .cloned() - .collect::>(); - if roots.is_empty() { - return Ok(ProjectMemoryFactSearchGraphCoverageV1::Complete { - root_count: 0, - relation_count: 0, - expanded_fact_count: 0, - }); - } - let graph_query = ProjectMemoryGraphQueryV1::new( - query.owner().clone(), - roots.clone(), - MAX_PROJECT_MEMORY_GRAPH_RELATIONS, - )?; - let page = match super::graph::project_memory_graph(db, graph_query, read_control).await { - Ok(page) => page, - Err(FactStoreError::GraphCancelled) => return Err(FactStoreError::GraphCancelled), - Err(error) => match project_memory_graph_degradation(&error) { - Some(reason) => { - return Ok(ProjectMemoryFactSearchGraphCoverageV1::Degraded { reason }); +fn project_memory_graph_assist<'a>( + db: &'a Database, + query: &'a ProjectMemoryFactSearchQuery, + candidates: &'a mut SearchCandidates, + read_control: &'a FactReadControl, +) -> Pin< + Box> + Send + 'a>, +> { + let span = tracing::trace_span!("runtime_core.memory.graph_assist"); + Box::pin( + async move { + if db.issue_memory_graph_runtime_operation().is_err() { + return Ok(ProjectMemoryFactSearchGraphCoverageV1::NotMounted); } - None => return Err(error), - }, - }; - let before = candidates.fact_ids.len(); - candidates.fact_ids.extend( - page.facts() - .iter() - .filter(|&fact| matches!(fact, ProjectMemoryFactProjectionV1::Available(_))) - .map(|fact| fact.fact_id().clone()), - ); - Ok(ProjectMemoryFactSearchGraphCoverageV1::Complete { - root_count: roots.len(), - relation_count: page.relations().len(), - expanded_fact_count: candidates.fact_ids.len().saturating_sub(before), - }) + let roots = candidates + .relation_roots + .iter() + .cloned() + .collect::>(); + if roots.is_empty() { + return Ok(ProjectMemoryFactSearchGraphCoverageV1::Complete { + root_count: 0, + relation_count: 0, + expanded_fact_count: 0, + }); + } + let graph_query = ProjectMemoryGraphQueryV1::new( + query.owner().clone(), + roots.clone(), + MAX_PROJECT_MEMORY_GRAPH_RELATIONS, + )?; + let page = match super::graph::project_memory_graph(db, graph_query, read_control).await + { + Ok(page) => page, + Err(FactStoreError::GraphCancelled) => return Err(FactStoreError::GraphCancelled), + Err(error) => match project_memory_graph_degradation(&error) { + Some(reason) => { + return Ok(ProjectMemoryFactSearchGraphCoverageV1::Degraded { reason }); + } + None => return Err(error), + }, + }; + let before = candidates.fact_ids.len(); + candidates.fact_ids.extend( + page.facts() + .iter() + .filter(|&fact| matches!(fact, ProjectMemoryFactProjectionV1::Available(_))) + .map(|fact| fact.fact_id().clone()), + ); + Ok(ProjectMemoryFactSearchGraphCoverageV1::Complete { + root_count: roots.len(), + relation_count: page.relations().len(), + expanded_fact_count: candidates.fact_ids.len().saturating_sub(before), + }) + } + .instrument(span), + ) } pub(super) fn project_memory_graph_degradation( @@ -572,14 +583,16 @@ async fn project_memory_rank_snapshot( .begin_memory_read_transaction(PROJECT_MEMORY_READ_OPERATION) .await .map_err(|error| storage_error(PROJECT_MEMORY_READ_OPERATION, error))?; - let result = project_memory_rank_facts_tx( + let result: Pin< + Box> + Send + '_>, + > = Box::pin(project_memory_rank_facts_tx( &transaction, query, Some(candidates), graph_coverage, read_control, - ) - .await; + )); + let result = result.await; finish_read_snapshot(transaction, result).await } diff --git a/crates/tracedecay-session-memory/src/memory/graph.rs b/crates/tracedecay-session-memory/src/memory/graph.rs index 0ceacbd22d..168dfb7c72 100644 --- a/crates/tracedecay-session-memory/src/memory/graph.rs +++ b/crates/tracedecay-session-memory/src/memory/graph.rs @@ -1,3 +1,6 @@ +use std::future::Future; +use std::pin::Pin; + use tracedecay_store::{ FactReadControl, ProjectMemoryGraphPageV1, ProjectMemoryGraphQueryV1, ProjectMemoryGraphStore, }; @@ -16,10 +19,14 @@ impl MemoryApplication { ) -> Result { self.ensure_owner(query.owner())?; let max_relations = query.max_relations(); - let page = self - .authority - .project_memory_graph(query, read_control) - .await?; + let page_read: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(self.authority.project_memory_graph(query, read_control)); + let page = page_read.await?; if page.owner() != &self.owner || page.relations().len() > max_relations || page.facts().iter().any(|fact| fact.owner() != &self.owner) diff --git a/crates/tracedecay-session-memory/src/monitor_ring.rs b/crates/tracedecay-session-memory/src/monitor_ring.rs index c7fd8720fb..71140fbbdd 100644 --- a/crates/tracedecay-session-memory/src/monitor_ring.rs +++ b/crates/tracedecay-session-memory/src/monitor_ring.rs @@ -77,8 +77,7 @@ pub fn write_entry_to( let mmap_path = dir.join(MMAP_FILENAME); let project = project_root .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default(); + .map_or_default(|n| n.to_string_lossy().to_string()); let _ = write_entry_inner(&mmap_path, prefix, &project, tool_name, delta, before); } diff --git a/crates/tracedecay-session-memory/src/observability_store.rs b/crates/tracedecay-session-memory/src/observability_store.rs index 9df89a1e48..68ec4c991a 100644 --- a/crates/tracedecay-session-memory/src/observability_store.rs +++ b/crates/tracedecay-session-memory/src/observability_store.rs @@ -1,5 +1,8 @@ //! Registered-store adapter for canonical observability envelopes. +use std::future::Future; +use std::pin::Pin; + use tracedecay_contracts::{ ApplicationContractError, ExecutionTopologyRollupFragmentPageV1, ExecutionTopologyRollupFragmentQueryV1, ExecutionTopologyRollupQueryPort, ObservabilityFuture, @@ -37,12 +40,18 @@ impl<'a> RegisteredObservabilityPortV1<'a> { authorized_scope_ref: &str, idempotency_key: &str, ) -> Result, ApplicationContractError> { - let Some(row) = self - .db - .read_observability_event(authorized_scope_ref, idempotency_key) - .await - .map_err(ApplicationContractError::Domain)? - else { + let row: Pin< + Box< + dyn Future< + Output = Result, String>, + > + Send + + '_, + >, + > = Box::pin( + self.db + .read_observability_event(authorized_scope_ref, idempotency_key), + ); + let Some(row) = row.await.map_err(ApplicationContractError::Domain)? else { return Ok(None); }; let envelope = row diff --git a/crates/tracedecay-session-memory/src/session/refresh.rs b/crates/tracedecay-session-memory/src/session/refresh.rs index bcbf5e010b..4f501021a7 100644 --- a/crates/tracedecay-session-memory/src/session/refresh.rs +++ b/crates/tracedecay-session-memory/src/session/refresh.rs @@ -1,5 +1,7 @@ use std::fmt; +use tracedecay_runtime_core::erase_send; + use sha2::{Digest, Sha256}; use tracedecay_contracts::RequestContext; use tracedecay_domain::canonical_text::encode_tagged_lowercase_hex; @@ -337,13 +339,13 @@ where .with_coverage_request(SessionTemporalCoverageRequestV1::new( target.temporal_mode(), )); - let receipt = match await_with_request_controls( - context, - binding, + let joined = erase_send!( self.store.begin_or_join_session_refresh(request), - ) - .await - { + tracedecay_store::SessionStoreResult< + tracedecay_store::SessionRefreshBeginOrJoinReceiptV1, + > + ); + let receipt = match await_with_request_controls(context, binding, joined).await { Ok(Ok(receipt)) => receipt, Ok(Err(SessionStoreError::IdempotencyConflict { .. })) => { return SessionRefreshOutcome::Busy; @@ -420,17 +422,15 @@ where if let Err(outcome) = self.authorize_handle(context, binding, handle) { return *outcome; } - let progress = match await_with_request_controls( - context, - binding, + let query = erase_send!( self.store .session_refresh_progress(SessionRefreshProgressRequestV1::new( handle.operation_id.clone(), handle.target.session_id.clone(), )), - ) - .await - { + tracedecay_store::SessionStoreResult> + ); + let progress = match await_with_request_controls(context, binding, query).await { Ok(Ok(progress)) => progress, Ok(Err(error)) => { return SessionRefreshOutcome::Unavailable(SessionRefreshUnavailable::store( @@ -479,17 +479,15 @@ where if let Some(outcome) = request_interruption(context, binding) { return outcome; } - let progress = match await_with_request_controls( - context, - binding, + let query = erase_send!( self.store .session_refresh_progress(SessionRefreshProgressRequestV1::new( handle.operation_id.clone(), handle.target.session_id.clone(), )), - ) - .await - { + tracedecay_store::SessionStoreResult> + ); + let progress = match await_with_request_controls(context, binding, query).await { Ok(Ok(progress)) => progress, Ok(Err(error)) => { return SessionRefreshOutcome::Unavailable(SessionRefreshUnavailable::store( @@ -508,13 +506,11 @@ where } attempted_progress = Some(progress.clone()); let request = cancellation_request(handle, progress.as_ref()); - match await_with_request_controls( - context, - binding, + let cancelled = erase_send!( self.store.cancel_session_refresh(request), - ) - .await - { + tracedecay_store::SessionStoreResult + ); + match await_with_request_controls(context, binding, cancelled).await { Ok(Ok(receipt)) => { return if (self.scheduler)().is_err() { SessionRefreshOutcome::CancelledReconciliationRequired(receipt) @@ -560,17 +556,15 @@ where binding: &SessionRequestBinding, handle: &SessionRefreshHandle, ) -> Result, Box> { - match await_with_request_controls( - context, - binding, + let query = erase_send!( self.store .session_refresh_receipt(SessionRefreshReceiptRequestV1::new( handle.operation_id.clone(), handle.target.session_id.clone(), )), - ) - .await - { + tracedecay_store::SessionStoreResult> + ); + match await_with_request_controls(context, binding, query).await { Ok(Ok(receipt)) => Ok(receipt.map(|receipt| { let source_coverage = receipt.source_coverage().and_then(|coverage| { source_coverage_for_target(Some(coverage), &handle.target, receipt.frontier()) @@ -669,10 +663,10 @@ fn request_interruption( } } -async fn await_with_request_controls( +async fn await_with_request_controls( context: &RequestContext, binding: &SessionRequestBinding, - future: impl std::future::Future, + future: impl std::future::Future + Send, ) -> Result> { if let Some(outcome) = request_interruption(context, binding) { return Err(Box::new(outcome)); diff --git a/crates/tracedecay-session-runtime/src/lcm_authority.rs b/crates/tracedecay-session-runtime/src/lcm_authority.rs index 36fe8a6319..b03265cfe7 100644 --- a/crates/tracedecay-session-runtime/src/lcm_authority.rs +++ b/crates/tracedecay-session-runtime/src/lcm_authority.rs @@ -67,15 +67,17 @@ impl LcmDaemonStore for RegisteredLcmDaemonStore { } fn status(&self, query: LcmStatusQuery) -> StoreFuture<'_, LcmStatus> { + let database = self.database.clone(); Box::pin(async move { - self.database - .lcm_status_with_options( + let gc_config = LcmGcConfig::default(); + let status: Pin> + Send>> = + Box::pin(database.lcm_status_with_options( &query.provider, query.session_id.as_deref(), query.deep, - &LcmGcConfig::default(), - ) - .await + &gc_config, + )); + status.await }) } @@ -254,31 +256,34 @@ impl DaemonLcmAuthority { match invocation.request { LcmAuthorityRequest::Compact(command) => { - self.execute_compaction( - &invocation.context, - &invocation.cancellation, - started_at, - *command, - ) - .await + let compaction: Pin + Send + '_>> = + Box::pin(self.execute_compaction( + &invocation.context, + &invocation.cancellation, + started_at, + *command, + )); + compaction.await } LcmAuthorityRequest::Status(query) => { - self.execute_status( - &invocation.context, - &invocation.cancellation, - started_at, - query, - ) - .await + let status: Pin + Send + '_>> = + Box::pin(self.execute_status( + &invocation.context, + &invocation.cancellation, + started_at, + query, + )); + status.await } LcmAuthorityRequest::Doctor(query) => { - self.execute_doctor( - &invocation.context, - &invocation.cancellation, - started_at, - query, - ) - .await + let doctor: Pin + Send + '_>> = + Box::pin(self.execute_doctor( + &invocation.context, + &invocation.cancellation, + started_at, + query, + )); + doctor.await } } } diff --git a/crates/tracedecay-session-runtime/src/lcm_authority/tests.rs b/crates/tracedecay-session-runtime/src/lcm_authority/tests.rs index 35d6b30e9a..c94be43625 100644 --- a/crates/tracedecay-session-runtime/src/lcm_authority/tests.rs +++ b/crates/tracedecay-session-runtime/src/lcm_authority/tests.rs @@ -30,17 +30,13 @@ struct FakeStore { impl FakeStore { fn calls(&self) -> Vec { - self.calls - .lock() - .map(|calls| calls.clone()) - .unwrap_or_default() + self.calls.lock().map_or_default(|calls| calls.clone()) } fn compact_requests(&self) -> Vec { self.compact_requests .lock() - .map(|requests| requests.clone()) - .unwrap_or_default() + .map_or_default(|requests| requests.clone()) } } diff --git a/crates/tracedecay-session-runtime/src/lcm_effects.rs b/crates/tracedecay-session-runtime/src/lcm_effects.rs index 0ffd2a9263..92c00d9c87 100644 --- a/crates/tracedecay-session-runtime/src/lcm_effects.rs +++ b/crates/tracedecay-session-runtime/src/lcm_effects.rs @@ -1,3 +1,5 @@ +use std::future::Future; +use std::pin::Pin; use std::time::Duration; use tracedecay_contracts::{CancellationSignal, Deadline}; @@ -57,10 +59,10 @@ impl LcmEffectControl { Ok(()) } - async fn execute( + async fn execute( &self, execution: &ExecutionControl, - mutation: impl std::future::Future>, + mutation: Pin> + Send + '_>>, ) -> Result { self.checkpoint()?; let result = mutation.await; @@ -111,11 +113,11 @@ impl DaemonLcmEffectService { self.control .execute( &execution, - tracing::Instrument::instrument( + Box::pin(tracing::Instrument::instrument( self.db .lcm_protect_session_raw_messages(&request.provider, &request.session_id), tracing::trace_span!("daemon.lcm.hydrate"), - ), + )), ) .await?; if matches!( @@ -260,12 +262,12 @@ impl DaemonLcmEffectService { self.control .execute( &execution, - tracing::Instrument::instrument( + Box::pin(tracing::Instrument::instrument( self.db.lcm_compress_guarded(request, &execution, move || { before_commit.checkpoint() }), tracing::trace_span!("daemon.lcm.commit"), - ), + )), ) .await } @@ -285,23 +287,33 @@ impl DaemonLcmEffectService { const RETAINED_PHASE_BYTES: u64 = tracedecay_lcm::LCM_SCAN_PAGE_MAX_BYTES as u64 / 2; let execution = self.control.execution_control(); let before_commit = self.control.clone(); + let compressed: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_lcm::summary_convergence::LcmBoundedCompressionResponse, + LcmError, + >, + > + Send, + >, + > = Box::pin(self.db.lcm_compress_retained_page_guarded( + request, + &execution, + move || before_commit.checkpoint(), + tracedecay_lcm::compression::RetainedCompressionGuard { + row_limit: RETAINED_PHASE_ROWS, + byte_limit: RETAINED_PHASE_BYTES, + expected_summary_source_range: expected_summary_source_range.cloned(), + }, + convergence_candidate, + )); self.control .execute( &execution, - tracing::Instrument::instrument( - self.db.lcm_compress_retained_page_guarded( - request, - &execution, - move || before_commit.checkpoint(), - tracedecay_lcm::compression::RetainedCompressionGuard { - row_limit: RETAINED_PHASE_ROWS, - byte_limit: RETAINED_PHASE_BYTES, - expected_summary_source_range: expected_summary_source_range.cloned(), - }, - convergence_candidate, - ), + Box::pin(tracing::Instrument::instrument( + compressed, tracing::trace_span!("daemon.lcm.retained_commit"), - ), + )), ) .await } @@ -313,7 +325,7 @@ impl DaemonLcmEffectService { let execution = self.control.execution_control(); let recovered = self .control - .execute(&execution, async { + .execute(&execution, Box::pin(async { SessionTemporalAccess::new(&*self.db) .recover_pending_session_relation_projection_page( RELATION_PAGE_LIMIT, @@ -331,7 +343,7 @@ impl DaemonLcmEffectService { "recover bounded retained LCM relation projection: {error}" )), }) - }) + })) .await?; self.control.checkpoint()?; Ok(recovered) @@ -347,11 +359,11 @@ impl DaemonLcmEffectService { self.control .execute( &execution, - tracing::Instrument::instrument( + Box::pin(tracing::Instrument::instrument( self.db .lcm_session_boundary_guarded(request, move || before_commit.checkpoint()), tracing::trace_span!("daemon.lcm.boundary"), - ), + )), ) .await } diff --git a/crates/tracedecay-session-runtime/src/lcm_effects/tests.rs b/crates/tracedecay-session-runtime/src/lcm_effects/tests.rs index 34e0cb2b11..2dba676951 100644 --- a/crates/tracedecay-session-runtime/src/lcm_effects/tests.rs +++ b/crates/tracedecay-session-runtime/src/lcm_effects/tests.rs @@ -232,10 +232,13 @@ async fn cancellation_observed_after_a_committed_effect_does_not_replace_its_res let control = LcmEffectControl::new(None, Some(&cancellation)); let execution = control.execution_control(); let result = control - .execute(&execution, async { - assert!(cancellation.cancel(tracedecay_domain::UtcMicros(2))); - Ok::<_, LcmError>("committed") - }) + .execute( + &execution, + Box::pin(async { + assert!(cancellation.cancel(tracedecay_domain::UtcMicros(2))); + Ok::<_, LcmError>("committed") + }), + ) .await; assert_eq!(result.unwrap(), "committed"); diff --git a/crates/tracedecay-session-runtime/src/lcm_summary_convergence.rs b/crates/tracedecay-session-runtime/src/lcm_summary_convergence.rs index a7fed97eda..05257306b3 100644 --- a/crates/tracedecay-session-runtime/src/lcm_summary_convergence.rs +++ b/crates/tracedecay-session-runtime/src/lcm_summary_convergence.rs @@ -1,5 +1,7 @@ //! Restart-safe background convergence of retained LCM summary state. +use std::future::Future; +use std::pin::Pin; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tracedecay_global_db::RegisteredGlobalDbLeaseV1; @@ -58,23 +60,77 @@ pub(crate) async fn run_summary_convergence_page( // Relation effects from a prior summary transaction are their own // session-bounded, work-budgeted page. The scheduler calls this function // only while holding the daemon-wide historical-work admission. - let relation_recovery = - super::lcm_effects::DaemonLcmEffectService::new(database.clone(), None, None) - .recover_retained_relation_projection_page() - .await?; - let backfill = backfill_queue(&database).await?; - let parked_requeue = requeue_parked_sessions(&database).await?; - let predecessor_range_rewrite = rewrite_predecessor_ranges(&database).await?; + let effects = super::lcm_effects::DaemonLcmEffectService::new(database.clone(), None, None); + let relation_recovery: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_session_temporal_store::SessionRelationRecoveryPage, + LcmError, + >, + > + Send + + '_, + >, + > = Box::pin(effects.recover_retained_relation_projection_page()); + let relation_recovery = relation_recovery.await?; + let backfill: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_lcm::summary_convergence::LcmSummaryQueueBackfillPage, + LcmError, + >, + > + Send + + '_, + >, + > = Box::pin(backfill_queue(&database)); + let backfill = backfill.await?; + let parked_requeue: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_lcm::summary_convergence::LcmParkedRequeuePage, + LcmError, + >, + > + Send + + '_, + >, + > = Box::pin(requeue_parked_sessions(&database)); + let parked_requeue = parked_requeue.await?; + let predecessor_range_rewrite: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_lcm::summary_convergence::LcmPredecessorRangeRewritePage, + LcmError, + >, + > + Send + + '_, + >, + > = Box::pin(rewrite_predecessor_ranges(&database)); + let predecessor_range_rewrite = predecessor_range_rewrite.await?; let now_unix_ms = unix_millis()?; let page_limit = page_limit.max(1); let mut sessions = Vec::with_capacity(page_limit); for _ in 0..page_limit { - let Some(candidate) = load_candidate(&database, now_unix_ms).await? else { + let loaded: Pin< + Box< + dyn Future, LcmError>> + + Send + + '_, + >, + > = Box::pin(load_candidate(&database, now_unix_ms)); + let Some(candidate) = loaded.await? else { break; }; - sessions.push(process_candidate(&database, candidate, now_unix_ms).await?); + let processed: Pin< + Box> + Send + '_>, + > = Box::pin(process_candidate(&database, candidate, now_unix_ms)); + sessions.push(processed.await?); } - let summary_retry_at = load_next_retry(&database).await?; + let summary_retry: Pin, LcmError>> + Send + '_>> = + Box::pin(load_next_retry(&database)); + let summary_retry_at = summary_retry.await?; let relation_retry_at = relation_recovery .next_retry_at_unix_seconds .map(|seconds| seconds.saturating_mul(1_000)); diff --git a/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs b/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs index 10c48d2d91..cc4f608345 100644 --- a/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs +++ b/crates/tracedecay-session-runtime/src/retained/lcm/retrieval.rs @@ -199,8 +199,7 @@ pub(super) async fn execute_grep( let message_type = message_type(request.message_type); let roles = request .role - .map(|role| vec![role.as_str().to_owned()]) - .unwrap_or_default(); + .map_or_default(|role| vec![role.as_str().to_owned()]); let start_time = time_filter( request.start_time.as_ref().or(request.since.as_ref()), SearchTimeBound::Start, diff --git a/crates/tracedecay-session-runtime/src/retained/session.rs b/crates/tracedecay-session-runtime/src/retained/session.rs index dcd0f1288b..d7237bc6b1 100644 --- a/crates/tracedecay-session-runtime/src/retained/session.rs +++ b/crates/tracedecay-session-runtime/src/retained/session.rs @@ -1,4 +1,6 @@ +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; use std::time::Duration; @@ -6,9 +8,9 @@ use tracedecay_contracts::retained_surfaces::{ GitScopeV1, HydrationStateResultV1, MessageSearchHitV1, MessageSearchRequestV1, MessageSearchResultV1, RetainedNextActionV1, RetainedOutcomeStatusV1, RetainedSurfaceOperation, RetainedSurfaceResultV1, SessionMessageV1, SessionRecordV1, SessionRefreshRequestV1, - SessionRefreshScopeV1, SessionsForRequestV1, TemporalCoverageOmissionV1, TemporalExplanationV1, - TemporalFreshnessV1, TemporalMetadataV1, TemporalOmissionV1, TemporalPopulationCountV1, - WorkflowsRequestV1, + SessionRefreshScopeV1, SessionsForRequestV1, SessionsForResultV1, TemporalCoverageOmissionV1, + TemporalExplanationV1, TemporalFreshnessV1, TemporalMetadataV1, TemporalOmissionV1, + TemporalPopulationCountV1, WorkflowsRequestV1, WorkflowsResultV1, }; use tracedecay_contracts::{ ApplicationOutcome, RequestAdmission, RetainedSessionExecutionPortV1, RetainedSessionRequestV1, @@ -273,16 +275,18 @@ impl<'a> DirectRetainedSessionPortV1<'a> { return Err(RetainedSurfaceExecutionErrorV1::Unsupported); }; ensure_mounted_project_context(context, authorities)?; + let queried: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(crate::session_queries::sessions_for( + Some(authorities.session_database.as_ref()), + request, + )); let result = self - .bounded(context, async { - Ok::<_, TraceDecayError>( - crate::session_queries::sessions_for( - Some(authorities.session_database.as_ref()), - request, - ) - .await, - ) - }) + .bounded(context, async { Ok::<_, TraceDecayError>(queried.await) }) .await??; evidence_outcome( context, @@ -305,16 +309,18 @@ impl<'a> DirectRetainedSessionPortV1<'a> { return Err(RetainedSurfaceExecutionErrorV1::Unsupported); }; ensure_mounted_project_context(context, authorities)?; + let queried: Pin< + Box< + dyn Future> + + Send + + '_, + >, + > = Box::pin(crate::session_queries::workflows( + Some(authorities.workflow_index.as_ref()), + request, + )); let result = self - .bounded(context, async { - Ok::<_, TraceDecayError>( - crate::session_queries::workflows( - Some(authorities.workflow_index.as_ref()), - request, - ) - .await, - ) - }) + .bounded(context, async { Ok::<_, TraceDecayError>(queried.await) }) .await??; if result.status == RetainedOutcomeStatusV1::Unavailable { let detail = result.error.as_ref().map_or_else( diff --git a/crates/tracedecay-session-runtime/src/session_queries.rs b/crates/tracedecay-session-runtime/src/session_queries.rs index 0eb3bab1cf..b5d154152a 100644 --- a/crates/tracedecay-session-runtime/src/session_queries.rs +++ b/crates/tracedecay-session-runtime/src/session_queries.rs @@ -8,8 +8,9 @@ use tracedecay_contracts::retained_surfaces::{ WorkflowQueryModeV1, WorkflowRunV1, WorkflowStatusV1, WorkflowsRequestV1, WorkflowsResultV1, }; use tracedecay_sessions::runtime::git_correlation::{ - CommitEvidence, CommitRelation, CommitRelationFilter, GitCorrelationError, GitRefFilter, - SessionGitCorrelationHit, SessionsForQuery, SpanOverlapKind, git_scope_filter_from_args, + CommitEvidence, CommitRelation, CommitRelationFilter, CorrelationIndexPresence, + GitCorrelationError, GitRefFilter, SessionGitCorrelationHit, SessionsForQuery, SpanOverlapKind, + git_scope_filter_from_args, }; use tracedecay_sessions::{ WorkflowAgent, WorkflowGitScope, WorkflowIndexReadPort, WorkflowIndexState, WorkflowRun, @@ -64,8 +65,12 @@ pub async fn sessions_for( limit, }; let correlation = GlobalDbGitCorrelationStore::new(database); - let (results, index_presence) = match tracing::Instrument::instrument( + let queried = tracedecay_runtime_core::erase_send!( correlation.sessions_for_with_relation_and_presence(&query, relation), + Result<(Vec, CorrelationIndexPresence), GitCorrelationError> + ); + let (results, index_presence) = match tracing::Instrument::instrument( + queried, tracing::trace_span!("daemon.retained.session.sessions_for.query"), ) .await @@ -83,9 +88,13 @@ pub async fn sessions_for( && matches!(query.git_ref, GitRefFilter::Commit(_)) && relation == CommitRelationFilter::Produced { - match tracing::Instrument::instrument( + let observed = tracedecay_runtime_core::erase_send!( correlation .sessions_for_with_relation_and_presence(&query, CommitRelationFilter::Observed), + Result<(Vec, CorrelationIndexPresence), GitCorrelationError> + ); + match tracing::Instrument::instrument( + observed, tracing::trace_span!("daemon.retained.session.sessions_for.observed_fallback"), ) .await diff --git a/crates/tracedecay-session-runtime/src/session_retrieval/admitted.rs b/crates/tracedecay-session-runtime/src/session_retrieval/admitted.rs index 02ac457e68..0e3f0c34d6 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval/admitted.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval/admitted.rs @@ -393,12 +393,14 @@ impl SessionApplicationRetrievalPortV1 for DaemonSessionRetrievalService { Ok(binding) => binding, Err(outcome) => return describe_binding_outcome(*outcome), }; + let described: Pin + Send + '_>> = + Box::pin(self.execute_lcm_describe_admitted(context, &binding, command)); tokio::select! { biased; () = cancellation.cancelled() => { LcmDescribeServiceOutcome::Cancelled } - outcome = self.execute_lcm_describe_admitted(context, &binding, command) => outcome, + outcome = described => outcome, } }) } @@ -421,12 +423,14 @@ impl SessionApplicationRetrievalPortV1 for DaemonSessionRetrievalService { Ok(binding) => binding, Err(outcome) => return expand_binding_outcome(*outcome), }; + let expanded: Pin + Send + '_>> = + Box::pin(self.execute_lcm_expand_admitted(context, &binding, command)); tokio::select! { biased; () = cancellation.cancelled() => { LcmExpandServiceOutcome::Cancelled } - outcome = self.execute_lcm_expand_admitted(context, &binding, command) => outcome, + outcome = expanded => outcome, } }) } diff --git a/crates/tracedecay-session-runtime/src/session_retrieval/lcm.rs b/crates/tracedecay-session-runtime/src/session_retrieval/lcm.rs index 4d99d2db4b..bed6b5f3c2 100644 --- a/crates/tracedecay-session-runtime/src/session_retrieval/lcm.rs +++ b/crates/tracedecay-session-runtime/src/session_retrieval/lcm.rs @@ -97,8 +97,7 @@ impl DaemonSessionRetrievalService { source_coverage: result .snapshot .source_coverage() - .map(|receipt| receipt.sources().to_vec()) - .unwrap_or_default(), + .map_or_default(|receipt| receipt.sources().to_vec()), cursor: result.next_cursor.clone(), explanations: result .ranked diff --git a/crates/tracedecay-session-runtime/src/session_sync/project_lifecycle.rs b/crates/tracedecay-session-runtime/src/session_sync/project_lifecycle.rs index 74ac57571e..1b97e6223c 100644 --- a/crates/tracedecay-session-runtime/src/session_sync/project_lifecycle.rs +++ b/crates/tracedecay-session-runtime/src/session_sync/project_lifecycle.rs @@ -1,4 +1,6 @@ +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::{Arc, PoisonError, RwLock}; use tracedecay_contracts::session_sync::{ @@ -459,11 +461,6 @@ impl DaemonSessionSyncService { .cloned() } - #[tracing::instrument( - name = "daemon.session_sync.lifecycle.execute_request", - level = "trace", - skip_all - )] pub(super) async fn execute_request( &self, request: SessionSyncRequestV1, @@ -478,8 +475,10 @@ impl DaemonSessionSyncService { reason_code: "session_sync_project_retired", }; }; - self.execute_request_admitted(request, Arc::clone(&context), project_sessions) - .await + let admitted: Pin + Send + '_>> = Box::pin( + self.execute_request_admitted(request, Arc::clone(&context), project_sessions), + ); + admitted.await } pub async fn retire_project( @@ -534,9 +533,7 @@ impl DaemonSessionSyncService { return Err(format!( "session sync project '{}' could not recover after rebind: {error}{}", project_id.as_str(), - drain - .map(|drain| format!("; rollback failed: {drain}")) - .unwrap_or_default(), + drain.map_or_default(|drain| format!("; rollback failed: {drain}")), )); } Ok(true) diff --git a/crates/tracedecay-session-runtime/src/session_sync/work.rs b/crates/tracedecay-session-runtime/src/session_sync/work.rs index 3ae8055307..b16b166c9d 100644 --- a/crates/tracedecay-session-runtime/src/session_sync/work.rs +++ b/crates/tracedecay-session-runtime/src/session_sync/work.rs @@ -1,3 +1,6 @@ +use std::future::Future; +use std::pin::Pin; + use super::*; const GIT_SYNC_COMMAND_DEADLINE: Duration = Duration::from_secs(5); @@ -431,10 +434,15 @@ impl SessionSyncProjectContext { ("project", project_sessions.as_ref()), ("profile", self.user_sessions.as_ref()), ] { - for (source_json, scope_json, committed_cursor_json) in database - .list_session_sync_source_frontiers() - .await - .map_err(store_error)? + let listed = tracedecay_runtime_core::erase_send!( + database.list_session_sync_source_frontiers(), + std::result::Result< + Vec<(String, String, String)>, + tracedecay_domain::TraceDecayError, + > + ); + for (source_json, scope_json, committed_cursor_json) in + listed.await.map_err(store_error)? { frontiers.push(SessionSyncSourceFrontierV1 { store_scope: store_scope.to_owned(), @@ -480,9 +488,9 @@ impl SessionSyncProjectContext { .await .map_err(store_error)?; Ok( - git_history_frontier_from_meta(change_sequence, source_rowid) - .map(|frontier| vec![git_history_source_frontier(&self.project_id, frontier)]) - .unwrap_or_default(), + git_history_frontier_from_meta(change_sequence, source_rowid).map_or_default( + |frontier| vec![git_history_source_frontier(&self.project_id, frontier)], + ), ) } @@ -497,18 +505,27 @@ impl SessionSyncProjectContext { Ok(observation) => observation, Err(interruption) => return SessionSyncWorkResult::Interrupted(interruption), }; + let stats = import_transcript_stats(tracedecay_sessions::TranscriptIngestStats::default()); + let coverage = observation.coverage.clone(); + let persisted: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Vec, + >, + > + Send + + '_, + >, + > = Box::pin(service.persist_progress( + self, + &project_sessions, + journal_key, + stats.clone(), + coverage.clone(), + )); let pass = async { - let stats = - import_transcript_stats(tracedecay_sessions::TranscriptIngestStats::default()); - let coverage = observation.coverage.clone(); let source_frontiers = tracing::Instrument::instrument( - service.persist_progress( - self, - &project_sessions, - journal_key, - stats.clone(), - coverage.clone(), - ), + persisted, tracing::trace_span!("daemon.session_sync.combined_frontier_persist"), ) .await; @@ -565,7 +582,17 @@ impl SessionSyncProjectContext { dry_run: options.dry_run(), project_root: Some(self.project_root.clone()), }; - let backfill = store.run_bounded_history_index_page(&backfill_options, &control); + let backfill: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_sessions::runtime::git_correlation::BoundedBackfillOutcome, + tracedecay_sessions::runtime::git_correlation::GitCorrelationError, + >, + > + Send + + '_, + >, + > = Box::pin(store.run_bounded_history_index_page(&backfill_options, &control)); tokio::pin!(backfill); let (result, mut requested_interruption) = tokio::select! { biased; diff --git a/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/history.rs b/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/history.rs index 3ba9c3982b..a10d820459 100644 --- a/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/history.rs +++ b/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/history.rs @@ -159,7 +159,14 @@ impl SessionHistoricalIngestor for ProjectSessionHistoricalIngestor { let authority = tracedecay_host_admission::session_ingest_authority::GlobalDbSessionIngestAuthority::new(self.database.clone()) .with_background_cpu(Arc::clone(&self.background_cpu)); - let pass = Box::pin( + let pass: Pin< + Box< + dyn Future< + Output = tracedecay_sessions::runtime::TranscriptIngestOutcome, + > + Send + + '_, + >, + > = Box::pin( tracedecay_sessions::runtime::ingest_project_sources_for_provider_with_cancellation_and_codex_state( self.profile_identity.brain_id(), self.profile_identity.profile_id(), @@ -279,7 +286,14 @@ impl SessionHistoricalIngestor for ProfileSessionHistoricalIngestor { let registry_authority = tracedecay_host_admission::session_ingest_authority::GlobalDbSessionIngestAuthority::new(self.registry_database.clone()) .with_background_cpu(Arc::clone(&self.background_cpu)); - let pass = Box::pin( + let pass: Pin< + Box< + dyn Future< + Output = tracedecay_sessions::runtime::TranscriptIngestOutcome, + > + Send + + '_, + >, + > = Box::pin( tracedecay_sessions::runtime::ingest_user_global_sources_for_startup_with_db_and_codex_state( self.profile_identity.brain_id(), self.profile_identity.profile_id(), diff --git a/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/worker.rs b/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/worker.rs index 4cfb6e06e2..2c6508e279 100644 --- a/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/worker.rs +++ b/crates/tracedecay-session-runtime/src/session_temporal_refresh_scheduler/worker.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::future::Future; use std::ops::Range; +use std::pin::Pin; use std::sync::Arc; use std::sync::PoisonError; use std::sync::atomic::Ordering; @@ -265,8 +266,20 @@ pub(super) async fn run_session_temporal_refresh_scheduler( .take() .filter(|_| history_requested); let history_outcome = if history_requested { + let history_pass: Pin< + Box< + dyn Future< + Output = (SessionHistoricalIngestOutcome, Option), + > + Send + + '_, + >, + > = Box::pin(session_history_refresh( + &history, + &history_admission, + granted, + )); let (outcome, release) = tracing::Instrument::instrument( - session_history_refresh(&history, &history_admission, granted), + history_pass, tracing::trace_span!("daemon.scheduler.session_temporal.history"), ) .await; @@ -319,8 +332,16 @@ pub(super) async fn run_session_temporal_refresh_scheduler( || state.has_requests() || history_requires_projection { + let projection: Pin< + Box + Send + '_>, + > = Box::pin(session_projection_refresh( + &database, + &state, + projector.as_ref(), + policy, + )); let pass = tracing::Instrument::instrument( - session_projection_refresh(&database, &state, projector.as_ref(), policy), + projection, tracing::trace_span!("daemon.scheduler.session_temporal.projection"), ); tokio::pin!(pass); @@ -405,22 +426,27 @@ pub(super) async fn run_session_temporal_refresh_scheduler( }; let summary_result = { let permit = summary_admission; - let page = async { - match convergence_page { - LcmConvergencePage::PredecessorRangeRewrite => { + let page: Pin< + Box< + dyn Future< + Output = Result< + crate::lcm_summary_convergence::LcmSummaryConvergencePage, + LcmError, + >, + > + Send, + >, + > = match convergence_page { + LcmConvergencePage::PredecessorRangeRewrite => Box::pin( crate::lcm_summary_convergence::run_predecessor_range_rewrite_page( database.clone(), - ) - .await - } - LcmConvergencePage::Full => { + ), + ), + LcmConvergencePage::Full => Box::pin( crate::lcm_summary_convergence::run_summary_convergence_page( database.clone(), crate::lcm_summary_convergence::LCM_SUMMARY_CONVERGENCE_PAGE_LIMIT, - ) - .await - } - } + ), + ), }; tokio::pin!(page); let result = tokio::select! { @@ -1092,7 +1118,14 @@ async fn commit_deferred_begins<'a>( .map(|deferred| (deferred.item, deferred.prepared)) .unzip(); let outcomes = match commit_refresh_groups(items.len(), policy.operation_deadline, |range| { - access.commit_session_refresh_begin_batch_group(&items[range], state.completion_control()) + let committed = tracedecay_runtime_core::erase_send!( + access.commit_session_refresh_begin_batch_group( + &items[range], + state.completion_control(), + ), + SessionStoreResult>> + ); + committed }) .await { @@ -1161,7 +1194,11 @@ async fn drain_staged_completions<'a>( let receipt_control = state.completion_control(); let receipt_outcomes = match commit_refresh_groups(projections.len(), policy.operation_deadline, |range| { - access.record_relation_receipts_group(&projections[range], &receipt_control) + let recorded = tracedecay_runtime_core::erase_send!( + access.record_relation_receipts_group(&projections[range], &receipt_control), + SessionStoreResult>> + ); + recorded }) .await { @@ -1232,8 +1269,16 @@ async fn drain_staged_completions<'a>( .collect::>(); let outcomes = match commit_refresh_groups(complete_items.len(), policy.operation_deadline, |range| { - access - .complete_session_refresh_group(&complete_items[range], state.completion_control()) + let completed = tracedecay_runtime_core::erase_send!( + access.complete_session_refresh_group( + &complete_items[range], + state.completion_control(), + ), + SessionStoreResult< + Vec>, + > + ); + completed }) .await { @@ -1390,16 +1435,23 @@ async fn apply_prepared_refresh_effect( ) { match effect { SessionTemporalRefreshEffect::Projection { progress, batch } => { - match store - .commit_session_refresh_begin_batch( - prepared.request.clone(), - prepared.recovery.accepted_at(), - progress, - batch, - state.completion_control(), - ) - .await - { + let committed: Pin< + Box< + dyn Future< + Output = SessionStoreResult< + tracedecay_session_temporal_store::SessionRefreshBeginBatchOutcomeV1, + >, + > + Send + + '_, + >, + > = Box::pin(store.commit_session_refresh_begin_batch( + prepared.request.clone(), + prepared.recovery.accepted_at(), + progress, + batch, + state.completion_control(), + )); + match committed.await { Ok(SessionRefreshBeginBatchOutcomeV1::Persisted { .. }) => { prepared.disarm(); report.begun += 1; @@ -1430,10 +1482,17 @@ async fn apply_prepared_refresh_effect( SessionTemporalRefreshEffect::Fail(request) => { // The durable failure marker needs an operation row: begin through // the committing path, then retire it like any durable refresh. - match store - .begin_or_join_session_refresh(prepared.request.clone()) - .await - { + let begun: Pin< + Box< + dyn Future< + Output = SessionStoreResult< + tracedecay_store::SessionRefreshBeginOrJoinReceiptV1, + >, + > + Send + + '_, + >, + > = Box::pin(store.begin_or_join_session_refresh(prepared.request.clone())); + match begun.await { Ok(receipt) => { prepared.disarm(); match receipt.disposition() { @@ -1578,11 +1637,19 @@ async fn project_running_refresh<'a>( }); } effect => { - apply_prepared_refresh_effect(store, state, prepared, effect, report) - .await; + let applied: Pin + Send + '_>> = Box::pin( + apply_prepared_refresh_effect( + store, state, prepared, effect, report, + ), + ); + applied.await; } }, - None => apply_refresh_effect(store, state, &recovery, effect, report).await, + None => { + let applied: Pin + Send + '_>> = + Box::pin(apply_refresh_effect(store, state, &recovery, effect, report)); + applied.await; + } } } => {} } @@ -1663,16 +1730,23 @@ pub async fn run_session_temporal_refresh_pass( if state.cancelled.load(Ordering::Acquire) { return report; } - let mut prepared = process_refresh_begin_requests( - &store, - state, - policy.max_begin_requests_per_pass, - &mut report, - ) - .await; - let Some((mut recoveries, admitted, discovery_deferred)) = - recoveries_for_pass(database, &store, state, policy, &mut report).await - else { + let begins: Pin>> + Send + '_>> = + Box::pin(process_refresh_begin_requests( + &store, + state, + policy.max_begin_requests_per_pass, + &mut report, + )); + let mut prepared = begins.await; + let recovered = tracedecay_runtime_core::erase_send!( + recoveries_for_pass(database, &store, state, policy, &mut report,), + Option<( + Vec, + Vec>, + bool, + )> + ); + let Some((mut recoveries, admitted, discovery_deferred)) = recovered.await else { return report; }; prepared.extend(admitted); @@ -1741,29 +1815,31 @@ pub async fn run_session_temporal_refresh_pass( } match recovery.restart_state() { SessionRefreshRestartStateV1::ReadyToComplete => { - stage_ready_completion( - &access, - &store, - state, - policy, - recovery, - &mut drain, - &mut report, - ) - .await; + let staged: Pin + Send + '_>> = + Box::pin(stage_ready_completion( + &access, + &store, + state, + policy, + recovery, + &mut drain, + &mut report, + )); + staged.await; } SessionRefreshRestartStateV1::BeginProjection | SessionRefreshRestartStateV1::ResumeProjection { .. } => { - project_running_refresh( - &context, - ProjectionWorkItem { - recovery, - prepared: prepared_by_key.remove(&operation), - }, - &mut drain, - &mut report, - ) - .await; + let projected: Pin + Send + '_>> = + Box::pin(project_running_refresh( + &context, + ProjectionWorkItem { + recovery, + prepared: prepared_by_key.remove(&operation), + }, + &mut drain, + &mut report, + )); + projected.await; } } selection.complete(&operation); @@ -1771,17 +1847,21 @@ pub async fn run_session_temporal_refresh_pass( break; } } - commit_deferred_begins(&access, state, policy, drain.deferred_begins, &mut report).await; - drain_staged_completions( - database, - &access, - &store, - state, - policy, - drain.staged_completions, - &mut report, - ) - .await; + let committed: Pin + Send + '_>> = Box::pin( + commit_deferred_begins(&access, state, policy, drain.deferred_begins, &mut report), + ); + committed.await; + let drained: Pin + Send + '_>> = + Box::pin(drain_staged_completions( + database, + &access, + &store, + state, + policy, + drain.staged_completions, + &mut report, + )); + drained.await; let terminal = report .completed .saturating_add(report.failed) diff --git a/crates/tracedecay-session-temporal-store/src/lib.rs b/crates/tracedecay-session-temporal-store/src/lib.rs index 7c85b5ed5a..29750dc017 100644 --- a/crates/tracedecay-session-temporal-store/src/lib.rs +++ b/crates/tracedecay-session-temporal-store/src/lib.rs @@ -48,6 +48,8 @@ mod sql; pub mod store; use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::future::Future; +use std::pin::Pin; use tracedecay_contracts::retrieval::{ SessionRetrievalBudgetAccountingV1, SessionRetrievalBudgetObservationV1, @@ -944,52 +946,65 @@ impl<'db, D: SessionTemporalRegisteredDb + Sync> control.checkpoint().map_err(map_control_error)?; let read = self.open_read_snapshot().await?; let temporal_read = TemporalSqlRead::registered(&read); - let (participants, watermarks, cursor_key, prepared, readiness) = - match request.snapshot_request().retrieval_scope() { - TemporalRetrievalScope::Session(_) => { - let (participants, watermarks, cursor_key) = - freeze_participants(&temporal_read, request).await?; - (participants, watermarks, cursor_key, None, None) - } - TemporalRetrievalScope::AllSessionsInAuthorizedRoot => { - let relation_authority = self.db.session_relation_store().ok(); - let candidate_read = match &relation_authority { - Some((relation_scope, relation_store)) => { - SessionTemporalReadPort::new_registered_with_relations( - &read, - relation_scope, - relation_store.clone(), - ) - } - None => SessionTemporalReadPort::new_registered(&read), - }; - let plan = tracedecay_temporal_query::plan_temporal_candidates( - request.query(), - request.direct_anchor(), - request.snapshot_request().semantic_filter().goals, - ); - let prepared = candidate_read - .prepare_root_candidate_cohort(request.snapshot_request(), &plan) - .await - .map_err(map_control_error)?; - let readiness = root_readiness(&temporal_read, request).await?; - if prepared.candidates().is_empty() { - return Err(SessionTemporalExecutionError::Empty { - freshness: readiness, - }); + let (participants, watermarks, cursor_key, prepared, readiness) = match request + .snapshot_request() + .retrieval_scope() + { + TemporalRetrievalScope::Session(_) => { + let (participants, watermarks, cursor_key) = + freeze_participants(&temporal_read, request).await?; + (participants, watermarks, cursor_key, None, None) + } + TemporalRetrievalScope::AllSessionsInAuthorizedRoot => { + let relation_authority = self.db.session_relation_store().ok(); + let candidate_read = match &relation_authority { + Some((relation_scope, relation_store)) => { + SessionTemporalReadPort::new_registered_with_relations( + &read, + relation_scope, + relation_store.clone(), + ) } - let (participants, watermarks, cursor_key) = - freeze_prepared_candidate_participants(&temporal_read, request, &prepared) - .await?; - ( - participants, - watermarks, - cursor_key, - Some(prepared), - Some(readiness), - ) + None => SessionTemporalReadPort::new_registered(&read), + }; + let plan = tracedecay_temporal_query::plan_temporal_candidates( + request.query(), + request.direct_anchor(), + request.snapshot_request().semantic_filter().goals, + ); + let prepared_cohort: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_temporal_query::snapshot::TemporalPreparedCandidateCohort, + tracedecay_temporal_query::execution::TemporalPortError, + >, + > + Send + + '_, + >, + > = Box::pin(candidate_read.prepare_root_candidate_cohort( + request.snapshot_request(), + &plan, + )); + let prepared = prepared_cohort.await.map_err(map_control_error)?; + let readiness = root_readiness(&temporal_read, request).await?; + if prepared.candidates().is_empty() { + return Err(SessionTemporalExecutionError::Empty { + freshness: readiness, + }); } - }; + let (participants, watermarks, cursor_key) = + freeze_prepared_candidate_participants(&temporal_read, request, &prepared) + .await?; + ( + participants, + watermarks, + cursor_key, + Some(prepared), + Some(readiness), + ) + } + }; control.checkpoint().map_err(map_control_error)?; let mut snapshot = TemporalExecutionSnapshot::new_authorized( request.snapshot_request().clone(), diff --git a/crates/tracedecay-session-temporal-store/src/projection/tests.rs b/crates/tracedecay-session-temporal-store/src/projection/tests.rs index 9574be136f..3908b37ebb 100644 --- a/crates/tracedecay-session-temporal-store/src/projection/tests.rs +++ b/crates/tracedecay-session-temporal-store/src/projection/tests.rs @@ -2866,11 +2866,7 @@ fn sqlite_family_bytes(path: &std::path::Path) -> u64 { name.push(*suffix); std::path::PathBuf::from(name) }; - total.saturating_add( - std::fs::metadata(member) - .map(|meta| meta.len()) - .unwrap_or(0), - ) + total.saturating_add(std::fs::metadata(member).map_or(0, |meta| meta.len())) }) } diff --git a/crates/tracedecay-session-temporal-store/src/refresh.rs b/crates/tracedecay-session-temporal-store/src/refresh.rs index e700bcd684..e26976a0b7 100644 --- a/crates/tracedecay-session-temporal-store/src/refresh.rs +++ b/crates/tracedecay-session-temporal-store/src/refresh.rs @@ -7,6 +7,7 @@ use tracedecay_domain::{ TemporalCoverageCountsV1, TemporalModeV1, UtcMicros, }; use tracedecay_runtime_core::db::engine::{Row, params}; +use tracedecay_runtime_core::erase_send; use tracedecay_store::{ SessionFrozenWatermarksV1, SessionRefreshBeginOrJoinReceiptV1, SessionRefreshBeginOrJoinRequestV1, SessionRefreshCancellationRequestV1, @@ -1085,11 +1086,19 @@ impl SessionTemporalAccess<'_, D> { &self, request: SessionRefreshProgressRequestV1, ) -> SessionStoreResult> { - let snapshot = self - .read_snapshot() - .await - .map_err(|error| storage(READ_REFRESH, error))?; - read_progress(&snapshot, request.session_id(), request.operation_id()).await + let opened = erase_send!( + self.read_snapshot(), + Result< + tracedecay_runtime_core::db::DatabaseEngineReadSnapshot, + tracedecay_domain::TraceDecayError, + > + ); + let snapshot = opened.await.map_err(|error| storage(READ_REFRESH, error))?; + erase_send!( + read_progress(&snapshot, request.session_id(), request.operation_id()), + SessionStoreResult> + ) + .await } #[tracing::instrument( @@ -1526,11 +1535,19 @@ impl SessionTemporalAccess<'_, D> { &self, request: SessionRefreshReceiptRequestV1, ) -> SessionStoreResult> { - let snapshot = self - .read_snapshot() - .await - .map_err(|error| storage(READ_REFRESH, error))?; - read_receipt(&snapshot, request.session_id(), request.operation_id()).await + let opened = erase_send!( + self.read_snapshot(), + Result< + tracedecay_runtime_core::db::DatabaseEngineReadSnapshot, + tracedecay_domain::TraceDecayError, + > + ); + let snapshot = opened.await.map_err(|error| storage(READ_REFRESH, error))?; + erase_send!( + read_receipt(&snapshot, request.session_id(), request.operation_id()), + SessionStoreResult> + ) + .await } #[tracing::instrument( diff --git a/crates/tracedecay-session-temporal-store/src/retrieval.rs b/crates/tracedecay-session-temporal-store/src/retrieval.rs index a0be02ce68..3ed6393c01 100644 --- a/crates/tracedecay-session-temporal-store/src/retrieval.rs +++ b/crates/tracedecay-session-temporal-store/src/retrieval.rs @@ -1,4 +1,6 @@ use std::collections::{BTreeSet, HashMap, HashSet}; +use std::future::Future; +use std::pin::Pin; /// `(session_id, generation, provider)` -> the `(state, frozen_watermarks_json)` /// rows the join produced for that participant key. @@ -281,18 +283,17 @@ impl<'a> SessionTemporalReadPort<'a> { Some(field_caps), CANDIDATE_READ_BUDGET, ); - let status = await_controlled( - control, - Box::pin(self.produce_candidates_from_request( - &TemporalRetrievalScope::AllSessionsInAuthorizedRoot, - request, - 1, - plan, - &page_request, - &mut sink, - )), - ) - .await?; + let produced: Pin< + Box> + Send + '_>, + > = Box::pin(self.produce_candidates_from_request( + &TemporalRetrievalScope::AllSessionsInAuthorizedRoot, + request, + 1, + plan, + &page_request, + &mut sink, + )); + let status = await_controlled(control, produced).await?; let page = sink.finish(status)?; if page.status() == PageStatus::More && state.consumed_items() == limits.candidate_limit { diff --git a/crates/tracedecay-sessions/src/admission/mod.rs b/crates/tracedecay-sessions/src/admission/mod.rs index ff9386006a..771bee78c6 100644 --- a/crates/tracedecay-sessions/src/admission/mod.rs +++ b/crates/tracedecay-sessions/src/admission/mod.rs @@ -1500,8 +1500,7 @@ pub(crate) mod test_support { .find(|(stored_scope, stored_path, _)| { stored_scope == scope && stored_path == path }) - .map(|(_, _, offset)| *offset) - .unwrap_or_default(); + .map_or_default(|(_, _, offset)| *offset); if actual != expected { return Err(HostAdmissionOutcome::parse_offset_conflict()); } @@ -1532,8 +1531,7 @@ pub(crate) mod test_support { .find(|(stored_scope, stored_path, _)| { stored_scope == scope && stored_path == path }) - .map(|(_, _, offset)| *offset) - .unwrap_or_default() + .map_or_default(|(_, _, offset)| *offset) }; if actual(first.0) != first.1 || actual(second.0) != second.1 { return Err(HostAdmissionOutcome::parse_offset_conflict()); diff --git a/crates/tracedecay-sessions/src/runtime/git_correlation.rs b/crates/tracedecay-sessions/src/runtime/git_correlation.rs index ab7aca64b2..8a8bdeccad 100644 --- a/crates/tracedecay-sessions/src/runtime/git_correlation.rs +++ b/crates/tracedecay-sessions/src/runtime/git_correlation.rs @@ -867,22 +867,20 @@ pub fn canonical_observation_git_evidence( let session_id = envelope.relations().session_id().as_str().to_owned(); let timestamp = envelope.evidence().native_timestamp(); let worktree = worktrees.worktree_for(canonical_session_location(&envelope)); - let spans = timestamp - .map(|ts| { - vec![SpanObservation { - provider: provider.clone(), - session_id: session_id.clone(), - thread_id: envelope - .relations() - .thread_id() - .map(|thread_id| thread_id.as_str().to_owned()), - branch: branch.clone(), - worktree: worktree.clone(), - ts, - source: SpanSource::Ingest, - }] - }) - .unwrap_or_default(); + let spans = timestamp.map_or_default(|ts| { + vec![SpanObservation { + provider: provider.clone(), + session_id: session_id.clone(), + thread_id: envelope + .relations() + .thread_id() + .map(|thread_id| thread_id.as_str().to_owned()), + branch: branch.clone(), + worktree: worktree.clone(), + ts, + source: SpanSource::Ingest, + }] + }); if commit_references.is_empty() { return Ok((Vec::new(), spans)); } diff --git a/crates/tracedecay-sessions/src/runtime/git_correlation/backfill.rs b/crates/tracedecay-sessions/src/runtime/git_correlation/backfill.rs index bcf1cc8b21..fa63f9999b 100644 --- a/crates/tracedecay-sessions/src/runtime/git_correlation/backfill.rs +++ b/crates/tracedecay-sessions/src/runtime/git_correlation/backfill.rs @@ -904,14 +904,13 @@ fn plan_session<'r, G: GitReflogSource + ?Sized>( // boundaries within a segment. let analytics_within = analytics_ts .get(&(row.provider.clone(), row.session_id.clone())) - .map(|times| { + .map_or_default(|times| { times .iter() .copied() .filter(|&ts| ts >= win_start && ts <= win_end) .collect() - }) - .unwrap_or_default(); + }); let segments = window_branch_segments( win_start, win_end, diff --git a/crates/tracedecay-sessions/src/runtime/ingest/project.rs b/crates/tracedecay-sessions/src/runtime/ingest/project.rs index b0e62061ce..049b3ae999 100644 --- a/crates/tracedecay-sessions/src/runtime/ingest/project.rs +++ b/crates/tracedecay-sessions/src/runtime/ingest/project.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use std::future::Future; use std::path::{Path, PathBuf}; +use tracedecay_runtime_core::erase_send; use super::authority::{IngestAdmissionBinding, SessionIngestAuthority}; use crate::observation::ObservationCancellation; @@ -185,7 +186,6 @@ async fn plan_project_provider_rotation( registered: (&BrainId, &UserProfileId, &A), project_root: &Path, @@ -258,13 +258,14 @@ async fn ingest_project_sources_for_provider_bounded_inner planned, - Err(failure) => return IngestPassOutcome::failed(failure).into_transcript_outcome(), - }; + let planned = erase_send!( + plan_project_provider_rotation(&transcript_store, provider, selected.len(), bounds), + Result<(Option, RoundRobinAdmission), TranscriptCatchUpFailure> + ); + let (rotation_frontier, plan) = match planned.await { + Ok(planned) => planned, + Err(failure) => return IngestPassOutcome::failed(failure).into_transcript_outcome(), + }; // File-transcript multi-source scheduling is retired: every catch-up // provider is observation/port driven. Start at the rotation plan's // coverage and fold provider-run backpressure onto that baseline. @@ -332,13 +333,14 @@ async fn ingest_project_sources_for_provider_bounded_inner + ); + match drained.await { Ok(projection_stats) => { provider_runs.deferred_units = provider_runs .deferred_units @@ -390,8 +392,11 @@ async fn ingest_project_sources_for_provider_bounded_inner + ) + .await { provider_runs.failures.push(failure); } @@ -401,11 +406,14 @@ async fn ingest_project_sources_for_provider_bounded_inner( registered: (&BrainId, &UserProfileId, &A), registry_db: &A, @@ -220,7 +221,11 @@ async fn ingest_user_global_sources_for_startup_inner vec![TranscriptCatchUpFailure::pass_cancelled()], ); } - let Some(roots) = registered_project_roots_from(registry_db).await else { + let roots = erase_send!( + registered_project_roots_from(registry_db), + Option> + ); + let Some(roots) = roots.await else { return TranscriptIngestOutcome::new( TranscriptIngestStats::default(), vec![TranscriptCatchUpFailure::new( @@ -240,29 +245,34 @@ async fn ingest_user_global_sources_for_startup_inner let (brain_id, profile_id, registered) = registered; let outcome = match codex_discovery { Some((hub, consumer)) => { - ingest_user_global_sources_for_provider_with_roots_bounded_and_codex_state( - (brain_id, profile_id, registered), - profile_root, - None, - roots, - default_ingest_pass_bounds(), - cancellation, - (hub, consumer), - ) - .await - .into_transcript_outcome() + let ingested = erase_send!( + ingest_user_global_sources_for_provider_with_roots_bounded_and_codex_state( + (brain_id, profile_id, registered), + profile_root, + None, + roots, + default_ingest_pass_bounds(), + cancellation, + (hub, consumer), + ), + IngestPassOutcome + ); + ingested.await.into_transcript_outcome() } None => { - ingest_user_global_sources_for_provider_with_roots_and_cancellation( - brain_id, - profile_id, - registered, - profile_root, - None, - roots, - cancellation, - ) - .await + let ingested = erase_send!( + ingest_user_global_sources_for_provider_with_roots_and_cancellation( + brain_id, + profile_id, + registered, + profile_root, + None, + roots, + cancellation, + ), + TranscriptIngestOutcome + ); + ingested.await } }; if !outcome.is_success() { diff --git a/crates/tracedecay-sessions/src/runtime/ingest/user_provider.rs b/crates/tracedecay-sessions/src/runtime/ingest/user_provider.rs index 98870adfc4..f6b3d9dc76 100644 --- a/crates/tracedecay-sessions/src/runtime/ingest/user_provider.rs +++ b/crates/tracedecay-sessions/src/runtime/ingest/user_provider.rs @@ -1,7 +1,10 @@ use std::collections::BTreeSet; +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; use tracedecay_domain::ObservationScopeV1; +use tracedecay_runtime_core::erase_send; use crate::admission::HostAdmission; use crate::observation::ObservationCancellation; @@ -112,30 +115,66 @@ pub(super) struct UserProviderUnit<'a, S> { Option<(&'a crate::runtime::hosts::codex::CodexDiscoveryHub, &'a str)>, } -impl UserProviderUnit<'_, S> { - pub(super) async fn run(self) -> UserProviderRunResult { - if self.cancellation.is_cancelled() { - return UserProviderRunResult::provider(ProviderRunOutcome::skipped()); - } - match self.candidate { - SessionProvider::Codex => UserProviderRunResult::provider(self.run_codex().await), - SessionProvider::Cursor => UserProviderRunResult::provider(self.run_cursor().await), - SessionProvider::Hermes => UserProviderRunResult::provider(self.run_hermes().await), - SessionProvider::Claude => self.run_claude().await, - SessionProvider::Kiro => UserProviderRunResult::provider(self.run_kiro().await), - SessionProvider::Kimi => UserProviderRunResult::provider(self.run_kimi().await), - SessionProvider::OpenCode => UserProviderRunResult::provider(self.run_opencode().await), - SessionProvider::Cline | SessionProvider::RooCode | SessionProvider::Kilo => { - UserProviderRunResult::provider(self.run_cline_like().await) +impl<'a, S: TranscriptIngestStore + 'a> UserProviderUnit<'a, S> { + pub(super) fn run(self) -> Pin + Send + 'a>> { + Box::pin(async move { + if self.cancellation.is_cancelled() { + return UserProviderRunResult::provider(ProviderRunOutcome::skipped()); } - SessionProvider::Vibe => UserProviderRunResult::provider(self.run_vibe().await), - SessionProvider::Pi => UserProviderRunResult::provider(self.run_pi().await), - } + match self.candidate { + SessionProvider::Codex => { + let ran = erase_send!(self.run_codex(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + SessionProvider::Cursor => { + let ran = erase_send!(self.run_cursor(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + SessionProvider::Hermes => { + let ran = erase_send!(self.run_hermes(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + SessionProvider::Claude => { + let ran = erase_send!(self.run_claude(), UserProviderRunResult); + ran.await + } + SessionProvider::Kiro => { + let ran = erase_send!(self.run_kiro(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + SessionProvider::Kimi => { + let ran = erase_send!(self.run_kimi(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + SessionProvider::OpenCode => { + let ran = erase_send!(self.run_opencode(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + SessionProvider::Cline | SessionProvider::RooCode | SessionProvider::Kilo => { + let ran = erase_send!(self.run_cline_like(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + SessionProvider::Vibe => { + let ran = erase_send!(self.run_vibe(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + SessionProvider::Pi => { + let ran = erase_send!(self.run_pi(), ProviderRunOutcome); + UserProviderRunResult::provider(ran.await) + } + } + }) } #[tracing::instrument(name = "sessions.ingest.user.codex", level = "trace", skip_all)] async fn run_codex(self) -> ProviderRunOutcome { - let stored = match read_codex_discovery_frontier(self.store).await { + let read_frontier = erase_send!( + read_codex_discovery_frontier(self.store), + crate::runtime::source::TranscriptIngestResult< + crate::runtime::hosts::codex::CodexDiscoveryFrontier, + > + ); + let stored = match read_frontier.await { Ok(frontier) => frontier, Err(error) => { return ProviderRunOutcome::failed( @@ -149,39 +188,42 @@ impl UserProviderUnit<'_, S> { ); } }; - let stored_coverage = - match read_host_provider_coverage(self.facade, &ObservationScopeV1::Profile, "codex") - .await - { - Ok(coverage) => coverage, - Err(error) => { - return ProviderRunOutcome::failed( - warn_transcript_catch_up_failure( - "codex", - "coverage", - &error, - "user Codex coverage read failed", - ), - 0, - ); - } - }; + let read_coverage = erase_send!( + read_host_provider_coverage(self.facade, &ObservationScopeV1::Profile, "codex"), + crate::runtime::source::TranscriptIngestResult> + ); + let stored_coverage = match read_coverage.await { + Ok(coverage) => coverage, + Err(error) => { + return ProviderRunOutcome::failed( + warn_transcript_catch_up_failure( + "codex", + "coverage", + &error, + "user Codex coverage read failed", + ), + 0, + ); + } + }; let frontier = stored.for_coverage(matches!( stored_coverage, Some(HostProviderCoverage::Complete) )); - match try_ingest_user_codex_sessions_rotated( - self.profile_root, - None, - self.roots.to_vec(), - self.facade, - Some(self.max_new_bytes), - self.cancellation, - frontier, - self.codex_discovery, - ) - .await - { + let ingested = erase_send!( + try_ingest_user_codex_sessions_rotated( + self.profile_root, + None, + self.roots.to_vec(), + self.facade, + Some(self.max_new_bytes), + self.cancellation, + frontier, + self.codex_discovery, + ), + crate::runtime::source::TranscriptIngestResult + ); + match ingested.await { Ok(result) => { let outcome = result.outcome; let mut run = ProviderRunOutcome::bounded( @@ -196,8 +238,11 @@ impl UserProviderUnit<'_, S> { if let Some(next_frontier) = result .committable_frontier .filter(|next_frontier| *next_frontier != stored) - && let Err(error) = - write_codex_discovery_frontier(self.store, stored, next_frontier).await + && let persisted = erase_send!( + write_codex_discovery_frontier(self.store, stored, next_frontier), + crate::runtime::source::TranscriptIngestResult<()> + ) + && let Err(error) = persisted.await { frontier_persisted = false; run.add_deferred_units(1); @@ -213,16 +258,18 @@ impl UserProviderUnit<'_, S> { } else { HostProviderCoverage::Complete }; - if let Err(coverage_error) = persist_host_provider_coverage( - self.facade, - &ObservationScopeV1::Profile, - "codex", - coverage, - u64::from(coverage != HostProviderCoverage::Complete), - None, - ) - .await - { + let persisted_coverage = erase_send!( + persist_host_provider_coverage( + self.facade, + &ObservationScopeV1::Profile, + "codex", + coverage, + u64::from(coverage != HostProviderCoverage::Complete), + None, + ), + crate::runtime::source::TranscriptIngestResult<()> + ); + if let Err(coverage_error) = persisted_coverage.await { run.add_failure(warn_transcript_catch_up_failure( "codex", "coverage", diff --git a/crates/tracedecay-sessions/src/runtime/observation/snapshot_observation.rs b/crates/tracedecay-sessions/src/runtime/observation/snapshot_observation.rs index 781afa52ae..aa7328f0fe 100644 --- a/crates/tracedecay-sessions/src/runtime/observation/snapshot_observation.rs +++ b/crates/tracedecay-sessions/src/runtime/observation/snapshot_observation.rs @@ -248,11 +248,10 @@ pub fn canonical_snapshot_envelope( range: ObservationSourceRangeV1, ) -> Result { let invalid = || ObservationRecordParseErrorV1::NormalizationFailed; - let role = native - .get("role") - .and_then(Value::as_str) - .map(CanonicalMessageRoleV1::from_wire_label) - .unwrap_or(CanonicalMessageRoleV1::Unknown); + let role = native.get("role").and_then(Value::as_str).map_or( + CanonicalMessageRoleV1::Unknown, + CanonicalMessageRoleV1::from_wire_label, + ); let timestamp = native.get("timestamp").and_then(Value::as_i64); let mut facts = Vec::new(); let location = native.get("location").filter(|location| { diff --git a/crates/tracedecay-sessions/src/runtime/store_access/lcm.rs b/crates/tracedecay-sessions/src/runtime/store_access/lcm.rs index 184531221c..bc99790f0e 100644 --- a/crates/tracedecay-sessions/src/runtime/store_access/lcm.rs +++ b/crates/tracedecay-sessions/src/runtime/store_access/lcm.rs @@ -1,4 +1,6 @@ +use std::future::Future; use std::path::Path; +use std::pin::Pin; use tracedecay_domain::ObservationScopeV1; use tracedecay_runtime_core::db::DatabaseEngineReadSnapshot; @@ -245,16 +247,20 @@ impl<'a, D: SessionRegisteredDb + Sync> SessionStoreAccess<'a, D> { deep: bool, gc_config: &LcmGcConfig, ) -> Result { - let snapshot = self.lcm_read_snapshot().await?; - query::status( - &snapshot, - self.lcm_storage_root()?, - provider, - session_id, - deep, - gc_config, - ) - .await + let opened: Pin< + Box> + Send + '_>, + > = Box::pin(self.lcm_read_snapshot()); + let snapshot = opened.await?; + let status: Pin> + Send + '_>> = + Box::pin(query::status( + &snapshot, + self.lcm_storage_root()?, + provider, + session_id, + deep, + gc_config, + )); + status.await } /// The session scope LCM writes this store's session rows under. diff --git a/crates/tracedecay-sessions/src/runtime/workflow/workflow_index/port.rs b/crates/tracedecay-sessions/src/runtime/workflow/workflow_index/port.rs index ac843bbc1c..782d744a1e 100644 --- a/crates/tracedecay-sessions/src/runtime/workflow/workflow_index/port.rs +++ b/crates/tracedecay-sessions/src/runtime/workflow/workflow_index/port.rs @@ -20,7 +20,7 @@ pub trait WorkflowIngestWriteTxn: QueryExecutor + Executor + Sized + Send { /// The sweep needs an authority check, a watermark it can advance, and a /// run/agent upsert; it needs nothing about the registered database that backs /// them. -pub trait WorkflowIngestSink: Sync { +pub trait WorkflowIngestSink: Send + Sync { /// Whether the bound authority is the registered `ProjectSessions` shard /// for `project_id`. A mismatch aborts the sweep instead of writing. fn matches_project_sessions_authority(&self, project_id: &tracedecay_domain::ProjectId) diff --git a/crates/tracedecay-source-edit/src/edits/reconcile_tests.rs b/crates/tracedecay-source-edit/src/edits/reconcile_tests.rs index f870f46b58..353ece9d16 100644 --- a/crates/tracedecay-source-edit/src/edits/reconcile_tests.rs +++ b/crates/tracedecay-source-edit/src/edits/reconcile_tests.rs @@ -193,11 +193,8 @@ async fn prepared_roll_forward_keeps_journal_when_rollback_material_cannot_persi let store = fixture.transaction_store(); let obstruction = store.join("rollback-records"); std::fs::write(&obstruction, b"not a directory").unwrap(); - let receipts = || { - std::fs::read_dir(store.join("receipts")) - .map(|entries| entries.count()) - .unwrap_or(0) - }; + let receipts = + || std::fs::read_dir(store.join("receipts")).map_or(0, |entries| entries.count()); let receipts_before = receipts(); assert!( diff --git a/crates/tracedecay-source-edit/src/move_symbol/use_parsing.rs b/crates/tracedecay-source-edit/src/move_symbol/use_parsing.rs index 566759fb65..f7d7fdf907 100644 --- a/crates/tracedecay-source-edit/src/move_symbol/use_parsing.rs +++ b/crates/tracedecay-source-edit/src/move_symbol/use_parsing.rs @@ -169,9 +169,7 @@ fn parse_use_bindings(stmt_text: &str) -> Option<(bool, Vec)> { .map(|binding| UseLeaf { binding }) .collect() } else { - leaf_binding(body) - .map(|binding| vec![UseLeaf { binding }]) - .unwrap_or_default() + leaf_binding(body).map_or_default(|binding| vec![UseLeaf { binding }]) }; if leaves.is_empty() && !glob { return None; diff --git a/crates/tracedecay-store-runtime/src/retained_memory.rs b/crates/tracedecay-store-runtime/src/retained_memory.rs index 663d09bf04..4961467e46 100644 --- a/crates/tracedecay-store-runtime/src/retained_memory.rs +++ b/crates/tracedecay-store-runtime/src/retained_memory.rs @@ -1,3 +1,5 @@ +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; mod target; @@ -34,6 +36,7 @@ use tracedecay_contracts::retained_receipts::{ effective_memory_deadline, evidence_outcome, memory_expiry_partial, prepare_retained_effect, }; use tracedecay_runtime_core::db::Database; +use tracedecay_runtime_core::erase_send; use tracedecay_session_memory::fact_store::DatabaseFactStore; use tracedecay_session_memory::memory_mapping::{self, FactPageOperation}; use tracedecay_session_memory::memory_mutation::{ @@ -55,46 +58,80 @@ macro_rules! execute_scoped_memory { ) => {{ match &$port.authority { DirectRetainedMemoryAuthorityV1::Project { authority } => { - let (target, _) = bounded_memory_operation($context, async { - open_project_retained_memory_target( - authority, - &authority.project_root, - &$context.request_context.scope().project_id, - $memory_scope, - $selector, - $access, - ) - .await - }) - .await?; - $executor( + let opened: Pin< + Box< + dyn Future< + Output = Result< + target::RetainedMemoryTargetV1<'static>, + RetainedSurfaceExecutionErrorV1, + >, + > + Send + + '_, + >, + > = Box::pin(open_project_retained_memory_target( + authority, + &authority.project_root, + &$context.request_context.scope().project_id, + $memory_scope, + $selector, + $access, + )); + let (target, _) = bounded_memory_operation($context, opened).await?; + let executed: Pin< + Box< + dyn Future< + Output = Result< + ApplicationOutcome, + RetainedSurfaceExecutionErrorV1, + >, + > + Send + + '_, + >, + > = Box::pin($executor( $context, target.database(), target.owner().clone(), $request, &$port.configuration_digest, - ) - .await + )); + executed.await } DirectRetainedMemoryAuthorityV1::Profile { registry } => { memory_mapping::ensure_profile_request_scope($memory_scope, $selector)?; - let (database, _) = bounded_memory_operation($context, async { + let opened: Pin< + Box< + dyn Future>> + + Send + + '_, + >, + > = Box::pin(registry.profile_memory()); + let (database, _) = bounded_memory_operation($context, async move { tracing::Instrument::instrument( - registry.profile_memory(), + opened, tracing::trace_span!("daemon.retained.memory.open_profile"), ) .await .map_err(map_execution_error) }) .await?; - $executor( + let executed: Pin< + Box< + dyn Future< + Output = Result< + ApplicationOutcome, + RetainedSurfaceExecutionErrorV1, + >, + > + Send + + '_, + >, + > = Box::pin($executor( $context, &database, FactOwnerV1::Profile, $request, &$port.configuration_digest, - ) - .await + )); + executed.await } } }}; @@ -285,52 +322,74 @@ impl<'a> DirectRetainedMemoryPortV1<'a> { } impl DirectRetainedMemoryPortV1<'_> { - pub async fn execute_request( - &self, - context: RetainedSurfaceExecutionContextV1<'_>, - request: RetainedMemoryRequestV1<'_>, - ) -> Result, RetainedSurfaceExecutionErrorV1> { - match request { - RetainedMemoryRequestV1::FactStoreAdd(request) => { - self.execute_add(&context, request).await - } - RetainedMemoryRequestV1::FactStoreSearch(request) => { - self.execute_read(&context, Read::Search(request)).await - } - RetainedMemoryRequestV1::FactStoreProbe(request) => { - self.execute_read(&context, Read::Probe(request)).await - } - RetainedMemoryRequestV1::FactStoreRelated(request) => { - self.execute_read(&context, Read::Related(request)).await - } - RetainedMemoryRequestV1::FactStoreReason(request) => { - self.execute_read(&context, Read::Reason(request)).await - } - RetainedMemoryRequestV1::FactStoreContradict(request) => { - self.execute_read(&context, Read::Contradict(request)).await - } - RetainedMemoryRequestV1::FactStoreGet(request) => { - self.execute_read(&context, Read::Get(request)).await - } - RetainedMemoryRequestV1::FactStoreUpdate(request) => { - self.execute_update(&context, request).await - } - RetainedMemoryRequestV1::FactStoreRemove(request) => { - self.execute_remove(&context, request).await - } - RetainedMemoryRequestV1::FactStoreSupersede(request) => { - self.execute_supersede(&context, request).await - } - RetainedMemoryRequestV1::FactStoreList(request) => { - self.execute_read(&context, Read::List(request)).await - } - RetainedMemoryRequestV1::FactFeedback(request) => { - self.execute_feedback(&context, request).await - } - RetainedMemoryRequestV1::MemoryStatus(request) => { - self.execute_status(&context, request).await - } - } + pub fn execute_request<'a>( + &'a self, + context: RetainedSurfaceExecutionContextV1<'a>, + request: RetainedMemoryRequestV1<'a>, + ) -> Pin< + Box< + dyn Future< + Output = Result< + ApplicationOutcome, + RetainedSurfaceExecutionErrorV1, + >, + > + Send + + 'a, + >, + > { + Box::pin(async move { + let executed: Pin< + Box< + dyn Future< + Output = Result< + ApplicationOutcome, + RetainedSurfaceExecutionErrorV1, + >, + > + Send, + >, + > = match request { + RetainedMemoryRequestV1::FactStoreAdd(request) => { + Box::pin(self.execute_add(&context, request)) + } + RetainedMemoryRequestV1::FactStoreSearch(request) => { + Box::pin(self.execute_read(&context, Read::Search(request))) + } + RetainedMemoryRequestV1::FactStoreProbe(request) => { + Box::pin(self.execute_read(&context, Read::Probe(request))) + } + RetainedMemoryRequestV1::FactStoreRelated(request) => { + Box::pin(self.execute_read(&context, Read::Related(request))) + } + RetainedMemoryRequestV1::FactStoreReason(request) => { + Box::pin(self.execute_read(&context, Read::Reason(request))) + } + RetainedMemoryRequestV1::FactStoreContradict(request) => { + Box::pin(self.execute_read(&context, Read::Contradict(request))) + } + RetainedMemoryRequestV1::FactStoreGet(request) => { + Box::pin(self.execute_read(&context, Read::Get(request))) + } + RetainedMemoryRequestV1::FactStoreUpdate(request) => { + Box::pin(self.execute_update(&context, request)) + } + RetainedMemoryRequestV1::FactStoreRemove(request) => { + Box::pin(self.execute_remove(&context, request)) + } + RetainedMemoryRequestV1::FactStoreSupersede(request) => { + Box::pin(self.execute_supersede(&context, request)) + } + RetainedMemoryRequestV1::FactStoreList(request) => { + Box::pin(self.execute_read(&context, Read::List(request))) + } + RetainedMemoryRequestV1::FactFeedback(request) => { + Box::pin(self.execute_feedback(&context, request)) + } + RetainedMemoryRequestV1::MemoryStatus(request) => { + Box::pin(self.execute_status(&context, request)) + } + }; + executed.await + }) } } @@ -340,7 +399,7 @@ impl RetainedMemoryExecutionPortV1 for DirectRetainedMemoryPortV1<'_> { context: RetainedSurfaceExecutionContextV1<'a>, request: RetainedMemoryRequestV1<'a>, ) -> RetainedSurfaceExecutionFutureV1<'a> { - Box::pin(async move { self.execute_request(context, request).await }) + self.execute_request(context, request) } } @@ -368,9 +427,18 @@ async fn execute_add_on_db( &operation_id, )?; let write_control = fact_write_control(context); + let committed = erase_send!( + memory.add_preflighted_project_memory_fact(preflight, &write_control), + Result< + ProjectMemoryFactAddRequestOutcome, + tracedecay_session_memory::memory::MemoryMutationError< + ProjectMemoryFactAddRequestOutcome, + >, + > + ); let (outcome, settled_after_expiry) = bounded_memory_operation(context, async { Ok(tracing::Instrument::instrument( - memory.add_preflighted_project_memory_fact(preflight, &write_control), + committed, tracing::trace_span!("daemon.retained.memory.add.commit"), ) .await) @@ -435,9 +503,18 @@ async fn execute_update_on_db( context.request_context.actor().clone(), )?; let write_control = fact_write_control(context); + let committed = erase_send!( + memory.update_project_memory_fact(command, &write_control), + Result< + tracedecay_store::ProjectMemoryFactUpdateOutcomeV1, + tracedecay_session_memory::memory::MemoryMutationError< + tracedecay_store::ProjectMemoryFactUpdateOutcomeV1, + >, + > + ); let (outcome, settled_after_expiry) = bounded_memory_operation(context, async { Ok(tracing::Instrument::instrument( - memory.update_project_memory_fact(command, &write_control), + committed, tracing::trace_span!("daemon.retained.memory.update.commit"), ) .await) @@ -487,9 +564,18 @@ async fn execute_remove_on_db( context.request_context.actor().clone(), )?; let write_control = fact_write_control(context); + let committed = erase_send!( + memory.remove_project_memory_fact(command, &write_control), + Result< + tracedecay_store::ProjectMemoryFactRemoveOutcomeV1, + tracedecay_session_memory::memory::MemoryMutationError< + tracedecay_store::ProjectMemoryFactRemoveOutcomeV1, + >, + > + ); let (outcome, settled_after_expiry) = bounded_memory_operation(context, async { Ok(tracing::Instrument::instrument( - memory.remove_project_memory_fact(command, &write_control), + committed, tracing::trace_span!("daemon.retained.memory.remove.commit"), ) .await) @@ -555,9 +641,18 @@ async fn execute_supersede_on_db( context.request_context.actor().clone(), )?; let write_control = fact_write_control(context); + let committed = erase_send!( + memory.supersede_project_memory_fact(command, &write_control), + Result< + tracedecay_store::ProjectMemoryFactSupersedeOutcomeV1, + tracedecay_session_memory::memory::MemoryMutationError< + tracedecay_store::ProjectMemoryFactSupersedeOutcomeV1, + >, + > + ); let (outcome, settled_after_expiry) = bounded_memory_operation(context, async { Ok(tracing::Instrument::instrument( - memory.supersede_project_memory_fact(command, &write_control), + committed, tracing::trace_span!("daemon.retained.memory.supersede.commit"), ) .await) @@ -615,9 +710,18 @@ async fn execute_feedback_on_db( context.request_context.actor().clone(), )?; let write_control = fact_write_control(context); + let committed = erase_send!( + memory.record_project_memory_fact_feedback(command, &write_control), + Result< + tracedecay_store::ProjectMemoryFactFeedbackOutcomeV1, + tracedecay_session_memory::memory::MemoryMutationError< + tracedecay_store::ProjectMemoryFactFeedbackOutcomeV1, + >, + > + ); let (outcome, settled_after_expiry) = bounded_memory_operation(context, async { Ok(tracing::Instrument::instrument( - memory.record_project_memory_fact_feedback(command, &write_control), + committed, tracing::trace_span!("daemon.retained.memory.feedback.commit"), ) .await) @@ -685,9 +789,16 @@ async fn search_on_db( MemoryOperationContext::from_request_id(&owner, "search", request_id, actor) .map_err(memory_mapping::map_memory_error)?; let read_control = fact_read_control(context); + let searched = erase_send!( + memory.search_project_memory_facts(query, &read_control), + Result< + tracedecay_store::ProjectMemoryFactSearchPageV1, + tracedecay_session_memory::memory::MemoryApplicationError, + > + ); let (page, _) = bounded_memory_operation(context, async { tracing::Instrument::instrument( - memory.search_project_memory_facts(query, &read_control), + searched, tracing::trace_span!("daemon.retained.memory.search.query"), ) .await @@ -895,31 +1006,42 @@ async fn semantic_search_on_db( let memory = memory_application(database, owner.clone())?; let query = memory_mapping::search_query(owner, kind, query_text, options, after, &binding)?; let read_control = fact_read_control(context); + let (span, queried) = match request { + SemanticRead::Probe(_) => ( + tracing::trace_span!("daemon.retained.memory.probe"), + erase_send!( + memory.probe_project_memory_facts(query, &read_control), + Result< + tracedecay_store::ProjectMemoryFactSearchPageV1, + tracedecay_session_memory::memory::MemoryApplicationError, + > + ), + ), + SemanticRead::Related(_) => ( + tracing::trace_span!("daemon.retained.memory.related"), + erase_send!( + memory.related_project_memory_facts(query, &read_control), + Result< + tracedecay_store::ProjectMemoryFactSearchPageV1, + tracedecay_session_memory::memory::MemoryApplicationError, + > + ), + ), + SemanticRead::Reason(_) => ( + tracing::trace_span!("daemon.retained.memory.reason"), + erase_send!( + memory.reason_project_memory_facts(query, &read_control), + Result< + tracedecay_store::ProjectMemoryFactSearchPageV1, + tracedecay_session_memory::memory::MemoryApplicationError, + > + ), + ), + }; let (page, _) = bounded_memory_operation(context, async { - let page = match request { - SemanticRead::Probe(_) => { - tracing::Instrument::instrument( - memory.probe_project_memory_facts(query, &read_control), - tracing::trace_span!("daemon.retained.memory.probe"), - ) - .await - } - SemanticRead::Related(_) => { - tracing::Instrument::instrument( - memory.related_project_memory_facts(query, &read_control), - tracing::trace_span!("daemon.retained.memory.related"), - ) - .await - } - SemanticRead::Reason(_) => { - tracing::Instrument::instrument( - memory.reason_project_memory_facts(query, &read_control), - tracing::trace_span!("daemon.retained.memory.reason"), - ) - .await - } - }; - page.map_err(memory_mapping::map_memory_error) + tracing::Instrument::instrument(queried, span) + .await + .map_err(memory_mapping::map_memory_error) }) .await?; let mapped = memory_mapping::search_page(&page, &binding)?; @@ -942,9 +1064,16 @@ async fn contradict_on_db( ) .map_err(memory_mapping::map_store_error)?; let read_control = fact_read_control(context); + let contradictions = erase_send!( + memory.find_project_memory_contradictions(query, &read_control), + Result< + tracedecay_store::ProjectMemoryFactContradictionPageV1, + tracedecay_session_memory::memory::MemoryApplicationError, + > + ); let (page, _) = bounded_memory_operation(context, async { tracing::Instrument::instrument( - memory.find_project_memory_contradictions(query, &read_control), + contradictions, tracing::trace_span!("daemon.retained.memory.contradict.query"), ) .await @@ -970,9 +1099,16 @@ async fn get_on_db( let target = ProjectMemoryFactIdV1::new(owner, request.fact_id.clone()) .map_err(memory_mapping::map_store_error)?; let read_control = fact_read_control(context); + let loaded = erase_send!( + memory.get_project_memory_fact(target.clone(), &read_control), + Result< + Option, + tracedecay_session_memory::memory::MemoryApplicationError, + > + ); let (projection, _) = bounded_memory_operation(context, async { tracing::Instrument::instrument( - memory.get_project_memory_fact(target.clone(), &read_control), + loaded, tracing::trace_span!("daemon.retained.memory.get.fact"), ) .await @@ -986,9 +1122,16 @@ async fn get_on_db( memory_mapping::MAX_RETAINED_FEEDBACK_HISTORY_LIMIT, ) .map_err(memory_mapping::map_store_error)?; + let history_page = erase_send!( + memory.get_project_memory_feedback_history(history_query, &read_control), + Result< + tracedecay_store::ProjectMemoryFactFeedbackHistoryV1, + tracedecay_session_memory::memory::MemoryApplicationError, + > + ); let (history, _) = bounded_memory_operation(context, async { tracing::Instrument::instrument( - memory.get_project_memory_feedback_history(history_query, &read_control), + history_page, tracing::trace_span!("daemon.retained.memory.get.history"), ) .await @@ -1023,9 +1166,16 @@ async fn list_on_db( ) .map_err(memory_mapping::map_store_error)?; let read_control = fact_read_control(context); + let listed = erase_send!( + memory.list_project_memory_facts(query, &read_control), + Result< + tracedecay_store::ProjectMemoryFactPageV1, + tracedecay_session_memory::memory::MemoryApplicationError, + > + ); let (page, _) = bounded_memory_operation(context, async { tracing::Instrument::instrument( - memory.list_project_memory_facts(query, &read_control), + listed, tracing::trace_span!("daemon.retained.memory.list.query"), ) .await @@ -1046,9 +1196,20 @@ async fn execute_status_on_db( ) -> Result, RetainedSurfaceExecutionErrorV1> { let memory = memory_application(database, owner)?; let read_control = fact_read_control(context); + let status_query: Pin< + Box< + dyn Future< + Output = Result< + tracedecay_store::ProjectMemoryMemoryStatusV1, + tracedecay_session_memory::memory::MemoryApplicationError, + >, + > + Send + + '_, + >, + > = Box::pin(memory.project_memory_status(&read_control)); let (status, _) = bounded_memory_operation(context, async { tracing::Instrument::instrument( - memory.project_memory_status(&read_control), + status_query, tracing::trace_span!("daemon.retained.memory.status.query"), ) .await diff --git a/crates/tracedecay-store-runtime/src/retained_memory/target.rs b/crates/tracedecay-store-runtime/src/retained_memory/target.rs index b292d3c4af..6b0dc0d89c 100644 --- a/crates/tracedecay-store-runtime/src/retained_memory/target.rs +++ b/crates/tracedecay-store-runtime/src/retained_memory/target.rs @@ -1,6 +1,8 @@ //! Exact retained-memory target selection for one admitted profile. +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; use std::sync::Arc; use tracedecay_contracts::retained_surfaces::{MemoryScopeV1, RetainedProjectSelectorV1}; @@ -75,11 +77,10 @@ pub async fn open_project_retained_memory_target( if selector.is_some() { return denied(); } - let database = authority - .registry - .profile_memory() - .await - .map_err(map_execution_error)?; + let opened: Pin< + Box>> + Send + '_>, + > = Box::pin(authority.registry.profile_memory()); + let database = opened.await.map_err(map_execution_error)?; return Ok(RetainedMemoryTargetV1::new( ProjectMemoryDbHandle::Owned(Box::new(Arc::unwrap_or_clone(database))), FactOwnerV1::Profile, @@ -141,7 +142,21 @@ pub async fn open_project_retained_memory_target( }, )); } - open_selected_project_read_only(authority, selected_project_id).await + let opened: Pin< + Box< + dyn Future< + Output = Result< + RetainedMemoryTargetV1<'static>, + RetainedSurfaceExecutionErrorV1, + >, + > + Send + + '_, + >, + > = Box::pin(open_selected_project_read_only( + authority, + selected_project_id, + )); + opened.await } #[tracing::instrument( @@ -153,9 +168,21 @@ async fn open_selected_project_read_only( authority: &RetainedMemoryTargetAuthorityV1, selected_project_id: &ProjectId, ) -> Result, RetainedSurfaceExecutionErrorV1> { - let context = authority - .profile_database - .project_registry_context_by_id(selected_project_id.as_str()) + let loaded: Pin< + Box< + dyn Future< + Output = tracedecay_domain::errors::Result< + Option, + >, + > + Send + + '_, + >, + > = Box::pin( + authority + .profile_database + .project_registry_context_by_id(selected_project_id.as_str()), + ); + let context = loaded .await .map_err(map_target_infrastructure_error)? .ok_or(RetainedSurfaceExecutionErrorV1::NotFoundOrNotAuthorized)?; @@ -170,11 +197,14 @@ async fn open_selected_project_read_only( if roots.is_empty() { return denied(); } - let database = authority - .registry - .project_memory_read_only(selected_project_id.clone(), roots) - .await - .map_err(map_target_infrastructure_error)?; + let opened: Pin< + Box> + Send + '_>, + > = Box::pin( + authority + .registry + .project_memory_read_only(selected_project_id.clone(), roots), + ); + let database = opened.await.map_err(map_target_infrastructure_error)?; let exact_scope = matches!( &database.registered_binding().shard_id.scope, StoreShardScopeV1::Project { project_id } if project_id == selected_project_id diff --git a/crates/tracedecay-store-runtime/src/session_registry.rs b/crates/tracedecay-store-runtime/src/session_registry.rs index 74955dfc03..c8a28b52bb 100644 --- a/crates/tracedecay-store-runtime/src/session_registry.rs +++ b/crates/tracedecay-store-runtime/src/session_registry.rs @@ -1018,6 +1018,106 @@ impl ProjectSessionTerminalProofV1 { } } +// These owner states nest a `BTreeMap` witness into retirement receipts. One +// combined `Send` proof exceeds the recursion limit (rust-lang/rust#159228); +// each field below is checked on its own. +unsafe impl Send for ProjectSessionClosedRetirementProofV1 {} + +const _: fn(&ProjectSessionClosedRetirementProofV1) = |value| { + fn assert_send(_: &T) {} + let ProjectSessionClosedRetirementProofV1 { + binding, + locator, + graph, + store, + } = value; + assert_send(binding); + assert_send(locator); + assert_send(graph); + assert_send(store); +}; + +unsafe impl Send for ProjectSessionTerminalProofV1 {} + +const _: fn(&ProjectSessionTerminalProofV1) = |value| { + fn assert_send(_: &T) {} + match value { + ProjectSessionTerminalProofV1::Live(proof) => assert_send(proof), + } +}; + +unsafe impl Send for ProjectSessionRecoveryPhaseV1 {} + +const _: fn(&ProjectSessionRecoveryPhaseV1) = |value| { + fn assert_send(_: &T) {} + match value { + ProjectSessionRecoveryPhaseV1::ReservationAbandoned + | ProjectSessionRecoveryPhaseV1::GraphNativeBoundary => {} + ProjectSessionRecoveryPhaseV1::GraphTerminal(graph) => assert_send(graph), + ProjectSessionRecoveryPhaseV1::Terminal(proof) => assert_send(proof), + } +}; + +unsafe impl Send for ProjectSessionRecoveryRequiredV1 {} + +const _: fn(&ProjectSessionRecoveryRequiredV1) = |value| { + fn assert_send(_: &T) {} + let ProjectSessionRecoveryRequiredV1 { + sessions, + candidate_sessions, + memory, + phase, + } = value; + assert_send(sessions); + assert_send(candidate_sessions); + assert_send(memory); + assert_send(phase); +}; + +unsafe impl Send for ProjectRuntimeRetirementFaultV1 {} + +const _: fn(&ProjectRuntimeRetirementFaultV1) = |value| { + fn assert_send(_: &T) {} + match value { + ProjectRuntimeRetirementFaultV1::ReservationTargetConsumed => {} + ProjectRuntimeRetirementFaultV1::Reconciliation(terminal) => assert_send(terminal), + ProjectRuntimeRetirementFaultV1::GraphRefusal(error) => assert_send(error), + ProjectRuntimeRetirementFaultV1::StoreStart(error) => assert_send(error), + ProjectRuntimeRetirementFaultV1::Terminal { graph, store } => { + assert_send(graph); + assert_send(store); + } + } +}; + +unsafe impl Send for ProjectRuntimeFaultedOwnersV1 {} + +const _: fn(&ProjectRuntimeFaultedOwnersV1) = |value| { + fn assert_send(_: &T) {} + let ProjectRuntimeFaultedOwnersV1 { + retained, + sessions, + fault, + } = value; + assert_send(retained); + assert_send(sessions); + assert_send(fault); +}; + +unsafe impl Send for ProjectRuntimeOwnerStateV1 {} + +const _: fn(&ProjectRuntimeOwnerStateV1) = |value| { + fn assert_send(_: &T) {} + match value { + ProjectRuntimeOwnerStateV1::Opening + | ProjectRuntimeOwnerStateV1::ReplacingSessions + | ProjectRuntimeOwnerStateV1::Retiring => {} + ProjectRuntimeOwnerStateV1::Ready(owners) => assert_send(owners), + ProjectRuntimeOwnerStateV1::RecoveryRequired(recovery) => assert_send(recovery), + ProjectRuntimeOwnerStateV1::Faulted(faulted) => assert_send(faulted), + } +}; + enum ProjectRuntimeRetirementFaultV1 { ReservationTargetConsumed, Reconciliation(MemoryGraphReconciliationRetirementTerminalV1), @@ -2618,6 +2718,74 @@ pub struct DaemonSessionRuntimeRegistryV1 { session_graph_publication_gate: StdMutex>>, } +// Each field is `Send + Sync`, but proving the registry walks those fields +// through `Arc>>` past the recursion limit +// (rust-lang/rust#159228). The const is the field proof. +unsafe impl Send for DaemonSessionRuntimeRegistryV1 {} +unsafe impl Sync for DaemonSessionRuntimeRegistryV1 {} + +const _: fn(&DaemonSessionRuntimeRegistryV1) = |value| { + fn assert_send_sync(_: &T) {} + let DaemonSessionRuntimeRegistryV1 { + identity, + incarnation, + resolver, + registry, + graph_registry, + graph_manifest_provider, + graph_lifecycle_cancelled, + operation_task_owner, + profile_pin, + profile_database_mount, + profile_database, + profile_memory, + profile_sessions_mount, + profile_sessions, + remote_nodes, + remote_credential_authority, + remote_replay_transaction, + remote_recovery_authorities, + project_owners, + code_graph_publication_gates, + registered_schema_convergence, + reset_required_stores, + retained_hook_tasks, + session_sync_service, + remote_recovery_project_lifecycle, + long_lived_session_maintenance, + #[cfg(any(test, feature = "test-helpers"))] + session_graph_publication_gate, + } = value; + assert_send_sync(identity); + assert_send_sync(incarnation); + assert_send_sync(resolver); + assert_send_sync(registry); + assert_send_sync(graph_registry); + assert_send_sync(graph_manifest_provider); + assert_send_sync(graph_lifecycle_cancelled); + assert_send_sync(operation_task_owner); + assert_send_sync(profile_pin); + assert_send_sync(profile_database_mount); + assert_send_sync(profile_database); + assert_send_sync(profile_memory); + assert_send_sync(profile_sessions_mount); + assert_send_sync(profile_sessions); + assert_send_sync(remote_nodes); + assert_send_sync(remote_credential_authority); + assert_send_sync(remote_replay_transaction); + assert_send_sync(remote_recovery_authorities); + assert_send_sync(project_owners); + assert_send_sync(code_graph_publication_gates); + assert_send_sync(registered_schema_convergence); + assert_send_sync(reset_required_stores); + assert_send_sync(retained_hook_tasks); + assert_send_sync(session_sync_service); + assert_send_sync(remote_recovery_project_lifecycle); + assert_send_sync(long_lived_session_maintenance); + #[cfg(any(test, feature = "test-helpers"))] + assert_send_sync(session_graph_publication_gate); +}; + impl DaemonSessionRuntimeRegistryV1 { #[cfg(any(test, feature = "test-helpers"))] pub fn lookup_store_runtime( diff --git a/crates/tracedecay-store-runtime/src/session_registry/code_graph.rs b/crates/tracedecay-store-runtime/src/session_registry/code_graph.rs index 04489f6993..f942627976 100644 --- a/crates/tracedecay-store-runtime/src/session_registry/code_graph.rs +++ b/crates/tracedecay-store-runtime/src/session_registry/code_graph.rs @@ -1,4 +1,6 @@ use std::cell::RefCell; +use std::future::Future; +use std::pin::Pin; #[cfg(any(test, feature = "test-helpers"))] use std::sync::atomic::AtomicUsize; use std::sync::{ @@ -2525,7 +2527,9 @@ impl DaemonSessionRuntimeRegistryV1 { && project_shard.brain_id == *self.identity.brain_id() && project_shard.profile_id == *self.identity.profile_id() { - match self.project_owners.wait_for_memory_graph(project_id).await { + let waited: Pin> + Send + '_>> = + Box::pin(self.project_owners.wait_for_memory_graph(project_id)); + match waited.await { Ok(false) => {} Ok(true) => return, Err(error) => { @@ -2539,15 +2543,20 @@ impl DaemonSessionRuntimeRegistryV1 { } } } - match graph_attachment::open_session_relation_owner( - &self.registry, - &self.graph_registry, - &self.graph_lifecycle_cancelled, - self.incarnation, - project_shard.clone(), - ) - .await - { + let opened = tracedecay_runtime_core::erase_send!( + graph_attachment::open_session_relation_owner( + &self.registry, + &self.graph_registry, + &self.graph_lifecycle_cancelled, + self.incarnation, + project_shard.clone(), + ), + Result<( + GraphDbOwnerAttachmentV1, + CanonicalGraphStoreOwnerRetirementTargetV1, + )> + ); + match opened.await { Ok((attachment, target)) => { tracing::info!( event = "code_graph_shard_reattached", @@ -2608,7 +2617,11 @@ impl DaemonSessionRuntimeRegistryV1 { self.identity.profile_id().clone(), project_id.clone(), ); - self.ensure_code_graph_shard_attached(&project_shard).await; + { + let attached: Pin + Send + '_>> = + Box::pin(self.ensure_code_graph_shard_attached(&project_shard)); + attached.await; + } let code_scope = match reference { Some(ref_id) => CodeShardScopeV1::Branch { worktree_id, @@ -2623,17 +2636,24 @@ impl DaemonSessionRuntimeRegistryV1 { repository_id.clone(), code_scope, ); - let authority = self - .registry - .retain_code_graph_store( - StoreRuntimeKey::new(project_shard.clone(), self.incarnation), - code_shard.clone(), - generation_id.clone(), - ) - .await - .map_err(|failure| { - session_registry_error("retain exact code graph authority", format!("{failure:?}")) - })?; + let retained_store: Pin< + Box< + dyn Future< + Output = std::result::Result< + Arc, + tracedecay_runtime_core::shard_runtime::registry::StoreRuntimeRegistryFailure, + >, + > + Send + + '_, + >, + > = Box::pin(self.registry.retain_code_graph_store( + StoreRuntimeKey::new(project_shard.clone(), self.incarnation), + code_shard.clone(), + generation_id.clone(), + )); + let authority = retained_store.await.map_err(|failure| { + session_registry_error("retain exact code graph authority", format!("{failure:?}")) + })?; // The pre-retain heal above keys the freshly reconstructed project // shard, but every later publish/recover lease keys the registry by // this lease's binding (`registered_database` uses @@ -2649,7 +2669,11 @@ impl DaemonSessionRuntimeRegistryV1 { bound = ?bound_shard, "code graph lease binding names a different shard than the reconstructed project shard" ); - self.ensure_code_graph_shard_attached(&bound_shard).await; + { + let attached: Pin + Send + '_>> = + Box::pin(self.ensure_code_graph_shard_attached(&bound_shard)); + attached.await; + } } let replay_root = project_database .database_path() @@ -2709,7 +2733,11 @@ impl DaemonSessionRuntimeRegistryV1 { self.identity.profile_id().clone(), project_id.clone(), ); - self.ensure_code_graph_shard_attached(&project_shard).await; + { + let attached: Pin + Send + '_>> = + Box::pin(self.ensure_code_graph_shard_attached(&project_shard)); + attached.await; + } let authority = self .registry .retain_graph_store(StoreRuntimeKey::new( @@ -2720,7 +2748,11 @@ impl DaemonSessionRuntimeRegistryV1 { .map_err(|error| GraphDbError::unavailable(format!("{error:?}")))?; let bound_shard = authority.binding().shard_id.clone(); if bound_shard != project_shard { - self.ensure_code_graph_shard_attached(&bound_shard).await; + { + let attached: Pin + Send + '_>> = + Box::pin(self.ensure_code_graph_shard_attached(&bound_shard)); + attached.await; + } } let authority_lease: Arc = authority; let mut storage = project_database @@ -2835,7 +2867,11 @@ impl DaemonSessionRuntimeRegistryV1 { // That exact failure surfaced on every retention tick as // `graph_replay_release_failed`, which blocked the code-generation // pass and let superseded sealed generations accumulate without bound. - self.ensure_code_graph_shard_attached(&project_shard).await; + { + let attached: Pin + Send + '_>> = + Box::pin(self.ensure_code_graph_shard_attached(&project_shard)); + attached.await; + } let authority = self .registry .retain_graph_store(StoreRuntimeKey::new( @@ -2855,7 +2891,11 @@ impl DaemonSessionRuntimeRegistryV1 { bound = ?bound_shard, "graph replay reconcile lease binding names a different shard than the reconstructed project shard" ); - self.ensure_code_graph_shard_attached(&bound_shard).await; + { + let attached: Pin + Send + '_>> = + Box::pin(self.ensure_code_graph_shard_attached(&bound_shard)); + attached.await; + } } let authority_lease: Arc = authority; let mut storage = project_database diff --git a/crates/tracedecay-store-runtime/src/session_registry/code_graph/seals.rs b/crates/tracedecay-store-runtime/src/session_registry/code_graph/seals.rs index ccb943f0a3..eb517fb5e6 100644 --- a/crates/tracedecay-store-runtime/src/session_registry/code_graph/seals.rs +++ b/crates/tracedecay-store-runtime/src/session_registry/code_graph/seals.rs @@ -82,13 +82,13 @@ fn staged_identity_matches( let handle_metadata = file .metadata() .map_err(|error| GraphDbError::unavailable(error.to_string()))?; - if staged_fingerprint(&path_metadata)? != *expected - || staged_fingerprint(&handle_metadata)? != *expected - { - return Ok(false); - } + let fingerprint_matches = staged_fingerprint(&path_metadata)? == *expected + && staged_fingerprint(&handle_metadata)? == *expected; #[cfg(windows)] { + if !fingerprint_matches { + return Ok(false); + } let path_file = File::open(path).map_err(|error| GraphDbError::unavailable(error.to_string()))?; let path_identity = tracedecay_private_fs::windows_file::information(&path_file) @@ -102,7 +102,7 @@ fn staged_identity_matches( } #[cfg(unix)] { - Ok(true) + Ok(fingerprint_matches) } #[cfg(not(any(unix, windows)))] { diff --git a/crates/tracedecay-store-runtime/src/session_registry/maintenance.rs b/crates/tracedecay-store-runtime/src/session_registry/maintenance.rs index 4e6410ec38..934bb7a7c1 100644 --- a/crates/tracedecay-store-runtime/src/session_registry/maintenance.rs +++ b/crates/tracedecay-store-runtime/src/session_registry/maintenance.rs @@ -672,23 +672,37 @@ impl DaemonSessionRuntimeRegistryV1 { runtime: StoreRuntimeClientLease, ) -> Pin> + Send + '_>> { Box::pin(async move { - let database = - Database::publish_runtime(runtime, DatabaseAccessMode::ReadWrite).await?; + let published: Pin< + Box< + dyn Future> + + Send, + >, + > = Box::pin(Database::publish_runtime( + runtime, + DatabaseAccessMode::ReadWrite, + )); + let database = published.await?; // Only long-lived daemons defer schema convergence to resumable // maintenance. A store refused for reset is never converged: its // reset deletes it. let long_lived = self.long_lived_session_maintenance; let (database, convergence) = if long_lived { - RegisteredGlobalDbOwnerV1::admit_and_attach_for_daemon( - database, - self.registry.open_cancellation(), - ) - .await? + let admitted = tracedecay_runtime_core::erase_send!( + RegisteredGlobalDbOwnerV1::admit_and_attach_for_daemon( + database, + self.registry.open_cancellation(), + ), + Result<( + RegisteredGlobalDbOwnerV1, + Option, + )> + ); + admitted.await? } else { - ( - RegisteredGlobalDbOwnerV1::admit_and_attach(database).await?, - None, - ) + let admitted: Pin< + Box> + Send>, + > = Box::pin(RegisteredGlobalDbOwnerV1::admit_and_attach(database)); + (admitted.await?, None) }; if convergence.is_some() { let lease = database.issue_lease().map_err(|error| { diff --git a/crates/tracedecay-store-runtime/src/session_registry/mounts.rs b/crates/tracedecay-store-runtime/src/session_registry/mounts.rs index 6f2056e5fe..0a2d3b3140 100644 --- a/crates/tracedecay-store-runtime/src/session_registry/mounts.rs +++ b/crates/tracedecay-store-runtime/src/session_registry/mounts.rs @@ -1,5 +1,7 @@ use std::collections::BTreeMap; +use std::future::Future; use std::path::{Path, PathBuf}; +use std::pin::Pin; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -146,20 +148,6 @@ impl DaemonSessionRuntimeRegistryV1 { identity: LocalProfileIdentityAuthorityV1, long_lived_session_maintenance: bool, ) -> Result { - let operation_runtime = - tracedecay_runtime_core::native_runtime::handle().ok_or_else(|| { - TraceDecayError::Config { - message: "session runtime registry requires a native execution runtime" - .to_owned(), - } - })?; - let operation_task_owner = Arc::new( - RuntimeOperationTaskOwnerV1::new(operation_runtime.clone()).map_err(|error| { - TraceDecayError::Config { - message: error.to_string(), - } - })?, - ); let remote_credential_authority = Arc::new(crate::DaemonRemoteCredentialAuthorityV1::new( identity.brain_id().clone(), identity.profile_id().clone(), @@ -184,15 +172,33 @@ impl DaemonSessionRuntimeRegistryV1 { identity.profile_root().to_path_buf(), ), )); - let registry_resolver: Arc = resolver.clone(); - let registry = StoreRuntimeRegistry::new( - operation_runtime, - registry_resolver, - Arc::new(LifecycleShardRuntimePublisher), - ) - .map_err(|error| { - session_registry_error("create store runtime registry", format!("{error:?}")) - })?; + let (operation_task_owner, registry) = + { + // The handle's Send proof is the asupersync self-cycle. Consume it + // before the first await so the constructor future does not capture it. + let operation_runtime = tracedecay_runtime_core::native_runtime::handle() + .ok_or_else(|| TraceDecayError::Config { + message: "session runtime registry requires a native execution runtime" + .to_owned(), + })?; + let operation_task_owner = Arc::new( + RuntimeOperationTaskOwnerV1::new(operation_runtime.clone()).map_err( + |error| TraceDecayError::Config { + message: error.to_string(), + }, + )?, + ); + let registry_resolver: Arc = resolver.clone(); + let registry = StoreRuntimeRegistry::new( + operation_runtime, + registry_resolver, + Arc::new(LifecycleShardRuntimePublisher), + ) + .map_err(|error| { + session_registry_error("create store runtime registry", format!("{error:?}")) + })?; + (operation_task_owner, registry) + }; let graph_manifest_provider = Arc::new(super::code_graph_manifest::DaemonCodeGraphManifestProviderV1::default()); let graph_registry = GraphDbRegistry::new_with_manifest_provider( @@ -208,8 +214,8 @@ impl DaemonSessionRuntimeRegistryV1 { })?; let profile_shard = StoreShardIdV1::profile(identity.brain_id().clone(), identity.profile_id().clone()); - let profile_runtime = tracing::Instrument::instrument( - open_runtime( + let opened: Pin> + Send + '_>> = + Box::pin(open_runtime( ®istry, resolver.as_ref(), StoreRuntimeOpenSpec::new( @@ -220,7 +226,9 @@ impl DaemonSessionRuntimeRegistryV1 { true, "mount profile authority store", ), - ), + )); + let profile_runtime = tracing::Instrument::instrument( + opened, tracing::trace_span!("daemon.store.profile_authority.bootstrap_open"), ) .await?; @@ -267,9 +275,17 @@ impl DaemonSessionRuntimeRegistryV1 { #[cfg(any(test, feature = "test-helpers"))] session_graph_publication_gate: std::sync::Mutex::new(None), }; - registry.mount_registered_remote_nodes().await?; + let mounted_nodes: Pin> + Send + '_>> = + Box::pin(registry.mount_registered_remote_nodes()); + tracing::Instrument::instrument( + mounted_nodes, + tracing::trace_span!("daemon.session_registry.mount_remote_nodes"), + ) + .await?; if registry.long_lived_session_maintenance { - registry.inspect_project_session_stores().await; + let inspected: Pin + Send + '_>> = + Box::pin(registry.inspect_project_session_stores()); + inspected.await; } Ok(registry) } @@ -317,9 +333,9 @@ impl DaemonSessionRuntimeRegistryV1 { manifests.sort(); let digest = registered_schema_admission_digest(); for manifest in manifests { - if let Err(error) = - Box::pin(self.inspect_project_session_store(&manifest, &digest)).await - { + let inspected: Pin> + Send + '_>> = + Box::pin(self.inspect_project_session_store(&manifest, &digest)); + if let Err(error) = inspected.await { tracing::warn!( event = "project_session_store_inspection_failed", path = %manifest.display(), @@ -404,11 +420,6 @@ impl DaemonSessionRuntimeRegistryV1 { } } - #[tracing::instrument( - name = "daemon.session_registry.mount_remote_nodes", - level = "trace", - skip_all - )] async fn mount_registered_remote_nodes(&self) -> Result<()> { let nodes_root = self.identity.profile_root().join("remote").join("nodes"); if !nodes_root.exists() { @@ -443,8 +454,9 @@ impl DaemonSessionRuntimeRegistryV1 { error.to_string(), ) })?; - self.remote_node_storage(node_id, Arc::clone(&keyring)) - .await?; + let mounted: Pin> + Send + '_>> = + Box::pin(self.remote_node_storage(node_id, Arc::clone(&keyring))); + mounted.await?; } Ok(()) } @@ -626,7 +638,7 @@ impl DaemonSessionRuntimeRegistryV1 { ); // Boxed store-open composition: keeps this mount machine (and the // measured wrapper embedding it by value) pointer-sized per await. - let runtime = tracing::Instrument::instrument( + let opened: Pin> + Send + '_>> = Box::pin(open_runtime( &self.registry, self.resolver.as_ref(), @@ -638,12 +650,15 @@ impl DaemonSessionRuntimeRegistryV1 { true, "mount profile authority store", ), - )), + )); + let runtime = tracing::Instrument::instrument( + opened, tracing::trace_span!("daemon.store.profile_authority.mount_open"), ) .await?; - let database = - Box::pin(self.attach_registered(runtime, "attach profile authority store")).await?; + let attached: Pin> + Send + '_>> = + Box::pin(self.attach_registered(runtime, "attach profile authority store")); + let database = attached.await?; let lease = database.issue_lease().map_err(|error| { session_registry_error( "issue profile authority database client", @@ -660,7 +675,9 @@ impl DaemonSessionRuntimeRegistryV1 { /// The profile session store. A store held in its typed reset-required /// state answers that refusal instead of a client. pub async fn profile_sessions(&self) -> Result { - refuse_reset_required(Box::pin(self.mount_profile_session_store()).await?) + let mounted: Pin> + Send + '_>> = + Box::pin(self.mount_profile_session_store()); + refuse_reset_required(mounted.await?) } async fn mount_profile_session_store(&self) -> Result { @@ -700,8 +717,10 @@ impl DaemonSessionRuntimeRegistryV1 { self.identity.brain_id().clone(), self.identity.profile_id().clone(), ); - let pin = Box::pin(self.profile_authority_pin("mount profile session store")).await?; - let runtime = tracing::Instrument::instrument( + let pin: Pin> + Send + '_>> = + Box::pin(self.profile_authority_pin("mount profile session store")); + let pin = pin.await?; + let opened: Pin> + Send + '_>> = Box::pin(open_runtime( &self.registry, self.resolver.as_ref(), @@ -713,12 +732,15 @@ impl DaemonSessionRuntimeRegistryV1 { true, "mount profile session store", ), - )), + )); + let runtime = tracing::Instrument::instrument( + opened, tracing::trace_span!("daemon.store.profile_sessions.open"), ) .await?; - let database = - Box::pin(self.attach_registered(runtime, "mount profile session store")).await?; + let attached: Pin> + Send + '_>> = + Box::pin(self.attach_registered(runtime, "mount profile session store")); + let database = attached.await?; let (database, lease) = self.publish_session_owner( database, shard_id, @@ -896,8 +918,8 @@ impl DaemonSessionRuntimeRegistryV1 { let pin = self .profile_authority_pin("mount profile memory store") .await?; - let runtime = tracing::Instrument::instrument( - open_runtime( + let opened: Pin> + Send + '_>> = + Box::pin(open_runtime( &self.registry, self.resolver.as_ref(), StoreRuntimeOpenSpec::new( @@ -908,11 +930,17 @@ impl DaemonSessionRuntimeRegistryV1 { true, "mount profile memory store", ), - ), + )); + let runtime = tracing::Instrument::instrument( + opened, tracing::trace_span!("daemon.store.profile_memory.open"), ) .await?; - let (owner, database) = self.publish_memory_owner(shard_id, runtime).await?; + let published = tracedecay_runtime_core::erase_send!( + self.publish_memory_owner(shard_id, runtime), + Result<(MemoryStoreOwnerV1, Arc)> + ); + let (owner, database) = published.await?; *self .profile_memory .lock() @@ -1032,10 +1060,11 @@ impl DaemonSessionRuntimeRegistryV1 { self.identity.profile_id().clone(), node_id.clone(), ); - let pin = self - .profile_authority_pin("mount Remote Brain node store") - .await?; - let (runtime, existed) = tracing::Instrument::instrument( + let pin: Pin< + Box> + Send + '_>, + > = Box::pin(self.profile_authority_pin("mount Remote Brain node store")); + let pin = pin.await?; + let opened = tracedecay_runtime_core::erase_send!( open_runtime_with_presence( &self.registry, self.resolver.as_ref(), @@ -1046,12 +1075,21 @@ impl DaemonSessionRuntimeRegistryV1 { true, "mount Remote Brain node store", ), + Result<(StoreRuntimeClientLease, bool)> + ); + let (runtime, existed) = tracing::Instrument::instrument( + opened, tracing::trace_span!("daemon.store.remote_node.open"), ) .await?; + let published: Pin< + Box> + Send + '_>, + > = Box::pin(Database::publish_runtime( + runtime, + DatabaseAccessMode::ReadWrite, + )); let owner = RemoteNodeStoreOwnerV1 { - database: Database::publish_runtime(runtime, DatabaseAccessMode::ReadWrite) - .await?, + database: published.await?, }; let database = owner.database.issue_lease().map_err(|error| { session_registry_error( diff --git a/crates/tracedecay-store/src/memory/project_memory/automatic_facts.rs b/crates/tracedecay-store/src/memory/project_memory/automatic_facts.rs index 2c112d6772..e5f67d1bc5 100644 --- a/crates/tracedecay-store/src/memory/project_memory/automatic_facts.rs +++ b/crates/tracedecay-store/src/memory/project_memory/automatic_facts.rs @@ -346,8 +346,9 @@ impl ProjectMemoryAutomaticFactApplyResultV1 { let (target_owner, target_fact_id) = receipt .effect() .applied_target() - .map(|target| (Some(target.owner()), Some(target.fact_id()))) - .unwrap_or((None, None)); + .map_or((None, None), |target| { + (Some(target.owner()), Some(target.fact_id())) + }); canonical_sha256(&AutomaticFactDigestProjection { domain: "tracedecay.project-memory.automatic-fact-apply-result.v1", apply_id: receipt.apply_id(), diff --git a/crates/tracedecay-temporal-query/src/resolution/resolver.rs b/crates/tracedecay-temporal-query/src/resolution/resolver.rs index 3e6b6a4df9..a59ca02310 100644 --- a/crates/tracedecay-temporal-query/src/resolution/resolver.rs +++ b/crates/tracedecay-temporal-query/src/resolution/resolver.rs @@ -39,8 +39,7 @@ fn evidence_strength( authority_rank(occurrence.evidence.authority), support .get(&occurrence.anchor_id) - .map(BTreeSet::len) - .unwrap_or_default(), + .map_or_default(BTreeSet::len), ) } @@ -138,15 +137,12 @@ fn cycle_members_among( hook: &mut dyn FnMut(ResolutionCheckpoint) -> Result<(), TemporalPortError>, ) -> Result, TemporalPortError> { let children_in_subgraph = |node: &RetrievalAnchorId| -> Vec { - descendants - .get(node) - .map(|set| { - set.iter() - .filter(|child| nodes.contains(*child)) - .cloned() - .collect() - }) - .unwrap_or_default() + descendants.get(node).map_or_default(|set| { + set.iter() + .filter(|child| nodes.contains(*child)) + .cloned() + .collect() + }) }; let mut index_of = BTreeMap::::new(); diff --git a/crates/tracedecay/src/doctor.rs b/crates/tracedecay/src/doctor.rs index 994ef05bf1..5699a3f59f 100644 --- a/crates/tracedecay/src/doctor.rs +++ b/crates/tracedecay/src/doctor.rs @@ -1069,7 +1069,7 @@ fn check_watcher(dc: &mut DoctorCounters, profile: &tracedecay_runtime_core::con degraded += 1; dc.warn(&format!( "{project}: degraded (bounded scheduler-reconciliation fallback){}", - ev.detail.map(|d| format!(", {d}")).unwrap_or_default() + ev.detail.map_or_default(|d| format!(", {d}")) )); } "git_watch_restart" => { @@ -1405,9 +1405,7 @@ fn warn_detected_unintegrated_host( dc.warn(&format!( "{} detected{} but tracedecay is not integrated, run `tracedecay install --agent {}`", agent.name(), - surface - .map(|surface| format!(" ({})", surface.display())) - .unwrap_or_default(), + surface.map_or_default(|surface| format!(" ({})", surface.display())), agent.id() )); } diff --git a/crates/tracedecay/src/mcp/server.rs b/crates/tracedecay/src/mcp/server.rs index 6b236169e4..1e23040b45 100644 --- a/crates/tracedecay/src/mcp/server.rs +++ b/crates/tracedecay/src/mcp/server.rs @@ -1362,8 +1362,7 @@ impl McpServer { let file_token_entries = self .file_token_map .lock() - .map(|tokens| tokens.len()) - .unwrap_or_default(); + .map_or_default(|tokens| tokens.len()); let ratio = |n: u64| { if total_requests == 0 { 0.0 diff --git a/crates/tracedecay/src/mcp/server/lcm_claude_recall_tests.rs b/crates/tracedecay/src/mcp/server/lcm_claude_recall_tests.rs index 9c245dce2a..f998ebff17 100644 --- a/crates/tracedecay/src/mcp/server/lcm_claude_recall_tests.rs +++ b/crates/tracedecay/src/mcp/server/lcm_claude_recall_tests.rs @@ -208,13 +208,12 @@ async fn ingested_server() -> (Arc, TempDir, TempDir, TempDir) { fn omission_reasons(evidence: &Value) -> Vec { evidence["omissions"] .as_array() - .map(|omissions| { + .map_or_default(|omissions| { omissions .iter() .map(|omission| omission["reason"].as_str().unwrap_or("").to_string()) .collect() }) - .unwrap_or_default() } #[tokio::test] diff --git a/crates/tracedecay/tests/claude_observation_benchmark/runner.rs b/crates/tracedecay/tests/claude_observation_benchmark/runner.rs index 9300e1458d..152de49186 100644 --- a/crates/tracedecay/tests/claude_observation_benchmark/runner.rs +++ b/crates/tracedecay/tests/claude_observation_benchmark/runner.rs @@ -64,16 +64,18 @@ fn benchmark_tempdir(prefix: &str) -> TempDir { // Bazel's executable tree is read-only; TEST_TMPDIR is its writable, // per-test scratch authority. Native benchmark runs retain target locality. let root = std::env::var_os("TEST_TMPDIR") - .map(PathBuf::from) - .unwrap_or_else(|| { - let executable = - fs::canonicalize(std::env::current_exe().expect("resolve test executable")) - .expect("canonicalize test executable"); - executable - .parent() - .expect("test executable parent") - .to_path_buf() - }) + .map_or_else( + || { + let executable = + fs::canonicalize(std::env::current_exe().expect("resolve test executable")) + .expect("canonicalize test executable"); + executable + .parent() + .expect("test executable parent") + .to_path_buf() + }, + PathBuf::from, + ) .join("tracedecay-benchmark-data"); fs::create_dir_all(&root).expect("create benchmark scratch root"); tempfile::Builder::new() diff --git a/crates/tracedecay/tests/common/mod.rs b/crates/tracedecay/tests/common/mod.rs index ec3875a77d..5b7a4d53c8 100644 --- a/crates/tracedecay/tests/common/mod.rs +++ b/crates/tracedecay/tests/common/mod.rs @@ -835,9 +835,8 @@ pub fn search_eval_bin(name: &str) -> PathBuf { "tracedecay-search-eval-direct" => "TRACEDECAY_SEARCH_EVAL_DIRECT_TEST_BIN", _ => "TRACEDECAY_SEARCH_EVAL_TEST_BIN", }; - let binary = std::env::var_os(override_env) - .map(PathBuf::from) - .unwrap_or_else(|| { + let binary = std::env::var_os(override_env).map_or_else( + || { let test_executable = std::env::current_exe().expect("test executable path should resolve"); let profile_dir = test_executable @@ -845,7 +844,9 @@ pub fn search_eval_bin(name: &str) -> PathBuf { .and_then(Path::parent) .expect("integration test should run from a Cargo profile directory"); profile_dir.join(format!("{name}{}", std::env::consts::EXE_SUFFIX)) - }); + }, + PathBuf::from, + ); assert!( binary.is_file(), "search-eval binary `{name}` is missing at {}; build it with `cargo build --workspace --bins --locked --features tracedecay/test-helpers` or set {override_env}. A `-p tracedecay-search-eval` build resolves `tracedecay-code-index` without its default language tiers and yields an evaluator that cannot parse Rust, so the package selection must match the test lane's.", diff --git a/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey/daemon_fixture.rs b/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey/daemon_fixture.rs index 069897ebb7..fb4a51fa12 100644 --- a/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey/daemon_fixture.rs +++ b/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey/daemon_fixture.rs @@ -85,9 +85,10 @@ pub(super) fn wait_for_workflow_mount(client: &Client, run_id: &RunId) { pub(super) fn spawn_project_daemon(home: &Path, project: &Path) -> common::DaemonProcess { let profile = home.join(".tracedecay"); PrivateStoreIo::create_dir_all(&profile).expect("advanced workflow daemon profile"); - let log_path = std::env::var_os("TRACEDECAY_TEST_DAEMON_LOG") - .map(PathBuf::from) - .unwrap_or_else(|| profile.join("advanced-workflow-daemon.log")); + let log_path = std::env::var_os("TRACEDECAY_TEST_DAEMON_LOG").map_or_else( + || profile.join("advanced-workflow-daemon.log"), + PathBuf::from, + ); let stderr = std::fs::OpenOptions::new() .create(true) .append(true) diff --git a/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey/task_session.rs b/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey/task_session.rs index 1c689a80e2..82a7311829 100644 --- a/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey/task_session.rs +++ b/crates/tracedecay/tests/daemon_suite/advanced_workflow_journey/task_session.rs @@ -100,8 +100,7 @@ impl DashboardProcess { self.base_url, self.diagnostics .lock() - .map(|captured| captured.clone()) - .unwrap_or_default() + .map_or_default(|captured| captured.clone()) ); std::thread::sleep(Duration::from_millis(200)); } diff --git a/crates/tracedecay/tests/dashboard_api_test/api.rs b/crates/tracedecay/tests/dashboard_api_test/api.rs index 22e0cbaf87..10b6a43276 100644 --- a/crates/tracedecay/tests/dashboard_api_test/api.rs +++ b/crates/tracedecay/tests/dashboard_api_test/api.rs @@ -975,7 +975,7 @@ fn lcm_large_session_pages_preserve_continuation_and_timeline_counts() { let cursor = first["payload"]["next_cursor"] .as_str() .expect("bounded page continuation"); - let second_url = url::Url::parse_with_params(&base, &[("cursor", cursor)]).unwrap(); + let second_url = url::Url::parse_with_params(&base, [("cursor", cursor)]).unwrap(); let (status, second) = get_json(&agent, second_url.as_str()); assert_eq!(status, 200); let second_messages = second["payload"]["messages"] diff --git a/crates/tracedecay/tests/hooks_lsp_suite/hook_spool_drain_test.rs b/crates/tracedecay/tests/hooks_lsp_suite/hook_spool_drain_test.rs index ea7bd878ad..497b6fd5fe 100644 --- a/crates/tracedecay/tests/hooks_lsp_suite/hook_spool_drain_test.rs +++ b/crates/tracedecay/tests/hooks_lsp_suite/hook_spool_drain_test.rs @@ -125,18 +125,16 @@ fn capture_stop(home: &Path, project: &Path, generation: &str) { } fn published_receipts(receipts: &Path) -> Vec { - let mut published = std::fs::read_dir(receipts) - .map(|entries| { - entries - .map(|entry| entry.unwrap().path()) - .filter(|path| { - path.file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.ends_with(".delivery.v1.json")) - }) - .collect::>() - }) - .unwrap_or_default(); + let mut published = std::fs::read_dir(receipts).map_or_default(|entries| { + entries + .map(|entry| entry.unwrap().path()) + .filter(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.ends_with(".delivery.v1.json")) + }) + .collect::>() + }); published.sort(); published } diff --git a/crates/tracedecay/tests/memory_suite/memory_eval_test.rs b/crates/tracedecay/tests/memory_suite/memory_eval_test.rs index e8aaf814f4..a94d7cf031 100644 --- a/crates/tracedecay/tests/memory_suite/memory_eval_test.rs +++ b/crates/tracedecay/tests/memory_suite/memory_eval_test.rs @@ -157,9 +157,9 @@ fn run_with_timeout(mut command: Command, timeout: Duration) -> Output { .unwrap_or_else(|error| panic!("failed to poll tracedecay: {error}")) { let join = |handle: Option>>| { - handle - .map(|handle| handle.join().expect("child pipe drain thread panicked")) - .unwrap_or_default() + handle.map_or_default(|handle| { + handle.join().expect("child pipe drain thread panicked") + }) }; return Output { status, diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs index 5e2f53c60f..0062c9c373 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs @@ -3664,8 +3664,7 @@ fn wall_clock_micros() -> UtcMicros { i64::try_from( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_micros()) - .unwrap_or(0), + .map_or(0, |duration| duration.as_micros()), ) .unwrap_or(i64::MAX), ) diff --git a/crates/tracedecay/tests/session_suite/fact_anchor_authority.rs b/crates/tracedecay/tests/session_suite/fact_anchor_authority.rs index 77534881aa..6f6d65139a 100644 --- a/crates/tracedecay/tests/session_suite/fact_anchor_authority.rs +++ b/crates/tracedecay/tests/session_suite/fact_anchor_authority.rs @@ -407,9 +407,7 @@ async fn missing_daemon_authority_fails_closed_without_a_fallback_store() { const ROOT_ENV: &str = "TRACEDECAY_MISSING_DAEMON_FIXTURE"; if !crate::common::in_child_test() { let scratch = TempDir::new_in( - std::env::var_os("TEST_TMPDIR") - .map(PathBuf::from) - .unwrap_or_else(std::env::temp_dir), + std::env::var_os("TEST_TMPDIR").map_or_else(std::env::temp_dir, PathBuf::from), ) .unwrap(); let temporary = scratch.path().join("temporary"); diff --git a/crates/tracedecay/tests/session_suite/lcm_compression/frontier.rs b/crates/tracedecay/tests/session_suite/lcm_compression/frontier.rs index f14db862d8..690aa6eafe 100644 --- a/crates/tracedecay/tests/session_suite/lcm_compression/frontier.rs +++ b/crates/tracedecay/tests/session_suite/lcm_compression/frontier.rs @@ -218,9 +218,7 @@ async fn late_summary_projection_failure_rolls_back_payload_files_and_canonical_ 0 ); let payload_dir = tmp.path().join(".tracedecay").join("lcm-payloads"); - let payload_count = std::fs::read_dir(payload_dir) - .map(|entries| entries.count()) - .unwrap_or_default(); + let payload_count = std::fs::read_dir(payload_dir).map_or_default(|entries| entries.count()); assert_eq!( payload_count, 0, "payload rollback must remove created files" diff --git a/crates/tracedecay/tests/session_suite/message_search_eval_test.rs b/crates/tracedecay/tests/session_suite/message_search_eval_test.rs index ef4bf10d13..7a50dcc1d4 100644 --- a/crates/tracedecay/tests/session_suite/message_search_eval_test.rs +++ b/crates/tracedecay/tests/session_suite/message_search_eval_test.rs @@ -116,13 +116,12 @@ fn str_vec(value: &Value, key: &str) -> Vec { value .get(key) .and_then(Value::as_array) - .map(|arr| { + .map_or_default(|arr| { arr.iter() .filter_map(Value::as_str) .map(str::to_string) .collect() }) - .unwrap_or_default() } fn index_of(ranked: &[String], id: &str) -> Option { diff --git a/crates/tracedecay/tests/storage_suite/project_identity_collapse_test.rs b/crates/tracedecay/tests/storage_suite/project_identity_collapse_test.rs index bf28fc5aea..8648eadd6b 100644 --- a/crates/tracedecay/tests/storage_suite/project_identity_collapse_test.rs +++ b/crates/tracedecay/tests/storage_suite/project_identity_collapse_test.rs @@ -147,9 +147,7 @@ async fn ephemeral_project_root_cannot_enter_a_durable_registry() { const PROFILE_ENV: &str = "TRACEDECAY_DURABLE_REGISTRY_FIXTURE"; if !crate::common::in_child_test() { let scratch = TempDir::new_in( - std::env::var_os("TEST_TMPDIR") - .map(PathBuf::from) - .unwrap_or_else(std::env::temp_dir), + std::env::var_os("TEST_TMPDIR").map_or_else(std::env::temp_dir, PathBuf::from), ) .unwrap(); let temporary = scratch.path().join("temporary"); diff --git a/crates/tracedecay/tests/transcript_ingest_suite/restart_atomicity.rs b/crates/tracedecay/tests/transcript_ingest_suite/restart_atomicity.rs index 1e6495be73..7ea651a92f 100644 --- a/crates/tracedecay/tests/transcript_ingest_suite/restart_atomicity.rs +++ b/crates/tracedecay/tests/transcript_ingest_suite/restart_atomicity.rs @@ -490,10 +490,9 @@ fn assert_no_transcript_adjacent_fallback_writer( assert!( !transcript .parent() - .map(|parent| parent.join("sessions.db").exists() + .is_some_and(|parent| parent.join("sessions.db").exists() || parent.join("tracedecay.db").exists() - || parent.join(".tracedecay").exists()) - .unwrap_or(false), + || parent.join(".tracedecay").exists()), "provider ingest must not create a transcript-adjacent fallback writer" ); assert_ne!( diff --git a/rust-toolchain.toml b/rust-toolchain.toml index deeb6d8aa8..34cffb79d5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,6 @@ [toolchain] -channel = "1.99.0" +# Dated nightly: asupersync's nightly-outcome-try default needs try_trait_v2. +# 2026-09-27 includes rust-lang/rust#162275; 2026-08-25 does not. +channel = "nightly-2026-09-27" # LSP acceptance negotiates capabilities with the pinned upstream analyzer. components = ["clippy", "rustfmt", "rust-analyzer", "rust-src"] diff --git a/scripts/bazel/rust_test_environment_probe.rs b/scripts/bazel/rust_test_environment_probe.rs index e749c8e21b..cbf100e0b5 100644 --- a/scripts/bazel/rust_test_environment_probe.rs +++ b/scripts/bazel/rust_test_environment_probe.rs @@ -1,3 +1,38 @@ +fn compiler_version( + program: impl AsRef, + home: &std::path::Path, + rustup_home: Option<&std::path::Path>, +) -> String { + let mut command = std::process::Command::new(program.as_ref()); + command.arg("--version").current_dir(home); + if let Some(rustup_home) = rustup_home { + command.env("RUSTUP_HOME", rustup_home); + } + let output = command + .output() + .expect("run fixture compiler outside the workspace"); + assert!( + output.status.success(), + "{:?}: {output:?}", + program.as_ref() + ); + String::from_utf8(output.stdout).expect("compiler version is UTF-8") +} + +fn assert_pinned_compiler(channel: &str, version: &str, label: &str) { + if channel.starts_with("nightly-") { + assert!( + version.contains("nightly"), + "{label} {version} is not the pinned nightly {channel}" + ); + } else { + assert!( + version.contains(channel), + "{label} {version} does not contain {channel}" + ); + } +} + fn main() { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("rules_rust must provide CARGO_MANIFEST_DIR"); @@ -34,36 +69,28 @@ fn main() { std::path::Path::new(&test_tmpdir).join("cargo-home"), ); let toolchain = std::env::var("RUSTUP_TOOLCHAIN").expect("pinned test toolchain"); + // A dated nightly's `rustc --version` is `1.x.0-nightly (hash date)`. + // The channel date is not in that string, so PATH identity is the + // declared binary's version text. + let channel = toolchain.replace('/', "-"); + let mut declared = std::collections::BTreeMap::new(); for variable in ["CARGO", "RUSTC", "RUSTDOC"] { let tool = std::path::PathBuf::from(std::env::var_os(variable).expect("fixture compiler")); assert!(tool.is_absolute(), "{variable}={}", tool.display()); - let output = std::process::Command::new(&tool) - .arg("--version") - .current_dir(&home) - .output() - .expect("run fixture compiler outside the workspace"); - assert!(output.status.success(), "{variable}: {:?}", output); - assert!( - String::from_utf8(output.stdout) - .unwrap() - .contains(&toolchain) - ); + let version = compiler_version(&tool, &home, None); + assert_pinned_compiler(&channel, &version, variable); + declared.insert(variable, version); } // Exercise the production runner's literal executable lookup under a // fresh home: no rustup proxy may download a second toolchain here. for tool in ["cargo", "rustc", "rustdoc"] { - let output = std::process::Command::new(tool) - .arg("--version") - .env("RUSTUP_HOME", home.join("empty-rustup")) - .current_dir(&home) - .output() - .expect("run declared toolchain through PATH"); - assert!(output.status.success(), "{tool}: {output:?}"); - assert!( - String::from_utf8(output.stdout) - .unwrap() - .contains(&toolchain) + let version = compiler_version(tool, &home, Some(home.join("empty-rustup").as_path())); + let variable = tool.to_ascii_uppercase(); + assert_eq!( + version, + declared[&variable.as_str()], + "PATH {tool} is not the declared toolchain" ); } let fixture = home.join("fixture"); diff --git a/scripts/test-check-sdk-publish-workflow.py b/scripts/test-check-sdk-publish-workflow.py index 52e2c7adf7..3f4c8f1809 100644 --- a/scripts/test-check-sdk-publish-workflow.py +++ b/scripts/test-check-sdk-publish-workflow.py @@ -162,14 +162,14 @@ def test_rejects_mutable_action_reference(self) -> None: old = self.workflow_fragment( r" - uses: dtolnay/rust-toolchain@[0-9a-f]{40} # stable\n" r" with:\n" - r" toolchain: stable", + r" toolchain: \$\{\{ steps\.rust-toolchain\.outputs\.toolchain \}\}", missing="build-typescript rust-toolchain pin missing from release.yml", ) self.assert_rejected_after( old, " - uses: dtolnay/rust-toolchain@stable\n" " with:\n" - " toolchain: stable", + " toolchain: ${{ steps.rust-toolchain.outputs.toolchain }}", "'build-typescript' uses unpinned action 'dtolnay/rust-toolchain@stable'", ) @@ -216,9 +216,9 @@ def test_rejects_local_helper_from_mutable_build_checkout(self) -> None: self.assert_rejected_after( " ref: ${{ needs.validate-release.outputs.source_sha }}\n" " persist-credentials: false\n\n" - " - uses: dtolnay/rust-toolchain@", + " # rust-toolchain.toml is the only compiler.", " persist-credentials: false\n\n" - " - uses: dtolnay/rust-toolchain@", + " # rust-toolchain.toml is the only compiler.", "'build-typescript' must first and only check out the validated source " "SHA without persisted credentials", )