diff --git a/.cargo/config.toml b/.cargo/config.toml
index c12efa73a9..28ad4ef952 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -31,7 +31,7 @@ CXXFLAGS_aarch64_pc_windows_msvc = { value = "-DNDEBUG /utf-8", force = false }
# connections. Nothing reads those statistics or sets a heap limit.
LIBSQLITE3_FLAGS = { value = "-DSQLITE_MAX_WORKER_THREADS=15 -DSQLITE_DEFAULT_MEMSTATUS=0", force = true }
-# Global accounting (savings ledger, worldwide-counter flushes) is ON by
+# Global accounting (the savings ledger) is ON by
# default for installed binaries, but `cargo test` / `cargo run` would then
# write temp-project rows into the developer's real ~/.tracedecay/global.db.
# Keep cargo-launched processes hermetic; tests that exercise the ledger
diff --git a/README.md b/README.md
index fb0965ed87..e4417998e7 100644
--- a/README.md
+++ b/README.md
@@ -195,8 +195,6 @@ authorized, sanitized, classified records under their remote policy.
Network-capable effects are separate from core retrieval:
-- Worldwide counter: when opted in, sends the pending aggregate token-savings
- amount, and `tracedecay doctor` reads the public total to check reachability.
- GitHub release checks and updates: contact GitHub release endpoints. An
explicitly configured private GitHub review source can use a read-only
credential from the operating-system keyring; missing, ambiguous,
@@ -212,19 +210,13 @@ Network-capable effects are separate from core retrieval:
path fails closed with typed `unavailable`, `denied`, or `stale-peer` state.
These services receive normal transport metadata, including the connection's
-source address, in addition to any request payload. The counter request is an
-aggregate amount, not repository content, but it is not an anonymity guarantee.
-Use a network policy or firewall if a command must make no outbound connection.
-An opt-out, denial, timeout, or unavailable remote service must remain visible
-as that state; it is not evidence of a zero counter, an up-to-date release, or
-current pricing. See [Security](SECURITY.md#network-access) for the complete
-outbound-access and credential boundary.
-
-Disable the worldwide counter with:
-
-```bash
-tracedecay disable-upload-counter
-```
+source address, in addition to any request payload. Use a network policy or
+firewall if a command must make no outbound connection. A denial, timeout, or
+unavailable remote service must remain visible as that state; it is not
+evidence of an up-to-date release or current pricing. TraceDecay no longer has
+a worldwide token counter: token savings and costs stay local. See
+[Security](SECURITY.md#network-access) for the complete outbound-access and
+credential boundary.
## Troubleshooting
diff --git a/SECURITY.md b/SECURITY.md
index 2d0d2dce1d..80992d8d4b 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -67,7 +67,6 @@ Outbound capabilities include:
| Configured remote sources and authorities | Policy-authorized retrieval and remote operations | Configured peer authentication and disclosure policy | Admission or authorization failures are reported; review each peer before enabling |
| `github.com` | Download binary during `tracedecay upgrade` | None (public releases) | Error shown to user |
| `huggingface.co` and Hugging Face artifact hosts | Download missing, revision-pinned semantic-model artifacts when semantic auto-download is enabled | None | Semantic retrieval reports model acquisition state or failure; exact, lexical, and graph retrieval remain available |
-| `tracedecay-counter.enzinol.workers.dev` | Aggregate token-savings counter | None | Silently ignored |
Provider usage and pricing do not add an outbound connection. `tracedecay cost`
reads immutable provider-native usage observations and the deterministic bundled
@@ -169,7 +168,6 @@ an old count of unsafe sites is not a security guarantee.
The following are **not** security issues:
-- The aggregate token counter sending a count to the public Cloudflare Worker endpoint (this is documented behavior and contains no identifying information beyond an approximate country derived from IP by Cloudflare)
- The database containing symbol names or file paths from your project (this is core functionality)
- The MCP edit tools modifying files (this is opt-in functionality your AI agent invokes through its own tool-approval flow)
- `tracedecay_run_affected_tests` compiling and running your project's own test suite (this is the tool's documented purpose)
diff --git a/crates/tracedecay-api/src/configuration.rs b/crates/tracedecay-api/src/configuration.rs
index 4817efb7a1..dc04cf01dc 100644
--- a/crates/tracedecay-api/src/configuration.rs
+++ b/crates/tracedecay-api/src/configuration.rs
@@ -72,8 +72,6 @@ pub struct UserSettingsPatch {
pub expected_revision_id: String,
pub idempotency_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
- pub upload_enabled: Option,
- #[serde(default, skip_serializing_if = "Option::is_none")]
pub watcher_debounce: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extraction_timeout_secs: Option,
@@ -121,7 +119,6 @@ pub fn parse_code_index_worker_settings_patch(
/// write that unsigned value and do not parse the wire string again.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdmittedUserSettingsPatch {
- pub upload_enabled: Option,
pub watcher_debounce_ms: Option,
pub extraction_timeout_secs: Option,
}
@@ -154,7 +151,6 @@ pub fn admit_user_settings_patch(
}
if errors.is_empty() {
Ok(AdmittedUserSettingsPatch {
- upload_enabled: patch.upload_enabled,
watcher_debounce_ms,
extraction_timeout_secs: patch.extraction_timeout_secs,
})
@@ -332,7 +328,7 @@ mod tests {
let result = parse_user_settings_patch(json!({
"expected_revision_id": "configuration.revision.project",
"idempotency_key": "configuration.idempotency.fixture",
- "upload_enabled": true,
+ "watcher_debounce": "2s",
"code_index_workers": { "mode": "automatic" }
}));
@@ -340,4 +336,17 @@ mod tests {
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["validation_errors"][0]["field"], "code_index_workers");
}
+
+ #[test]
+ fn user_patch_refuses_the_retired_worldwide_counter_upload_field() {
+ let result = parse_user_settings_patch(json!({
+ "expected_revision_id": "configuration.revision.profile",
+ "idempotency_key": "configuration.idempotency.fixture",
+ "upload_enabled": true
+ }));
+
+ let (status, Json(body)) = result.expect_err("a retired field must be refused");
+ assert_eq!(status, StatusCode::BAD_REQUEST);
+ assert_eq!(body["validation_errors"][0]["field"], "upload_enabled");
+ }
}
diff --git a/crates/tracedecay-cli/src/cli.rs b/crates/tracedecay-cli/src/cli.rs
index 7eb5537359..eaa11bd830 100644
--- a/crates/tracedecay-cli/src/cli.rs
+++ b/crates/tracedecay-cli/src/cli.rs
@@ -578,20 +578,6 @@ pub enum Commands {
#[arg(short, long)]
path: Option,
},
- /// Disable uploading token counts to the worldwide counter
- #[command(
- name = "disable-upload-counter",
- long_about = DISABLE_UPLOAD_COUNTER_LONG_ABOUT,
- after_help = DISABLE_UPLOAD_COUNTER_AFTER_HELP
- )]
- DisableUploadCounter,
- /// Enable uploading token counts to the worldwide counter
- #[command(
- name = "enable-upload-counter",
- long_about = ENABLE_UPLOAD_COUNTER_LONG_ABOUT,
- after_help = ENABLE_UPLOAD_COUNTER_AFTER_HELP
- )]
- EnableUploadCounter,
/// Check tracedecay installation, configuration, and agent integration
#[command(long_about = DOCTOR_LONG_ABOUT, after_help = DOCTOR_AFTER_HELP)]
Doctor {
diff --git a/crates/tracedecay-cli/src/cli/help.rs b/crates/tracedecay-cli/src/cli/help.rs
index 77bc4f4833..a0d8e2c722 100644
--- a/crates/tracedecay-cli/src/cli/help.rs
+++ b/crates/tracedecay-cli/src/cli/help.rs
@@ -446,29 +446,6 @@ Examples:
Related: tracedecay current-counter, tracedecay gain.";
-pub(crate) const DISABLE_UPLOAD_COUNTER_LONG_ABOUT: &str = "\
-Opts this machine out of contributing anonymous token-savings counts to the \
-public worldwide counter. Only aggregate numbers are ever uploaded, never \
-code, paths, or queries, but uploading is entirely optional.";
-
-pub(crate) const DISABLE_UPLOAD_COUNTER_AFTER_HELP: &str = "\
-Examples:
- tracedecay disable-upload-counter Stop contributing counts
-
-Related: tracedecay enable-upload-counter, tracedecay gain (local ledger
-is unaffected).";
-
-pub(crate) const ENABLE_UPLOAD_COUNTER_LONG_ABOUT: &str = "\
-Re-enables contributing anonymous token-savings counts to the public \
-worldwide counter after a previous `disable-upload-counter`. Only aggregate \
-numbers are uploaded, never code, paths, or queries.";
-
-pub(crate) const ENABLE_UPLOAD_COUNTER_AFTER_HELP: &str = "\
-Examples:
- tracedecay enable-upload-counter Resume contributing counts
-
-Related: tracedecay disable-upload-counter, tracedecay gain.";
-
pub(crate) const DOCTOR_LONG_ABOUT: &str = "\
Checks the binary, PATH, daemon service, project index, and every agent \
integration, printing actionable fixes for anything broken. The running \
diff --git a/crates/tracedecay-cli/src/cloud.rs b/crates/tracedecay-cli/src/cloud.rs
index ced9744617..27618d8b48 100644
--- a/crates/tracedecay-cli/src/cloud.rs
+++ b/crates/tracedecay-cli/src/cloud.rs
@@ -1,7 +1,5 @@
-//! HTTP client for the worldwide counter Cloudflare Worker and GitHub release
-//! version checking.
+//! HTTP client for GitHub release version checking.
//!
-//! Counter operations are best-effort: failures surface as `None` / empty.
//! Release lookups classify every failure as a [`ReleaseLookupError`]. All
//! are synchronous `ureq` calls that block the calling thread for up to their
//! own timeout, which an enclosing Tokio deadline cannot cut short. A caller
@@ -14,20 +12,14 @@ use serde::de::DeserializeOwned;
use tracedecay_application::http_agent::http_agent;
use tracedecay_dashboard_api::cloud::ReleaseLookupError;
-/// The Cloudflare Worker endpoint URL.
-const WORKER_URL: &str = "https://tracedecay-counter.enzinol.workers.dev";
-
/// Base URL of the GitHub REST API release lookups are issued against.
pub(crate) const GITHUB_API_URL: &str = "https://api.github.com";
const GITHUB_OWNER: &str = "ScriptedAlchemy";
const GITHUB_REPOSITORY: &str = "tracedecay";
-/// Timeout for flush (upload) requests.
-const FLUSH_TIMEOUT: Duration = Duration::from_secs(2);
-
-/// Timeout for opportunistic advisory reads (worldwide counter and the
-/// background / `doctor` version probe). Keep this short so a stalled
-/// GitHub or worker never blocks those paths.
+/// Timeout for opportunistic advisory reads (the background / `doctor`
+/// version probe). Keep this short so a stalled GitHub never blocks those
+/// paths.
const FETCH_TIMEOUT: Duration = Duration::from_secs(1);
/// Bound for each GitHub release-metadata read of an explicit `tracedecay
@@ -38,53 +30,6 @@ const FETCH_TIMEOUT: Duration = Duration::from_secs(1);
/// keep [`FETCH_TIMEOUT`].
pub(crate) const UPGRADE_RELEASE_LOOKUP_TIMEOUT: Duration = Duration::from_secs(30);
-/// Response from the worker's POST /increment and GET /total endpoints.
-#[derive(serde::Deserialize)]
-struct WorkerResponse {
- total: u64,
-}
-
-/// Creates a ureq agent with the given timeout.
-pub fn agent_with_timeout(timeout: Duration) -> ureq::Agent {
- http_agent(
- ureq::Agent::config_builder()
- .timeout_global(Some(timeout))
- .build(),
- )
-}
-
-/// Uploads pending tokens to the worldwide counter.
-/// Returns the new worldwide total on success, or None on any failure.
-#[tracing::instrument(name = "cloud.flush_pending", level = "trace", skip_all)]
-pub fn flush_pending(amount: u64) -> Option {
- if amount == 0 {
- return None;
- }
- let body = serde_json::json!({ "amount": amount });
- let agent = agent_with_timeout(FLUSH_TIMEOUT);
- let parsed: WorkerResponse = agent
- .post(&format!("{WORKER_URL}/increment"))
- .send_json(&body)
- .ok()?
- .body_mut()
- .read_json()
- .ok()?;
- Some(parsed.total)
-}
-
-/// Fetches the current worldwide total from the worker.
-/// Returns None on timeout, network error, or parse failure.
-#[tracing::instrument(name = "cloud.fetch_worldwide_total", level = "trace", skip_all)]
-pub fn fetch_worldwide_total() -> Option {
- fetch_worldwide_total_from(&format!("{WORKER_URL}/total"))
-}
-
-fn fetch_worldwide_total_from(url: &str) -> Option {
- let agent = agent_with_timeout(FETCH_TIMEOUT);
- let parsed: WorkerResponse = agent.get(url).call().ok()?.body_mut().read_json().ok()?;
- Some(parsed.total)
-}
-
/// Response from GitHub releases API (only the fields we need).
#[derive(serde::Deserialize)]
struct GitHubRelease {
@@ -356,16 +301,15 @@ pub fn is_beta() -> bool {
tracedecay_dashboard_api::cloud::is_beta(env!("CARGO_PKG_VERSION"))
}
-/// Admits these ureq implementations into the composition library so MCP
-/// flush/version checks can run after the binary starts. Unregistered
-/// lookups stay `None` (already best-effort).
+/// Admits the ureq release lookup into the composition library so MCP version
+/// checks can run after the binary starts. An unregistered lookup stays
+/// `None` (already best-effort).
pub fn admit_sync_probes() {
- tracedecay_dashboard_api::cloud::admit_sync_cloud_probes(flush_pending, fetch_latest_version);
+ tracedecay_dashboard_api::cloud::admit_sync_cloud_probes(fetch_latest_version);
}
pub fn doctor_network_probes() -> tracedecay::doctor::AdmittedDoctorNetworkProbes {
tracedecay::doctor::AdmittedDoctorNetworkProbes {
- fetch_worldwide_total,
fetch_latest_version,
}
}
@@ -570,33 +514,6 @@ mod tests {
);
}
- #[test]
- fn advisory_worldwide_total_still_times_out_at_the_one_second_budget() {
- let (base, _heads) = stub_after(
- Duration::from_millis(1500),
- Some(respond(
- "200 OK",
- "Content-Type: application/json\r\n",
- r#"{"total":42}"#,
- )),
- );
- let started = Instant::now();
-
- assert!(
- fetch_worldwide_total_from(&format!("{base}/total")).is_none(),
- "advisory worldwide-total must keep the one-second budget"
- );
- let elapsed = started.elapsed();
- assert!(
- elapsed >= FETCH_TIMEOUT,
- "advisory worldwide-total returned before its one-second budget: {elapsed:?}"
- );
- assert!(
- elapsed < Duration::from_millis(1500),
- "advisory worldwide-total waited for the delayed answer: {elapsed:?}"
- );
- }
-
fn respond(status: &str, headers: &str, body: &str) -> &'static str {
format!(
"HTTP/1.1 {status}\r\n{headers}Content-Length: {}\r\nConnection: close\r\n\r\n{body}",
diff --git a/crates/tracedecay-cli/src/commands.rs b/crates/tracedecay-cli/src/commands.rs
index d86586b06b..be8a552d0c 100644
--- a/crates/tracedecay-cli/src/commands.rs
+++ b/crates/tracedecay-cli/src/commands.rs
@@ -25,12 +25,11 @@ pub(crate) use index::{handle_init, handle_no_command, handle_sync};
pub(crate) use profile_storage::handle_profile_storage_action;
pub(crate) use scope::resolve_project_scope;
pub(crate) use settings::{
- canonical_upload_enabled, current_configuration_revision, current_project_setting,
- handle_upload_counter, mutate_project_configuration, project_configuration_set,
- repeated_query_hints_enabled, report_configuration_receipt,
+ current_configuration_revision, current_project_setting, mutate_project_configuration,
+ project_configuration_set, repeated_query_hints_enabled, report_configuration_receipt,
};
pub(crate) use stale_store_reset::handle_wipe_stale;
pub(crate) use storage::{
- ProfileOfflineAuthority, RestoreWording, annotate_reset_required, handle_list, handle_wipe,
- join_outcome_and_restore, process_error_text, take_profile_offline, try_admit_profile_registry,
+ ProfileOfflineAuthority, RestoreWording, handle_list, handle_wipe, join_outcome_and_restore,
+ process_error_text, take_profile_offline, try_admit_profile_registry,
};
diff --git a/crates/tracedecay-cli/src/commands/settings.rs b/crates/tracedecay-cli/src/commands/settings.rs
index 435110761c..e203031ac6 100644
--- a/crates/tracedecay-cli/src/commands/settings.rs
+++ b/crates/tracedecay-cli/src/commands/settings.rs
@@ -17,7 +17,6 @@ use tracedecay_daemon_protocol::RequestedOutputFormat;
use tracedecay_domain::configuration::{
ConfigurationIdempotencyKey, ConfigurationLayerIdV1, ConfigurationRevisionId,
ConfigurationValueV1, SettingKey, USER_REPEATED_QUERY_HINTS_SETTING_KEY,
- USER_UPLOAD_ENABLED_SETTING_KEY, UserProfileId,
};
use tracedecay_domain::{ProjectId, UtcMicros, canonical_sha256};
use tracedecay_tool_catalog::ApplicationSurfaceOperation;
@@ -43,25 +42,6 @@ fn cli_configuration_idempotency_key(
.map_err(|error| config_error(format!("invalid configuration request key: {error}")))
}
-fn cli_user_configuration_idempotency_key(
- profile_id: &UserProfileId,
- expected_revision: &ConfigurationRevisionId,
- mutations: &[ConfigurationDirectMutationRequestV1],
-) -> tracedecay_domain::errors::Result {
- let digest = canonical_sha256(&(
- "tracedecay.cli.user-configuration-mutation.v1",
- profile_id,
- expected_revision,
- mutations,
- ))
- .map_err(|error| config_error(format!("invalid user configuration mutation: {error}")))?;
- let suffix = digest
- .hex_suffix()
- .ok_or_else(|| config_error("user configuration mutation digest is malformed"))?;
- ConfigurationIdempotencyKey::new(format!("configuration.idempotency.cli.user.{suffix}"))
- .map_err(|error| config_error(format!("invalid user configuration request key: {error}")))
-}
-
fn configuration_deadline(
operation: ApplicationSurfaceOperation,
observed_at: UtcMicros,
@@ -210,13 +190,6 @@ async fn resolved_setting(
.map_err(|error| config_error(format!("invalid configuration setting: {error}")))
}
-/// The profile's worldwide-counter upload setting, wherever the command runs.
-pub(crate) async fn canonical_upload_enabled(
- profile: &ProfileRoot,
-) -> tracedecay_domain::errors::Result {
- boolean_setting(&resolved_setting(profile, None, USER_UPLOAD_ENABLED_SETTING_KEY).await?)
-}
-
/// The profile's experimental repeated-query hint setting for `serve`.
pub(crate) async fn repeated_query_hints_enabled(
profile: &ProfileRoot,
@@ -280,31 +253,6 @@ pub(crate) async fn mutate_project_configuration(
configuration_effect_receipt(envelope, &idempotency_key).map(Some)
}
-async fn mutate_user_configuration(
- profile: &ProfileRoot,
- profile_id: &UserProfileId,
- expected_revision: ConfigurationRevisionId,
- mutations: Vec,
-) -> tracedecay_domain::errors::Result> {
- if mutations.is_empty() {
- return Ok(None);
- }
- let idempotency_key =
- cli_user_configuration_idempotency_key(profile_id, &expected_revision, &mutations)?;
- let envelope = invoke_configuration_surface(
- profile,
- None,
- ApplicationSurfaceOperation::ConfigurationBatch,
- ConfigurationWireRequestV1::Batch(ConfigurationBatchRequestV1 {
- mutations,
- expected_revision,
- idempotency_key: idempotency_key.clone(),
- }),
- )
- .await?;
- configuration_effect_receipt(envelope, &idempotency_key).map(Some)
-}
-
fn configuration_effect_receipt(
envelope: ApplicationEnvelope,
idempotency_key: &ConfigurationIdempotencyKey,
@@ -347,37 +295,3 @@ pub(crate) fn report_configuration_receipt(receipt: Option<&EffectReceipt>) {
eprintln!("Receipt: {}", receipt.request_id.as_str());
}
}
-
-#[tracing::instrument(name = "cli.settings.upload_counter", level = "trace", skip_all)]
-pub(crate) async fn handle_upload_counter(
- profile: &ProfileRoot,
- enable: bool,
-) -> tracedecay_domain::errors::Result<()> {
- let current = resolved_setting(profile, None, USER_UPLOAD_ENABLED_SETTING_KEY).await?;
- let profile_id =
- tracedecay_daemon_identity::profile_identity::load_existing(profile.data_dir())?
- .profile_id()
- .clone();
- let mutations = if boolean_setting(¤t)? != enable {
- vec![ConfigurationDirectMutationRequestV1::Set {
- layer: ConfigurationLayerIdV1::UserProfile {
- profile_id: profile_id.clone(),
- },
- key: current.key,
- value: Box::new(ConfigurationValueV1::Boolean(enable)),
- }]
- } else {
- Vec::new()
- };
- let receipt =
- mutate_user_configuration(profile, &profile_id, current.revision_id, mutations).await?;
- if enable {
- eprintln!("Worldwide counter upload enabled.");
- } else {
- eprintln!(
- "Worldwide counter upload disabled. You can re-enable with `tracedecay enable-upload-counter`."
- );
- }
- report_configuration_receipt(receipt.as_ref());
- Ok(())
-}
diff --git a/crates/tracedecay-cli/src/global.rs b/crates/tracedecay-cli/src/global.rs
index c96a487b65..5ea8e37a9c 100644
--- a/crates/tracedecay-cli/src/global.rs
+++ b/crates/tracedecay-cli/src/global.rs
@@ -32,55 +32,6 @@ pub(crate) async fn classify_project_storage_with_registry(
.await
}
-/// Returns how many seconds have elapsed since a persisted timestamp.
-///
-/// User config timestamps can land in the future because of clock skew,
-/// manual edits, or state copied across machines. Clamp those cases to zero so
-/// cooldown/version-cache logic degrades gracefully instead of getting stuck on
-/// a negative delta.
-fn elapsed_since(now: i64, recorded_at: i64) -> i64 {
- if recorded_at >= now {
- 0
- } else {
- now - recorded_at
- }
-}
-
-/// Best-effort: try to flush pending tokens to the worldwide counter.
-///
-/// `upload_enabled` is the already-resolved canonical user-profile setting;
-/// `config` only carries the pending counter and cooldown timestamps.
-/// `force` = true on status/sync commands (always attempt), false on others
-/// (only flush if stale > 30s).
-pub(crate) fn try_flush(
- config: &mut tracedecay_session_memory::user_config::UserConfig,
- force: bool,
- upload_enabled: bool,
-) {
- if config.pending_upload == 0 || !upload_enabled {
- return;
- }
- let now = tracedecay_runtime_core::tracedecay::current_timestamp();
-
- // Cooldown: skip if last flush attempt failed less than 60s ago
- if config.last_flush_attempt_at > config.last_upload_at
- && elapsed_since(now, config.last_flush_attempt_at) < 60
- {
- return;
- }
-
- // Staleness check for non-force commands
- if !force && elapsed_since(now, config.last_upload_at) < 30 {
- return;
- }
-
- config.last_flush_attempt_at = now;
- if crate::cloud::flush_pending(config.pending_upload).is_some() {
- config.pending_upload = 0;
- config.last_upload_at = now;
- }
-}
-
/// Returns the project paths the `wipe` command should act on.
///
/// `--all` returns every path tracked in the global DB (including stale rows).
@@ -702,24 +653,4 @@ mod gather_tests {
vec![std::path::PathBuf::from("/repos/a")]
);
}
-
- #[test]
- fn elapsed_since_clamps_future_timestamps() {
- assert_eq!(elapsed_since(100, 40), 60);
- assert_eq!(elapsed_since(100, 100), 0);
- assert_eq!(elapsed_since(100, 140), 0);
- }
-
- #[test]
- fn canonical_upload_denial_leaves_pending_tokens_unflushed() {
- let mut config = tracedecay_session_memory::user_config::UserConfig {
- pending_upload: 42,
- ..tracedecay_session_memory::user_config::UserConfig::default()
- };
-
- try_flush(&mut config, true, false);
-
- assert_eq!(config.pending_upload, 42);
- assert_eq!(config.last_flush_attempt_at, 0);
- }
}
diff --git a/crates/tracedecay-cli/src/main.rs b/crates/tracedecay-cli/src/main.rs
index f31f3bf161..3e1280fb3e 100644
--- a/crates/tracedecay-cli/src/main.rs
+++ b/crates/tracedecay-cli/src/main.rs
@@ -733,7 +733,7 @@ async fn run(cli: Cli) -> tracedecay_domain::errors::Result {
}
};
- run_startup_preamble(&profile, &command).await;
+ run_startup_preamble(&profile, &command);
dispatch_command(&profile, command, host_bundle).await
}
@@ -754,28 +754,9 @@ fn command_profile(command: Option<&Commands>) -> tracedecay_domain::errors::Res
}
#[tracing::instrument(name = "cli.startup.preamble", level = "trace", skip_all)]
-async fn run_startup_preamble(profile: &ProfileRoot, command: &Commands) {
+fn run_startup_preamble(profile: &ProfileRoot, command: &Commands) {
let startup_policy = CommandStartupPolicy::for_command(command);
- // Check first-run before any config save creates the file.
- let profile_root = profile.data_dir();
- let is_first_run = !tracedecay_session_memory::user_config::UserConfig::exists(profile_root);
-
- let is_force_flush = matches!(command, Commands::Sync { .. } | Commands::Status { .. });
- match tracedecay_session_memory::user_config::UserConfig::load(profile_root) {
- Ok(user_config) => {
- flush_worldwide_counter(profile, command, is_force_flush, user_config).await;
- }
- Err(err) => eprintln!("warning: {err}"),
- }
-
- if is_first_run && startup_policy.runs_startup_maintenance() {
- eprintln!(
- "note: tracedecay can optionally upload anonymous token savings counts to a worldwide counter.\n\
- \x20 Run `tracedecay enable-upload-counter` to opt in."
- );
- }
-
if startup_policy.runs_agent_install_check()
&& let Some(home) = profile.home()
{
@@ -783,52 +764,6 @@ async fn run_startup_preamble(profile: &ProfileRoot, command: &Commands) {
}
}
-async fn flush_worldwide_counter(
- profile: &ProfileRoot,
- command: &Commands,
- is_force_flush: bool,
- mut user_config: tracedecay_session_memory::user_config::UserConfig,
-) {
- let profile_root = profile.data_dir();
- // Skip the worldwide-counter flush on hot startup paths. `try_flush`
- // makes a synchronous HTTP call which can add seconds to
- // `tracedecay serve` startup on slow networks, long enough to blow the
- // MCP client's 30 s `initialize` timeout. The canonical setting lookup is
- // only consulted when there are pending tokens to flush: with nothing
- // pending the setting cannot change behavior, and probing it on every
- // command turned the daemon's transient "runtime still mounting" state
- // into per-command stderr noise. A failed lookup on an ordinary command
- // is deferred (the next command retries); the flush-bearing commands
- // (`sync`, `status`) still surface it, so a persistent failure
- // stays visible exactly where the flush is expected to happen.
- // The setting belongs to the profile, so the flush does not depend on
- // the current directory.
- if runs_worldwide_counter_flush(command) && user_config.pending_upload > 0 {
- match commands::canonical_upload_enabled(profile).await {
- Ok(upload_enabled) => {
- global::try_flush(&mut user_config, is_force_flush, upload_enabled);
- }
- Err(error) if is_force_flush => {
- eprintln!(
- "warning: canonical worldwide-counter upload setting is unavailable: {}",
- commands::annotate_reset_required(error, None)
- );
- }
- Err(error) => {
- tracing::debug!(
- "worldwide-counter flush deferred: canonical upload setting unavailable: \
- {error}"
- );
- }
- }
- }
- if !is_local_install_command(command)
- && let Err(err) = user_config.save_if_exists(profile_root)
- {
- eprintln!("warning: could not save tracedecay config: {err}");
- }
-}
-
async fn resolve_registered_project_root(
profile: &ProfileRoot,
project_id: Option,
@@ -975,10 +910,7 @@ impl CommandFamily {
| Commands::PostUpdate { .. }
| Commands::PackageHook { .. }
| Commands::Channel { .. } => Self::Update,
- Commands::CurrentCounter { .. }
- | Commands::ResetCounter { .. }
- | Commands::DisableUploadCounter
- | Commands::EnableUploadCounter => Self::Configuration,
+ Commands::CurrentCounter { .. } | Commands::ResetCounter { .. } => Self::Configuration,
Commands::Doctor { .. }
| Commands::Cost { .. }
| Commands::Gain { .. }
@@ -1822,12 +1754,6 @@ async fn dispatch_configuration_command(
}
eprintln!("Local counter reset (was {prev})");
}
- Commands::DisableUploadCounter => {
- commands::handle_upload_counter(profile, false).await?;
- }
- Commands::EnableUploadCounter => {
- commands::handle_upload_counter(profile, true).await?;
- }
_ => unreachable!("non-configuration command passed to configuration dispatcher"),
}
Ok(())
@@ -2023,6 +1949,7 @@ impl CommandStartupPolicy {
}
}
+ #[cfg(test)]
fn runs_startup_maintenance(self) -> bool {
!matches!(self, Self::SkipAll)
}
@@ -2032,11 +1959,6 @@ impl CommandStartupPolicy {
}
}
-fn runs_worldwide_counter_flush(command: &Commands) -> bool {
- !matches!(command, Commands::Init { .. })
- && CommandStartupPolicy::for_command(command).runs_startup_maintenance()
-}
-
#[cfg(test)]
fn should_skip_startup_maintenance(command: &Commands) -> bool {
!CommandStartupPolicy::for_command(command).runs_startup_maintenance()
@@ -2047,10 +1969,6 @@ fn should_skip_agent_install_check(command: &Commands) -> bool {
!CommandStartupPolicy::for_command(command).runs_agent_install_check()
}
-fn is_local_install_command(command: &Commands) -> bool {
- matches!(command, Commands::Install { local: true, .. })
-}
-
fn normalize_tool_reserved_global_flags(cli: &mut Cli) {
if !cli.dry_run {
return;
diff --git a/crates/tracedecay-cli/src/native_root_tests.rs b/crates/tracedecay-cli/src/native_root_tests.rs
index 88cfe50643..6f0681009d 100644
--- a/crates/tracedecay-cli/src/native_root_tests.rs
+++ b/crates/tracedecay-cli/src/native_root_tests.rs
@@ -31,7 +31,7 @@ fn run_on_native_root(profile: &ProfileRoot, args: &[&str]) -> Result (Option, V
(output.status.code(), body)
}
-fn run_upload_counter_command(home: &Path, cwd: &Path, command: &str) {
- let output = tracedecay_command_with_home(home)
- .current_dir(cwd)
- .arg(command)
- .stdin(Stdio::null())
- .output()
- .expect("upload counter command should run");
- assert!(output.status.success(), "{command} failed: {output:?}");
-}
-
/// A user-scoped setting belongs to the profile, so it resolves wherever the
-/// command runs: a value written inside a project is the one doctor and
-/// `configuration_get` report outside any project, and a write made outside a
-/// project is the one the project then reads. A project-scoped key still needs
-/// a project.
+/// command runs: the same value is reported inside and outside any project. A
+/// project-scoped key still needs a project, and the retired worldwide-counter
+/// upload setting is no longer a registered key.
#[test]
fn user_settings_resolve_from_the_profile_outside_any_project() {
let home = TempDir::new().unwrap();
@@ -4060,40 +4049,22 @@ fn user_settings_resolve_from_the_profile_outside_any_project() {
init_committed_git_project_with_cli(&home, &project);
let _daemon = spawn_tracedecay_daemon(&home);
- run_upload_counter_command(&home, &project, "enable-upload-counter");
-
- let doctor = tracedecay_command_with_home(&home)
- .current_dir(&outside)
- .arg("doctor")
- .stdin(Stdio::null())
- .output()
- .expect("doctor should run");
- let doctor_stderr = String::from_utf8_lossy(&doctor.stderr);
- assert!(
- doctor_stderr.contains("Worldwide counter upload enabled"),
- "doctor outside a project must report the profile's upload setting:\n{doctor_stderr}"
- );
- assert!(
- !doctor_stderr.contains("upload setting unavailable")
- && !doctor_stderr.contains("canonical configuration is unavailable"),
- "doctor outside a project reported the user setting unavailable:\n{doctor_stderr}"
- );
-
- let (code, enabled) = configuration_get_from(&home, &outside, "user.upload_enabled.v1");
- assert_eq!(code, Some(0), "{enabled}");
- assert_eq!(
- enabled["structuredContent"]["outcome"]["value"]["payload"]["effective_value"],
- json!({ "kind": "boolean", "value": true }),
- "{enabled}"
- );
+ for cwd in [&outside, &project] {
+ let (code, hints) = configuration_get_from(&home, cwd, "user.repeated_query_hints.v1");
+ assert_eq!(code, Some(0), "{hints}");
+ assert_eq!(
+ hints["structuredContent"]["outcome"]["value"]["payload"]["effective_value"],
+ json!({ "kind": "boolean", "value": false }),
+ "the profile setting must resolve from {}: {hints}",
+ cwd.display()
+ );
+ }
- run_upload_counter_command(&home, &outside, "disable-upload-counter");
- let (code, disabled) = configuration_get_from(&home, &project, "user.upload_enabled.v1");
- assert_eq!(code, Some(0), "{disabled}");
- assert_eq!(
- disabled["structuredContent"]["outcome"]["value"]["payload"]["effective_value"],
- json!({ "kind": "boolean", "value": false }),
- "a write made outside a project must be the value the project reads: {disabled}"
+ let (code, retired) = configuration_get_from(&home, &outside, "user.upload_enabled.v1");
+ assert_ne!(
+ code,
+ Some(0),
+ "the retired upload setting must be refused, not read: {retired}"
);
let (code, refused) = configuration_get_from(&home, &outside, "index.max_file_size.v1");
diff --git a/crates/tracedecay-cli/tests/core_cli_suite/user_config_test.rs b/crates/tracedecay-cli/tests/core_cli_suite/user_config_test.rs
index 381ba9dd64..ce0365ebdd 100644
--- a/crates/tracedecay-cli/tests/core_cli_suite/user_config_test.rs
+++ b/crates/tracedecay-cli/tests/core_cli_suite/user_config_test.rs
@@ -2,9 +2,9 @@ use tracedecay_session_memory::user_config::UserConfig;
#[test]
fn missing_fields_use_defaults() {
- let toml_str = "pending_upload = 5\n";
+ let toml_str = "last_version_check_at = 5\n";
let parsed: UserConfig = toml::from_str(toml_str).unwrap();
- assert_eq!(parsed.pending_upload, 5);
- assert_eq!(parsed.last_upload_at, 0);
+ assert_eq!(parsed.last_version_check_at, 5);
+ assert_eq!(parsed.last_version_warning_at, 0);
assert!(parsed.installed_agents.is_empty());
}
diff --git a/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs b/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs
index f61c947ef8..e447a37183 100644
--- a/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs
+++ b/crates/tracedecay-cli/tests/host_journeys_suite/host_lifecycle_cli_acceptance/sweep_outcomes.rs
@@ -518,7 +518,7 @@ fn doctor_fails_a_corrupt_profile_config_with_its_repair() {
let _daemon = ProfileDaemon::start(&cli);
let config = cli.profile.join("config.toml");
let installed = fs::read_to_string(&config).unwrap();
- fs::write(&config, "pending_upload = 0\n true").unwrap();
+ fs::write(&config, "last_version_check_at = 0\n true").unwrap();
let doctor = cli.run(&["doctor"]);
let doctor_stderr = stderr(&doctor);
diff --git a/crates/tracedecay-configuration/src/configuration/user_settings.rs b/crates/tracedecay-configuration/src/configuration/user_settings.rs
index eb490cd0e2..5b3a8c6c10 100644
--- a/crates/tracedecay-configuration/src/configuration/user_settings.rs
+++ b/crates/tracedecay-configuration/src/configuration/user_settings.rs
@@ -12,8 +12,7 @@ use std::pin::Pin;
use tracedecay_automation::config::AutomationConfig;
use tracedecay_domain::configuration::{
ConfigurationLayerIdV1, ConfigurationValueV1, SettingKey,
- USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY, USER_UPLOAD_ENABLED_SETTING_KEY,
- USER_WATCHER_DEBOUNCE_MS_SETTING_KEY, UserProfileId,
+ USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY, USER_WATCHER_DEBOUNCE_MS_SETTING_KEY, UserProfileId,
};
use tracedecay_contracts::now_micros;
@@ -29,7 +28,6 @@ pub type UserSettingsFuture<'a, T> =
pub struct UserSettingsSnapshotV1 {
pub configuration_snapshot_id: String,
pub configuration_revision_id: String,
- pub upload_enabled: bool,
pub watcher_debounce: String,
pub watcher_debounce_ms: u64,
pub extraction_timeout_secs: u64,
@@ -40,7 +38,6 @@ pub struct UserSettingsSnapshotV1 {
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct UserSettingsMutationV1 {
- pub upload_enabled: Option,
/// Positive milliseconds already parsed at the settings boundary.
pub watcher_debounce_ms: Option,
pub extraction_timeout_secs: Option,
@@ -142,14 +139,12 @@ fn user_settings_snapshot(
current: &ProfileConfigurationV1,
metadata: UserMetadata,
) -> Result {
- let upload_enabled = required_bool(current, USER_UPLOAD_ENABLED_SETTING_KEY)?;
let watcher_debounce_ms = required_unsigned(current, USER_WATCHER_DEBOUNCE_MS_SETTING_KEY)?;
let extraction_timeout_secs =
required_unsigned(current, USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY)?;
Ok(UserSettingsSnapshotV1 {
configuration_snapshot_id: current.snapshot.snapshot_id.as_str().to_owned(),
configuration_revision_id: current.revision_id.as_str().to_owned(),
- upload_enabled,
watcher_debounce: format_duration_millis(watcher_debounce_ms),
watcher_debounce_ms,
extraction_timeout_secs,
@@ -173,13 +168,6 @@ pub fn plan_user_settings_mutation(
let mut mutations = Vec::new();
let mut restart_recommended = false;
- if let Some(upload_enabled) = mutation.upload_enabled {
- mutations.push(set(
- layer.clone(),
- USER_UPLOAD_ENABLED_SETTING_KEY,
- ConfigurationValueV1::Boolean(upload_enabled),
- )?);
- }
if let Some(watcher_debounce_ms) = mutation.watcher_debounce_ms {
mutations.push(set(
layer.clone(),
@@ -235,16 +223,6 @@ fn setting_key(raw_key: &str) -> Result
.map_err(|error| unavailable(format!("registered user setting key: {error}")))
}
-fn required_bool(
- current: &ProfileConfigurationV1,
- raw_key: &str,
-) -> Result {
- match current.value(raw_key) {
- Ok(ConfigurationValueV1::Boolean(value)) => Ok(*value),
- _ => Err(unavailable(format!("boolean user setting {raw_key}"))),
- }
-}
-
fn required_unsigned(
current: &ProfileConfigurationV1,
raw_key: &str,
@@ -271,7 +249,6 @@ mod tests {
UserSettingsSnapshotV1 {
configuration_snapshot_id: "configuration.snapshot.fixture".to_owned(),
configuration_revision_id: "configuration.revision.fixture".to_owned(),
- upload_enabled: false,
watcher_debounce: "2s".to_owned(),
watcher_debounce_ms: 2_000,
extraction_timeout_secs: 60,
@@ -288,14 +265,13 @@ mod tests {
&snapshot(),
profile_id.clone(),
UserSettingsMutationV1 {
- upload_enabled: Some(true),
watcher_debounce_ms: Some(15_000),
extraction_timeout_secs: Some(60),
},
)
.unwrap();
- assert_eq!(plan.mutations.len(), 3);
+ assert_eq!(plan.mutations.len(), 2);
assert!(plan.restart_recommended);
assert!(plan.mutations.iter().any(|mutation| matches!(
mutation,
diff --git a/crates/tracedecay-dashboard-api/src/cloud.rs b/crates/tracedecay-dashboard-api/src/cloud.rs
index 598e6e4ab3..8f436a98e1 100644
--- a/crates/tracedecay-dashboard-api/src/cloud.rs
+++ b/crates/tracedecay-dashboard-api/src/cloud.rs
@@ -164,27 +164,17 @@ fn http_date(unix_seconds: u64) -> String {
httpdate::fmt_http_date(UNIX_EPOCH + Duration::from_secs(unix_seconds))
}
-type FlushPending = fn(u64) -> Option;
type FetchLatestVersion = fn() -> Result;
-static FLUSH_PENDING: OnceLock = OnceLock::new();
static FETCH_LATEST_VERSION: OnceLock = OnceLock::new();
-/// Admits the CLI binary's sync ureq implementations. A second admission is
-/// ignored so tests that re-enter process start stay idempotent. Unregistered
-/// lookups return `None`.
-pub fn admit_sync_cloud_probes(
- flush_pending: FlushPending,
- fetch_latest_version: FetchLatestVersion,
-) {
- let _ = FLUSH_PENDING.set(flush_pending);
+/// Admits the CLI binary's sync ureq release lookup. A second admission is
+/// ignored so tests that re-enter process start stay idempotent. An
+/// unregistered lookup returns `None`.
+pub fn admit_sync_cloud_probes(fetch_latest_version: FetchLatestVersion) {
let _ = FETCH_LATEST_VERSION.set(fetch_latest_version);
}
-pub fn flush_pending(amount: u64) -> Option {
- FLUSH_PENDING.get().and_then(|flush| flush(amount))
-}
-
/// Set by test harnesses so no spawned CLI or daemon asks GitHub for the
/// latest release; the advisory version check then has no answer.
pub const NO_RELEASE_CHECK_ENV: &str = "TRACEDECAY_NO_RELEASE_CHECK";
diff --git a/crates/tracedecay-dashboard-api/src/settings_api.rs b/crates/tracedecay-dashboard-api/src/settings_api.rs
index 5d69f5d0b1..4056a5a43d 100644
--- a/crates/tracedecay-dashboard-api/src/settings_api.rs
+++ b/crates/tracedecay-dashboard-api/src/settings_api.rs
@@ -212,7 +212,6 @@ struct UserSettingsPayloadV1 {
/// user settings revision above.
code_index_worker_configuration_snapshot_id: String,
code_index_worker_configuration_revision_id: String,
- upload_enabled: bool,
code_index_workers: CodeIndexWorkerSelectionV1,
/// The admitted process-wide plan. A persisted selection may differ until
/// the daemon restarts, so unavailable is carried as `null` rather than
@@ -431,7 +430,6 @@ pub async fn patch_user_settings(
¤t,
profile_id,
UserSettingsMutationV1 {
- upload_enabled: admitted.upload_enabled,
watcher_debounce_ms: admitted.watcher_debounce_ms,
extraction_timeout_secs: admitted.extraction_timeout_secs,
},
@@ -678,7 +676,6 @@ fn user_settings_payload(
code_index_worker_configuration_revision_id: worker_configuration
.configuration_revision_id
.clone(),
- upload_enabled: user.upload_enabled,
code_index_workers: worker_configuration.code_index_workers,
code_index_worker_status: tracedecay_code_index::parallelism::installed_worker_status(),
watcher_debounce: user.watcher_debounce.clone(),
diff --git a/crates/tracedecay-domain/src/configuration.rs b/crates/tracedecay-domain/src/configuration.rs
index 8b771a20c6..304f2e80fd 100644
--- a/crates/tracedecay-domain/src/configuration.rs
+++ b/crates/tracedecay-domain/src/configuration.rs
@@ -61,13 +61,18 @@ pub const RETIRED_CORE_SETTING_KEYS_V1: &[&str] = &[
"index.git_ignore.v1",
"semantic.runtime.v1",
"sync.orphan_db_gc_days.v1",
- USER_UPLOAD_ENABLED_SETTING_KEY,
+ "user.upload_enabled.v1",
USER_WATCHER_DEBOUNCE_MS_SETTING_KEY,
USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY,
];
+/// Profile setting keys that shipped and were then retired. A profile store
+/// carrying one converges by dropping it on the next read; the value has no
+/// registered owner anymore. `user.upload_enabled.v1` was the opt-in of the
+/// worldwide token counter, removed without a final upload.
+pub const RETIRED_PROFILE_SETTING_KEYS_V1: &[&str] = &["user.upload_enabled.v1"];
+
/// Canonical user-profile settings.
-pub const USER_UPLOAD_ENABLED_SETTING_KEY: &str = "user.upload_enabled.v1";
pub const USER_REPEATED_QUERY_HINTS_SETTING_KEY: &str = "user.repeated_query_hints.v1";
pub const USER_CODE_INDEX_WORKERS_SETTING_KEY: &str = "user.code_index_workers.v1";
pub const USER_WATCHER_DEBOUNCE_MS_SETTING_KEY: &str = "user.watcher_debounce_ms.v1";
@@ -80,7 +85,6 @@ pub const USER_WORK_EXPERTISE_CONSENT_SETTING_KEY: &str = "user.work_expertise_c
/// Work read pins it together with the project's consent in one revision.
pub const PROFILE_SETTING_KEYS_V1: &[&str] = &[
USER_CODE_INDEX_WORKERS_SETTING_KEY,
- USER_UPLOAD_ENABLED_SETTING_KEY,
USER_REPEATED_QUERY_HINTS_SETTING_KEY,
USER_WATCHER_DEBOUNCE_MS_SETTING_KEY,
USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY,
@@ -129,7 +133,6 @@ pub const CONFIGURATION_SETTING_KEYS_V1: &[&str] = &[
AUTOMATION_SETTINGS_SETTING_KEY,
LCM_SUMMARIZER_EXECUTABLES_SETTING_KEY,
crate::feedback::PROXIMITY_RISK_THRESHOLD_SETTING_KEY_V1,
- USER_UPLOAD_ENABLED_SETTING_KEY,
USER_REPEATED_QUERY_HINTS_SETTING_KEY,
USER_CODE_INDEX_WORKERS_SETTING_KEY,
USER_WATCHER_DEBOUNCE_MS_SETTING_KEY,
diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs
index 93fce9395a..3c73c73636 100644
--- a/crates/tracedecay-domain/src/lib.rs
+++ b/crates/tracedecay-domain/src/lib.rs
@@ -125,13 +125,13 @@ pub use configuration::{
TELEMETRY_TIMINGS_SETTING_KEY, TopologyConcurrencyPolicyV1, TopologyEscalationPolicyV1,
TopologyGatePolicyV1, TopologyNotificationLevelV1, TopologyPolicyDigestV1,
USER_CODE_INDEX_WORKERS_SETTING_KEY, USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY,
- USER_REPEATED_QUERY_HINTS_SETTING_KEY, USER_UPLOAD_ENABLED_SETTING_KEY,
- USER_WATCHER_DEBOUNCE_MS_SETTING_KEY, USER_WORK_EXPERTISE_CONSENT_SETTING_KEY, UserProfileId,
- WORK_EXECUTABLE_BINDINGS_SETTING_KEY, WORK_TOPOLOGY_POLICY_SETTING_KEY,
- WorkExecutableBindingV1, WorkExecutableCapabilityV1, WorkExpertiseCategoryV1,
- WorkExpertiseConsentV1, WorkTopologyPolicyV1, WorktreeCleanlinessRequirementV1,
- WorktreePlacementModeV1, WorktreePlacementRootId, WorktreeRetentionPolicyV1,
- WorktreeRootPolicyV1, resolve_restrictive_capabilities, safe_work_topology_policy_v1,
+ USER_REPEATED_QUERY_HINTS_SETTING_KEY, USER_WATCHER_DEBOUNCE_MS_SETTING_KEY,
+ USER_WORK_EXPERTISE_CONSENT_SETTING_KEY, UserProfileId, WORK_EXECUTABLE_BINDINGS_SETTING_KEY,
+ WORK_TOPOLOGY_POLICY_SETTING_KEY, WorkExecutableBindingV1, WorkExecutableCapabilityV1,
+ WorkExpertiseCategoryV1, WorkExpertiseConsentV1, WorkTopologyPolicyV1,
+ WorktreeCleanlinessRequirementV1, WorktreePlacementModeV1, WorktreePlacementRootId,
+ WorktreeRetentionPolicyV1, WorktreeRootPolicyV1, resolve_restrictive_capabilities,
+ safe_work_topology_policy_v1,
};
pub use cursor_binding::{
CURSOR_INVALID_CODE, CURSOR_PARAMETER_CHANGED_CODE, CursorBindingBuilderV1,
diff --git a/crates/tracedecay-global-db/src/configuration/registry.rs b/crates/tracedecay-global-db/src/configuration/registry.rs
index 6645a1aa37..e31e1ece16 100644
--- a/crates/tracedecay-global-db/src/configuration/registry.rs
+++ b/crates/tracedecay-global-db/src/configuration/registry.rs
@@ -23,10 +23,9 @@ use tracedecay_domain::configuration::{
SYNC_WATCH_MAX_DELAY_MS_SETTING_KEY, SYNC_WATCH_MAX_PROJECTS_SETTING_KEY, SettingDefinitionV1,
SettingKey, SettingScopeV1, SettingSensitivityV1, TELEMETRY_TIMINGS_SETTING_KEY,
USER_CODE_INDEX_WORKERS_SETTING_KEY, USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY,
- USER_REPEATED_QUERY_HINTS_SETTING_KEY, USER_UPLOAD_ENABLED_SETTING_KEY,
- USER_WATCHER_DEBOUNCE_MS_SETTING_KEY, USER_WORK_EXPERTISE_CONSENT_SETTING_KEY,
- WORK_EXECUTABLE_BINDINGS_SETTING_KEY, WORK_TOPOLOGY_POLICY_SETTING_KEY, WorkExpertiseConsentV1,
- safe_work_topology_policy_v1,
+ USER_REPEATED_QUERY_HINTS_SETTING_KEY, USER_WATCHER_DEBOUNCE_MS_SETTING_KEY,
+ USER_WORK_EXPERTISE_CONSENT_SETTING_KEY, WORK_EXECUTABLE_BINDINGS_SETTING_KEY,
+ WORK_TOPOLOGY_POLICY_SETTING_KEY, WorkExpertiseConsentV1, safe_work_topology_policy_v1,
};
use tracedecay_domain::feedback::PROXIMITY_RISK_THRESHOLD_SETTING_KEY_V1;
use tracedecay_domain::{DomainError, validate_index_path_patterns};
@@ -415,11 +414,6 @@ fn register_profile_scalar_settings(
registry: &mut ConfigurationRegistry,
) -> Result<(), ConfigurationRegistryError> {
for (key, default_value, restart_requirement) in [
- (
- USER_UPLOAD_ENABLED_SETTING_KEY,
- ConfigurationValueV1::Boolean(false),
- RestartRequirementV1::None,
- ),
(
USER_REPEATED_QUERY_HINTS_SETTING_KEY,
ConfigurationValueV1::Boolean(false),
@@ -862,11 +856,6 @@ mod user_profile_settings_tests {
fn editable_profile_settings_are_registered_with_exact_scope_and_restart_semantics() {
let registry = ConfigurationRegistry::profile().expect("registry");
for (raw_key, kind, restart) in [
- (
- USER_UPLOAD_ENABLED_SETTING_KEY,
- ConfigurationValueKindV1::Boolean,
- RestartRequirementV1::None,
- ),
(
USER_REPEATED_QUERY_HINTS_SETTING_KEY,
ConfigurationValueKindV1::Boolean,
diff --git a/crates/tracedecay-global-db/src/configuration/resolver.rs b/crates/tracedecay-global-db/src/configuration/resolver.rs
index 233bb9b55d..2dffabd8ab 100644
--- a/crates/tracedecay-global-db/src/configuration/resolver.rs
+++ b/crates/tracedecay-global-db/src/configuration/resolver.rs
@@ -220,7 +220,7 @@ mod tests {
},
revision_id: id("revision.project.1"),
entries: BTreeMap::from([(
- id("user.upload_enabled.v1"),
+ id("user.repeated_query_hints.v1"),
ConfigurationValueV1::Boolean(true),
)]),
};
@@ -228,7 +228,7 @@ mod tests {
resolve_configuration(®istry, &[layer])
.unwrap_err()
.to_string(),
- "setting user.upload_enabled.v1 cannot be placed in Project { project_id: ProjectId(\"project.fixture\") }"
+ "setting user.repeated_query_hints.v1 cannot be placed in Project { project_id: ProjectId(\"project.fixture\") }"
);
let user_layer = ConfigurationLayerV1 {
@@ -237,7 +237,7 @@ mod tests {
},
revision_id: id("revision.profile.1"),
entries: BTreeMap::from([(
- id("user.upload_enabled.v1"),
+ id("user.repeated_query_hints.v1"),
ConfigurationValueV1::Boolean(true),
)]),
};
diff --git a/crates/tracedecay-global-db/src/configuration/store.rs b/crates/tracedecay-global-db/src/configuration/store.rs
index e8eed26376..b7a4948960 100644
--- a/crates/tracedecay-global-db/src/configuration/store.rs
+++ b/crates/tracedecay-global-db/src/configuration/store.rs
@@ -18,10 +18,10 @@ use tracedecay_domain::configuration::{
ConfigurationReceiptId, ConfigurationRevisionId, ConfigurationSnapshotV1, ConfigurationValueV1,
INDEX_NATIVE_GRAPH_ACTIVATION_SETTING_KEY, LCM_SUMMARIZER_EXECUTABLES_SETTING_KEY,
ProtectedChange, ProtectedChangePlan, ProtectedChangeSnapshotError,
- RETIRED_CORE_SETTING_KEYS_V1, RedactedConfigurationChangeV1, RollbackModeV1,
- SOURCE_BINDINGS_SETTING_KEY, SYNC_WATCH_LINKED_WORKTREES_SETTING_KEY, ScopeControlOperationV1,
- SettingKey, USER_CODE_INDEX_WORKERS_SETTING_KEY, USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY,
- USER_REPEATED_QUERY_HINTS_SETTING_KEY, USER_UPLOAD_ENABLED_SETTING_KEY,
+ RETIRED_CORE_SETTING_KEYS_V1, RETIRED_PROFILE_SETTING_KEYS_V1, RedactedConfigurationChangeV1,
+ RollbackModeV1, SOURCE_BINDINGS_SETTING_KEY, SYNC_WATCH_LINKED_WORKTREES_SETTING_KEY,
+ ScopeControlOperationV1, SettingKey, USER_CODE_INDEX_WORKERS_SETTING_KEY,
+ USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY, USER_REPEATED_QUERY_HINTS_SETTING_KEY,
USER_WATCHER_DEBOUNCE_MS_SETTING_KEY, UserProfileId, WORK_TOPOLOGY_POLICY_SETTING_KEY,
};
use tracedecay_domain::{AccessPolicyDigest, ActorId, ManifestDigest, UtcMicros, canonical_sha256};
@@ -666,7 +666,6 @@ pub struct ProfileCodeIndexWorkerCommitV1 {
/// Profile settings registered after a profile store was first initialized.
/// A store written before one existed converges by adding its default.
const PROFILE_ADDITIVE_SETTING_KEYS: &[&str] = &[
- USER_UPLOAD_ENABLED_SETTING_KEY,
USER_REPEATED_QUERY_HINTS_SETTING_KEY,
USER_WATCHER_DEBOUNCE_MS_SETTING_KEY,
USER_EXTRACTION_TIMEOUT_SECS_SETTING_KEY,
@@ -725,12 +724,19 @@ impl<'db> ProfileConfigurationStore<'db> {
}
}
let current = self.current_state().await?;
- if registry.definitions().all(|definition| {
- current
- .snapshot
- .effective_values
- .contains_key(&definition.key)
- }) {
+ let carries_retired_key = current.snapshot.effective_values.keys().any(|key| {
+ RETIRED_PROFILE_SETTING_KEYS_V1
+ .iter()
+ .any(|retired| key.as_str() == *retired)
+ });
+ if !carries_retired_key
+ && registry.definitions().all(|definition| {
+ current
+ .snapshot
+ .effective_values
+ .contains_key(&definition.key)
+ })
+ {
return Self::project(&self.profile_id, current, ®istry);
}
let converged = match self
@@ -740,7 +746,7 @@ impl<'db> ProfileConfigurationStore<'db> {
occurred_at,
®istry,
PROFILE_ADDITIVE_SETTING_KEYS,
- &[],
+ RETIRED_PROFILE_SETTING_KEYS_V1,
)
.await
{
diff --git a/crates/tracedecay-global-db/src/configuration/store/tests/persistence.rs b/crates/tracedecay-global-db/src/configuration/store/tests/persistence.rs
index 61ccf8c36a..a4913c6b31 100644
--- a/crates/tracedecay-global-db/src/configuration/store/tests/persistence.rs
+++ b/crates/tracedecay-global-db/src/configuration/store/tests/persistence.rs
@@ -18,8 +18,9 @@ use crate::configuration::registry::ConfigurationRegistry;
use crate::configuration::resolver::resolve_configuration;
use tracedecay_domain::configuration::{
CodeIndexWorkerSelectionV1, ConfigurationIdempotencyKey, ConfigurationLayerIdV1,
- ConfigurationMutationOperationV1, SettingKey, USER_CODE_INDEX_WORKERS_SETTING_KEY,
- USER_REPEATED_QUERY_HINTS_SETTING_KEY, USER_UPLOAD_ENABLED_SETTING_KEY,
+ ConfigurationMutationOperationV1, ConfigurationValueKindV1, DeprecationStateV1,
+ RestartRequirementV1, SettingDefinitionV1, SettingKey, SettingScopeV1, SettingSensitivityV1,
+ USER_CODE_INDEX_WORKERS_SETTING_KEY, USER_REPEATED_QUERY_HINTS_SETTING_KEY,
};
#[asupersync::test]
@@ -176,10 +177,6 @@ async fn worker_only_profile_store_converges_onto_the_profile_registry() {
converged.code_index_workers().unwrap().selection,
CodeIndexWorkerSelectionV1::Exact { workers: 8 }
);
- assert_eq!(
- converged.value(USER_UPLOAD_ENABLED_SETTING_KEY).unwrap(),
- &ConfigurationValueV1::Boolean(false)
- );
assert_eq!(
converged
.value(USER_REPEATED_QUERY_HINTS_SETTING_KEY)
@@ -188,6 +185,122 @@ async fn worker_only_profile_store_converges_onto_the_profile_registry() {
);
}
+/// A profile store written while the worldwide counter shipped carries the
+/// `user.upload_enabled.v1` opt-in, possibly enabled. It converges on the
+/// first read: the retired key is dropped without any upload, the operator's
+/// other values survive, and a second read finds nothing left to do.
+#[asupersync::test]
+async fn profile_store_carrying_the_retired_upload_setting_converges_and_keeps_other_values() {
+ let directory = tempfile::tempdir().unwrap();
+ let profile_root = directory.path().join("profile");
+ let runtime = crate::tests::harness::HostAdmissionTestRuntimeV1::profile(&profile_root)
+ .await
+ .unwrap();
+ let db = runtime
+ .registered_database(HostAdmissionScope::Profile)
+ .unwrap();
+ let profile_id = db.binding().shard_id.profile_id.clone();
+ let upload_key = SettingKey::new("user.upload_enabled.v1").unwrap();
+ let worker_key = SettingKey::new(USER_CODE_INDEX_WORKERS_SETTING_KEY).unwrap();
+ assert!(
+ ConfigurationRegistry::profile()
+ .unwrap()
+ .definition(&upload_key)
+ .is_err(),
+ "the current profile registry must not register the retired key"
+ );
+ let mut legacy_definitions = ConfigurationRegistry::profile()
+ .unwrap()
+ .definitions()
+ .cloned()
+ .collect::>();
+ legacy_definitions.push(SettingDefinitionV1 {
+ key: upload_key.clone(),
+ schema_revision: crate::configuration::registry::CONFIGURATION_REGISTRY_SCHEMA_REVISION,
+ value_kind: ConfigurationValueKindV1::Boolean,
+ default_value: ConfigurationValueV1::Boolean(false),
+ sensitivity: SettingSensitivityV1::Public,
+ scope: SettingScopeV1::UserProfile,
+ restart_requirement: RestartRequirementV1::None,
+ deprecation: DeprecationStateV1::Active,
+ });
+ let legacy = ConfigurationRegistry::from_definitions(legacy_definitions).unwrap();
+ let initial_revision = id("configuration.profile.initial.v1");
+ GlobalDbConfigurationControlStore::new_registered(db)
+ .initialize_canonical_with_registry(
+ &initial_revision,
+ &resolve_configuration(&legacy, &[]).unwrap(),
+ UtcMicros(1),
+ &legacy,
+ )
+ .await
+ .unwrap();
+ let layer = ConfigurationLayerIdV1::UserProfile {
+ profile_id: profile_id.clone(),
+ };
+ let mut revision = initial_revision;
+ for (name, key, value) in [
+ (
+ "worker",
+ worker_key.clone(),
+ ConfigurationValueV1::CodeIndexWorkerSelection(CodeIndexWorkerSelectionV1::Exact {
+ workers: 8,
+ }),
+ ),
+ (
+ "upload",
+ upload_key.clone(),
+ ConfigurationValueV1::Boolean(true),
+ ),
+ ] {
+ let authority = control_authority_with_key_for_layer(
+ ConfigurationMutationOperationV1::DirectMutation,
+ &revision,
+ Some(
+ ConfigurationIdempotencyKey::new(format!(
+ "configuration.idempotency.legacy-{name}"
+ ))
+ .unwrap(),
+ ),
+ layer.clone(),
+ );
+ let transaction = db.begin_write_transaction().await.unwrap();
+ let committed = super::super::mutation::commit_direct_in_transaction_with_registry(
+ &transaction,
+ &authority,
+ &DirectConfigurationMutation::Set {
+ layer: layer.clone(),
+ key,
+ value: Box::new(value),
+ },
+ &revision,
+ &legacy,
+ )
+ .await
+ .unwrap();
+ transaction.commit().await.unwrap();
+ revision = committed.current.revision_id;
+ }
+
+ let store = super::super::ProfileConfigurationStore::new_registered(db, &profile_id).unwrap();
+ let converged = store.read_or_initialize(UtcMicros(3)).await.unwrap();
+
+ assert_ne!(converged.revision_id, revision);
+ assert!(
+ !converged
+ .snapshot
+ .effective_values
+ .contains_key(&upload_key),
+ "the retired upload opt-in must be dropped, not carried"
+ );
+ assert_eq!(
+ converged.code_index_workers().unwrap().selection,
+ CodeIndexWorkerSelectionV1::Exact { workers: 8 }
+ );
+ let settled = store.read_or_initialize(UtcMicros(4)).await.unwrap();
+ assert_eq!(settled.revision_id, converged.revision_id);
+}
+
#[asupersync::test]
async fn project_revision_store_rejects_profile_worker_snapshot() {
let (_directory, runtime, root) = global_setup().await;
diff --git a/crates/tracedecay-global-db/src/support.rs b/crates/tracedecay-global-db/src/support.rs
index 6f35e1d369..7460e5a8ee 100644
--- a/crates/tracedecay-global-db/src/support.rs
+++ b/crates/tracedecay-global-db/src/support.rs
@@ -61,8 +61,8 @@ pub fn env_flag(name: &str) -> bool {
std::env::var(name).is_ok_and(|value| env_value_truthy(&value))
}
-/// Whether user-level global accounting (the cross-project `savings_ledger`
-/// plus worldwide-counter flushes in the MCP server) is enabled.
+/// Whether user-level global accounting (the cross-project `savings_ledger`)
+/// is enabled.
///
/// Enabled **by default**: every other writer of the user-level `global.db`
/// (CLI sync, hooks, `tracedecay cost`, the dashboard) is ungated, and the
diff --git a/crates/tracedecay-session-memory/src/user_config.rs b/crates/tracedecay-session-memory/src/user_config.rs
index 82235f2b1b..9ca04d9d85 100644
--- a/crates/tracedecay-session-memory/src/user_config.rs
+++ b/crates/tracedecay-session-memory/src/user_config.rs
@@ -15,11 +15,16 @@ use tracedecay_domain::canonical_text::default_true;
use tracedecay_automation::config::AutomationConfig;
use tracedecay_runtime_core::storage::{append_lock_path, retry_transient_file_op};
-/// Keys canonical `user.*` configuration settings own, and caches of the
-/// retired status worldwide/flag decoration. They are never read from this
-/// file.
-const RETIRED_KEYS: [&str; 7] = [
+/// Keys canonical `user.*` configuration settings own, and state of the
+/// retired worldwide counter (its upload opt-in, pending amount and flush
+/// timestamps) and status worldwide/flag decoration. They are never read from
+/// this file; an older profile that still carries them loads normally and the
+/// next write erases them. The pending amount is dropped without an upload.
+const RETIRED_KEYS: [&str; 10] = [
"upload_enabled",
+ "pending_upload",
+ "last_upload_at",
+ "last_flush_attempt_at",
"watcher_debounce",
"extraction_timeout_secs",
"last_worldwide_total",
@@ -31,18 +36,6 @@ const RETIRED_KEYS: [&str; 7] = [
/// User-level tracedecay configuration.
#[derive(Debug, Serialize, Deserialize)]
pub struct UserConfig {
- /// Tokens accumulated locally, not yet uploaded.
- #[serde(default)]
- pub pending_upload: u64,
-
- /// UNIX timestamp of last successful upload.
- #[serde(default)]
- pub last_upload_at: i64,
-
- /// UNIX timestamp of last flush attempt (success or failure).
- #[serde(default)]
- pub last_flush_attempt_at: i64,
-
/// Cached latest version from GitHub releases.
#[serde(default)]
pub cached_latest_version: String,
@@ -102,9 +95,6 @@ pub struct UserConfig {
impl Default for UserConfig {
fn default() -> Self {
Self {
- pending_upload: 0,
- last_upload_at: 0,
- last_flush_attempt_at: 0,
cached_latest_version: String::new(),
last_version_check_at: 0,
last_version_warning_at: 0,
@@ -275,7 +265,7 @@ impl UserConfig {
///
/// An unreadable or malformed file is an error, not defaults: those would
/// silently turn a stored opt-out (dashboard, memory injection) back on
- /// and drop the tracked hosts and pending counts.
+ /// and drop the tracked hosts.
pub fn load(profile_root: &Path) -> std::result::Result {
read_profile_config::(profile_root).map(Self::without_retired_keys)
}
@@ -441,7 +431,7 @@ mod tests {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
// The torn write seen in the wild: a valid line followed by a bare
// " true" orphan with no key.
- let torn = "pending_upload = 0\n true";
+ let torn = "last_version_check_at = 0\n true";
std::fs::write(&path, torn).unwrap();
let entries = || {
let mut names = std::fs::read_dir(path.parent().unwrap())
@@ -452,7 +442,7 @@ mod tests {
names
};
let config = UserConfig {
- pending_upload: 7,
+ last_version_check_at: 7,
..UserConfig::default()
};
@@ -471,9 +461,9 @@ mod tests {
assert_eq!(load_error.to_string(), expected);
assert_eq!(entries(), vec!["config.toml".to_owned()]);
- std::fs::write(&path, "pending_upload = 0\n").unwrap();
+ std::fs::write(&path, "last_version_check_at = 0\n").unwrap();
config.save(profile).expect("a parseable config saves");
- assert_eq!(UserConfig::load(profile).unwrap().pending_upload, 7);
+ assert_eq!(UserConfig::load(profile).unwrap().last_version_check_at, 7);
}
#[test]
@@ -529,13 +519,13 @@ mod tests {
let path = config_path(profile);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
- let handles: Vec<_> = (0..8u64)
+ let handles: Vec<_> = (0..8i64)
.map(|thread_idx| {
let profile = profile.to_path_buf();
std::thread::spawn(move || {
- for i in 0..20u64 {
+ for i in 0..20i64 {
let config = UserConfig {
- pending_upload: thread_idx * 100 + i,
+ last_version_check_at: thread_idx * 100 + i,
..UserConfig::default()
};
// Every write must succeed and leave a parseable file.
@@ -554,10 +544,10 @@ mod tests {
let parsed = toml::from_str::(&saved)
.expect("file must be parseable after concurrent saves");
assert_eq!(
- parsed.pending_upload % 100,
+ parsed.last_version_check_at % 100,
19,
"the surviving file is one writer's final save, got {}",
- parsed.pending_upload
+ parsed.last_version_check_at
);
}
@@ -583,9 +573,9 @@ mod tests {
}
});
- for i in 0..150u64 {
+ for i in 0..150i64 {
let config = UserConfig {
- pending_upload: i,
+ last_version_check_at: i,
..UserConfig::default()
};
config.save(profile).expect("writer save should succeed");
@@ -595,7 +585,7 @@ mod tests {
assert_eq!(
toml::from_str::(&saved)
.expect("final config parses")
- .pending_upload,
+ .last_version_check_at,
149
);
}
@@ -608,7 +598,9 @@ mod tests {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(
&path,
- "upload_enabled = true\nwatcher_debounce = \"9s\"\nextraction_timeout_secs = 5\n\
+ "upload_enabled = true\npending_upload = 4200\nlast_upload_at = 1711375100\n\
+ last_flush_attempt_at = 1711375150\n\
+ watcher_debounce = \"9s\"\nextraction_timeout_secs = 5\n\
last_worldwide_total = 1000000\nlast_worldwide_fetch_at = 1711375200\n\
cached_country_flags = [\"🇺🇸\", \"🇩🇪\"]\nlast_flags_fetch_at = 1711375200\n\
future_key = \"keep-me\"\n[future_table]\nflag = true\n",
@@ -616,7 +608,7 @@ mod tests {
.unwrap();
let mut config = UserConfig::load(profile).unwrap();
- config.pending_upload = 3;
+ config.last_version_check_at = 3;
config
.save(profile)
@@ -625,9 +617,12 @@ mod tests {
assert!(saved.contains("future_key = \"keep-me\""));
assert!(saved.contains("[future_table]"));
assert!(saved.contains("flag = true"));
- assert!(saved.contains("pending_upload = 3"));
+ assert!(saved.contains("last_version_check_at = 3"));
for retired in [
"upload_enabled",
+ "pending_upload",
+ "last_upload_at",
+ "last_flush_attempt_at",
"watcher_debounce",
"extraction_timeout_secs",
"last_worldwide_total",
diff --git a/crates/tracedecay/src/daemon/production_harness/configuration_idempotency_journey_test.rs b/crates/tracedecay/src/daemon/production_harness/configuration_idempotency_journey_test.rs
index 73e5f96ac1..e35f722b0a 100644
--- a/crates/tracedecay/src/daemon/production_harness/configuration_idempotency_journey_test.rs
+++ b/crates/tracedecay/src/daemon/production_harness/configuration_idempotency_journey_test.rs
@@ -10,7 +10,7 @@ use tracedecay_contracts::{
use tracedecay_domain::configuration::{
ConfigurationIdempotencyKey, ConfigurationLayerIdV1, ConfigurationRevisionId,
ConfigurationValueV1, SettingKey, TELEMETRY_TIMINGS_SETTING_KEY,
- USER_UPLOAD_ENABLED_SETTING_KEY, UserProfileId,
+ USER_REPEATED_QUERY_HINTS_SETTING_KEY, UserProfileId,
};
use tracedecay_sdk::client::{Client, ClientError, ConnectionMode};
use tracedecay_sdk::operations::{ApplicationConfigurationSet, TypedOperation};
@@ -330,7 +330,7 @@ async fn user_profile_configuration_batch_has_cli_dashboard_parity_after_restart
layer: ConfigurationLayerIdV1::UserProfile {
profile_id: profile_id.clone(),
},
- key: SettingKey::new(USER_UPLOAD_ENABLED_SETTING_KEY).expect("user setting key"),
+ key: SettingKey::new(USER_REPEATED_QUERY_HINTS_SETTING_KEY).expect("user setting key"),
value: Box::new(ConfigurationValueV1::Boolean(true)),
}],
expected_revision: expected_revision.clone(),
@@ -390,7 +390,7 @@ async fn user_profile_configuration_batch_has_cli_dashboard_parity_after_restart
let changed = ConfigurationBatchRequestV1 {
mutations: vec![ConfigurationDirectMutationRequestV1::Set {
layer: ConfigurationLayerIdV1::UserProfile { profile_id },
- key: SettingKey::new(USER_UPLOAD_ENABLED_SETTING_KEY).expect("user setting key"),
+ key: SettingKey::new(USER_REPEATED_QUERY_HINTS_SETTING_KEY).expect("user setting key"),
value: Box::new(ConfigurationValueV1::Boolean(false)),
}],
..request
diff --git a/crates/tracedecay/src/doctor.rs b/crates/tracedecay/src/doctor.rs
index 6b8fb5e20a..3b98ef2c49 100644
--- a/crates/tracedecay/src/doctor.rs
+++ b/crates/tracedecay/src/doctor.rs
@@ -11,26 +11,13 @@ use tracedecay_contracts::project_open::{
use tracedecay_contracts::storage::{
SchemaConvergenceFindingV1, SchemaConvergenceProgressV1, SchemaConvergenceStateV1,
};
-use tracedecay_contracts::{ApplicationOutcome, ResolvedSetting};
-use tracedecay_domain::configuration::{
- ConfigurationValueV1, SettingKey, USER_UPLOAD_ENABLED_SETTING_KEY,
-};
use tracedecay_domain::errors::ProjectOpenFailureKind;
-use tracedecay_tool_catalog::{ApplicationSurfaceOperation, BindingSurface};
use tracedecay_agent_hosts::agents::{self, DoctorCounters, HealthcheckContext};
use tracedecay_application::advisory::github_runtime;
use tracedecay_automation_runtime::automation::effect_runtime::pending_automation_effect_resets_blocking;
-use tracedecay_contracts::request_identity::{GlobalRequestSurface, mint_global_request_id};
-use tracedecay_contracts::{ConfigurationGetRequestV1, ConfigurationWireRequestV1};
-use tracedecay_daemon_protocol::ApplicationSurfaceRequest;
-use tracedecay_daemon_protocol::RequestedOutputFormat;
-use tracedecay_daemon_service::application_surface::{
- execute_application_surface, resolve_application_surface_dispatch,
-};
#[cfg(unix)]
use tracedecay_daemon_service::logging::recent_watcher_events;
-use tracedecay_runtime_core::text::format_token_count;
/// Opens an isolated daemon-registered profile database so Doctor tests can
/// exercise the read-only session-temporal health adapter against the real
@@ -96,7 +83,6 @@ impl DoctorTestRuntime {
/// itself, the composition root cannot depend on the CLI crate.
#[derive(Clone, Copy)]
pub struct AdmittedDoctorNetworkProbes {
- pub fetch_worldwide_total: fn() -> Option,
pub fetch_latest_version:
fn() -> Result,
}
@@ -150,7 +136,7 @@ pub async fn run_doctor(
let project_path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let daemon_listening = tracedecay_daemon_control::daemon_socket_connectable(profile);
- let mut independent = launch_independent_doctor_checks(
+ let independent = launch_independent_doctor_checks(
profile,
build_version,
&project_path,
@@ -173,14 +159,9 @@ pub async fn run_doctor(
&mut pending_reset,
)?;
dc.replay_from(join_doctor_check(independent.watcher).await?);
- let upload_enabled = join_doctor_check(independent.upload).await?;
- check_user_config(&mut dc, profile.data_dir(), upload_enabled.as_ref());
- independent.network.admit_worldwide(upload_enabled.as_ref());
+ check_user_config(&mut dc, profile.data_dir());
dc.replay_from(join_doctor_check(independent.hosts).await?);
- independent
- .network
- .report(&mut dc, upload_enabled.as_ref())
- .await?;
+ independent.network.report(&mut dc).await?;
print_summary(&dc);
let completion = doctor_result(&dc, pending_reset);
@@ -197,7 +178,6 @@ struct IndependentDoctorChecks {
daemon_status:
DoctorCheck>>>,
watcher: DoctorCheck,
- upload: DoctorCheck>,
hosts: DoctorCheck,
network: DoctorNetworkChecks,
}
@@ -220,7 +200,6 @@ fn launch_independent_doctor_checks(
let project_for_hosts = project_path.to_path_buf();
let profile_for_status = profile.clone();
let project_for_status = project_path.to_path_buf();
- let profile_for_upload = profile.clone();
IndependentDoctorChecks {
daemon_service: spawn_doctor_check(move || {
@@ -248,15 +227,6 @@ fn launch_independent_doctor_checks(
watcher: spawn_doctor_check(move || {
collect_quiet(|dc| check_watcher(dc, &profile_for_watcher))
}),
- upload: spawn_doctor_task(async move {
- if daemon_listening {
- configured_upload_enabled(&profile_for_upload)
- .await
- .map(UploadSetting::Resolved)
- } else {
- Ok(UploadSetting::DaemonUnavailable)
- }
- }),
hosts: spawn_doctor_check(move || {
collect_quiet(|dc| check_host_integrations(dc, &profile_for_hosts, &project_for_hosts))
}),
@@ -463,7 +433,6 @@ struct ObservedDoctorFindingsV1 {
}
/// Typed state for a profile with no listening daemon.
-const DAEMON_UNAVAILABLE: &str = "daemon_unavailable";
const DAEMON_UNAVAILABLE_STATEMENT: &str = "daemon_unavailable: no TraceDecay daemon is listening \
for this profile, so the daemon's canonical Doctor findings were not read and only \
binary-local checks ran. Pending operator action: start the daemon (`tracedecay daemon \
@@ -1216,135 +1185,24 @@ fn automation_effect_reset_findings(
.collect())
}
-#[tracing::instrument(name = "doctor.config.upload", level = "trace", skip_all)]
-async fn configured_upload_enabled(
- profile: &tracedecay_runtime_core::config::ProfileRoot,
-) -> tracedecay_domain::errors::Result {
- let setting = configured_setting(profile, USER_UPLOAD_ENABLED_SETTING_KEY).await?;
- match setting.effective_value {
- ConfigurationValueV1::Boolean(enabled) => Ok(enabled),
- _ => Err(tracedecay_domain::errors::TraceDecayError::Config {
- message: "worldwide counter upload setting is not boolean".to_owned(),
- }),
- }
-}
-
-async fn configured_setting(
- profile: &tracedecay_runtime_core::config::ProfileRoot,
- key: &str,
-) -> tracedecay_domain::errors::Result {
- let operation = ApplicationSurfaceOperation::ConfigurationGet;
- let key = SettingKey::new(key).map_err(|error| {
- tracedecay_domain::errors::TraceDecayError::Config {
- message: error.to_string(),
- }
- })?;
- let request_id =
- mint_global_request_id(GlobalRequestSurface::DaemonDoctor).map_err(|error| {
- tracedecay_domain::errors::TraceDecayError::Config {
- message: format!("could not create Doctor configuration request: {error}"),
- }
- })?;
- // The setting belongs to the profile: the request names no project.
- let handshake = crate::daemon::handshake_for_current_client(profile, None, None, false, false)?;
- let client = crate::daemon::invocation_client_for_current(profile, handshake)?;
- let dispatched = resolve_application_surface_dispatch(
- BindingSurface::Cli,
- operation,
- request_id.clone(),
- ApplicationSurfaceRequest::Configuration(ConfigurationWireRequestV1::Get(
- ConfigurationGetRequestV1 { key: key.clone() },
- )),
- RequestedOutputFormat::Json,
- )
- .map_err(|error| tracedecay_domain::errors::TraceDecayError::Config {
- message: error.to_string(),
- })?;
- let result = execute_application_surface(operation, dispatched, Some(&client))
- .await
- .map_err(|error| tracedecay_domain::errors::TraceDecayError::Config {
- message: error.to_string(),
- })?;
- let envelope = match result.result {
- Ok(envelope) => envelope,
- Err(problem) => {
- return Err(tracedecay_domain::errors::TraceDecayError::Config {
- message: problem.problem.summary(),
- });
- }
- };
- let ApplicationOutcome::Evidence(evidence) = envelope.outcome else {
- return Err(tracedecay_domain::errors::TraceDecayError::Config {
- message: "configuration get returned a non-evidence outcome".to_owned(),
- });
- };
- let setting: ResolvedSetting = serde_json::from_value(evidence.payload.ok_or_else(|| {
- tracedecay_domain::errors::TraceDecayError::Config {
- message: "configuration get omitted its payload".to_owned(),
- }
- })?)
- .map_err(|error| tracedecay_domain::errors::TraceDecayError::Config {
- message: format!("configuration get returned an invalid setting: {error}"),
- })?;
- if setting.key != key {
- return Err(tracedecay_domain::errors::TraceDecayError::Config {
- message: "configuration get returned the wrong setting".to_owned(),
- });
- }
- Ok(setting)
-}
-
-/// The worldwide-counter upload setting as the profile's configuration owner
-/// answered it.
-#[derive(Debug, PartialEq, Eq)]
-enum UploadSetting {
- Resolved(bool),
- /// No daemon listens for this profile, so no configuration owner answered.
- DaemonUnavailable,
-}
-
-/// Check canonical user configuration and pending upload state.
-fn check_user_config(
- dc: &mut DoctorCounters,
- profile_root: &Path,
- upload_enabled: Result<&UploadSetting, &tracedecay_domain::errors::TraceDecayError>,
-) {
+/// Check the profile `config.toml` and the GitHub review sources it names.
+fn check_user_config(dc: &mut DoctorCounters, profile_root: &Path) {
dc.section("User config");
- match upload_enabled {
- Ok(UploadSetting::Resolved(true)) => dc.pass("Worldwide counter upload enabled"),
- Ok(UploadSetting::Resolved(false)) => {
- dc.info("Worldwide counter upload disabled (default)");
- }
- Ok(UploadSetting::DaemonUnavailable) => {
- dc.info(&format!(
- "Worldwide counter upload setting unread: {DAEMON_UNAVAILABLE}"
- ));
- }
- Err(error) => dc.warn(&format!(
- "Worldwide counter upload setting unavailable from canonical configuration: {error}"
- )),
- }
match tracedecay_session_memory::user_config::UserConfig::load(profile_root) {
- Ok(config) => {
- if config.pending_upload > 0 {
- dc.info(&format!("Pending upload: {} tokens", config.pending_upload));
- }
- match github_runtime::check_configured_github_review_sources_v1(profile_root) {
- Ok(findings) => {
- for finding in findings {
- dc.fail(&format!(
- "GitHub review source {finding}, so it is not registered; fix or \
+ Ok(_) => match github_runtime::check_configured_github_review_sources_v1(profile_root) {
+ Ok(findings) => {
+ for finding in findings {
+ dc.fail(&format!(
+ "GitHub review source {finding}, so it is not registered; fix or \
remove its github_review_sources entry in {}",
- tracedecay_session_memory::user_config::config_path(profile_root)
- .display()
- ));
- }
+ tracedecay_session_memory::user_config::config_path(profile_root).display()
+ ));
}
- Err(error) => dc.fail(&format!(
- "GitHub review sources are unusable, none is registered: {error}"
- )),
}
- }
+ Err(error) => dc.fail(&format!(
+ "GitHub review sources are unusable, none is registered: {error}"
+ )),
+ },
Err(error) => dc.fail(&format!("Profile config is unusable: {error}")),
}
}
@@ -1464,72 +1322,28 @@ fn tracked_hosts(dc: &mut DoctorCounters, profile_root: &Path) -> Vec {
struct DoctorNetworkChecks {
latest: DoctorCheck>,
- worldwide: Option>>,
- fetch_worldwide_total: fn() -> Option,
}
impl DoctorNetworkChecks {
fn start(network: AdmittedDoctorNetworkProbes) -> Self {
Self {
latest: spawn_doctor_check(move || (network.fetch_latest_version)()),
- worldwide: None,
- fetch_worldwide_total: network.fetch_worldwide_total,
- }
- }
-
- fn admit_worldwide(
- &mut self,
- upload_enabled: Result<&UploadSetting, &tracedecay_domain::errors::TraceDecayError>,
- ) {
- if matches!(upload_enabled, Ok(UploadSetting::Resolved(true))) {
- self.worldwide = Some(spawn_doctor_check(self.fetch_worldwide_total));
}
}
#[tracing::instrument(name = "doctor.check.network", level = "trace", skip_all)]
- async fn report(
- self,
- dc: &mut DoctorCounters,
- upload_enabled: Result<&UploadSetting, &tracedecay_domain::errors::TraceDecayError>,
- ) -> tracedecay_domain::errors::Result<()> {
+ async fn report(self, dc: &mut DoctorCounters) -> tracedecay_domain::errors::Result<()> {
let latest = join_doctor_check(self.latest).await?;
- let worldwide = match self.worldwide {
- Some(handle) => Some(join_doctor_check(handle).await?),
- None => None,
- };
- render_network(dc, upload_enabled, worldwide, latest);
+ render_network(dc, latest);
Ok(())
}
}
fn render_network(
dc: &mut DoctorCounters,
- upload_enabled: Result<&UploadSetting, &tracedecay_domain::errors::TraceDecayError>,
- worldwide: Option>,
latest: Result,
) {
dc.section("Network");
- match upload_enabled {
- Ok(UploadSetting::Resolved(true)) => {
- if let Some(total) = worldwide.flatten() {
- dc.pass(&format!(
- "Worldwide counter reachable (total: {})",
- format_token_count(total)
- ));
- } else {
- dc.warn("Worldwide counter unreachable (offline or timeout)");
- }
- }
- Ok(UploadSetting::Resolved(false)) => {
- dc.info("Worldwide counter skipped (upload disabled)");
- }
- Ok(UploadSetting::DaemonUnavailable) => {
- dc.info(&format!("Worldwide counter check skipped: {DAEMON_UNAVAILABLE}"));
- }
- Err(error) => dc.warn(&format!(
- "Worldwide counter check skipped because canonical configuration is unavailable: {error}"
- )),
- }
match latest {
Ok(latest) => dc.pass(&format!("GitHub releases API reachable (latest v{latest})")),
Err(error) => dc.warn(&format!("GitHub release lookup failed, {error}")),
diff --git a/crates/tracedecay/src/doctor/tests.rs b/crates/tracedecay/src/doctor/tests.rs
index 79c89692ff..a5a9cf4db3 100644
--- a/crates/tracedecay/src/doctor/tests.rs
+++ b/crates/tracedecay/src/doctor/tests.rs
@@ -746,50 +746,26 @@ async fn temporal_health_does_not_block_on_a_saturated_general_reader_lane() {
drop(held);
}
-static NETWORK_PROBE_STARTED: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
-
-fn rendezvous_network_probe() {
- use std::sync::atomic::Ordering;
-
- NETWORK_PROBE_STARTED.fetch_add(1, Ordering::SeqCst);
- while NETWORK_PROBE_STARTED.load(Ordering::SeqCst) < 2 {
- std::thread::yield_now();
- }
-}
-
#[tokio::test]
-async fn independent_network_probes_overlap() {
- use std::sync::atomic::Ordering;
-
- NETWORK_PROBE_STARTED.store(0, Ordering::SeqCst);
+async fn network_section_probes_only_the_github_release_lookup() {
let mut counters = DoctorCounters::quiet();
- let mut network = DoctorNetworkChecks::start(AdmittedDoctorNetworkProbes {
- fetch_worldwide_total: || {
- rendezvous_network_probe();
- Some(7)
- },
- fetch_latest_version: || {
- rendezvous_network_probe();
- Ok("1.2.3".to_owned())
- },
+ let network = DoctorNetworkChecks::start(AdmittedDoctorNetworkProbes {
+ fetch_latest_version: || Ok("1.2.3".to_owned()),
});
- network.admit_worldwide(Ok(&UploadSetting::Resolved(true)));
- network
- .report(&mut counters, Ok(&UploadSetting::Resolved(true)))
- .await
- .unwrap();
+ network.report(&mut counters).await.unwrap();
assert!(
counters.checks.iter().any(|check| check
.message
- .contains("Worldwide counter reachable (total: 7)")),
+ .contains("GitHub releases API reachable (latest v1.2.3)")),
"{:?}",
counters.checks
);
assert!(
- counters.checks.iter().any(|check| check
- .message
- .contains("GitHub releases API reachable (latest v1.2.3)")),
- "{:?}",
+ !counters
+ .checks
+ .iter()
+ .any(|check| check.message.to_lowercase().contains("worldwide")),
+ "doctor must not probe or mention the retired worldwide counter: {:?}",
counters.checks
);
}
diff --git a/crates/tracedecay/src/mcp/server.rs b/crates/tracedecay/src/mcp/server.rs
index 0fe82fc0f2..0e509a912a 100644
--- a/crates/tracedecay/src/mcp/server.rs
+++ b/crates/tracedecay/src/mcp/server.rs
@@ -303,9 +303,6 @@ pub struct McpServer {
/// clone and swap in the freshly synced map on completion.
file_token_map: Arc>>,
tokens_saved: Option,
- last_flushed_tokens: Option,
- /// UNIX timestamp of last worldwide flush (`0` = never).
- last_flush_at: AtomicI64,
/// User-level database tracking all projects (best-effort). Wrapped in
/// `Arc` so spawned savings-recording tasks can hold a cheap clone of
/// the handle instead of opening a new connection per call.
@@ -1006,8 +1003,6 @@ impl McpServer {
diagnostics_lsp,
file_token_map: Arc::new(std::sync::Mutex::new(file_token_map)),
tokens_saved: persisted_tokens_saved.map(AtomicU64::new),
- last_flushed_tokens: persisted_tokens_saved.map(AtomicU64::new),
- last_flush_at: AtomicI64::new(0),
global_db,
accounting_db,
profile,
diff --git a/crates/tracedecay/src/mcp/server/connection.rs b/crates/tracedecay/src/mcp/server/connection.rs
index 69fcf2f938..81f6da9bd1 100644
--- a/crates/tracedecay/src/mcp/server/connection.rs
+++ b/crates/tracedecay/src/mcp/server/connection.rs
@@ -224,8 +224,8 @@ impl McpServer {
}
impl McpServer {
- /// Persists the tokens-saved counter, flushes pending tokens to the
- /// worldwide counter, checkpoints the WAL, and logs a session summary.
+ /// Persists the tokens-saved counter, checkpoints the WAL, and logs a
+ /// session summary.
///
/// Idempotent, safe to call multiple times. `run` invokes it once when
/// its main loop exits; callers (e.g. `main.rs`, tests) may invoke it
@@ -279,46 +279,6 @@ impl McpServer {
}
gdb.checkpoint().await;
}
-
- // Flush remaining delta to worldwide counter (what periodic flushes missed).
- if let Some(last_flushed_tokens) = self.last_flushed_tokens.as_ref() {
- let last_flushed = last_flushed_tokens.load(Ordering::Relaxed);
- if (self.accounting_db.is_some() || self.global_db.is_some())
- && tokens_saved > last_flushed
- {
- let delta = tokens_saved - last_flushed;
- match (self.canonical_upload_enabled().await, self.owner_profile()) {
- (Err(error), _) => failures.push(format!(
- "worldwide counter upload configuration unavailable: {error}"
- )),
- (Ok(_), None) => failures.push(
- "worldwide counter upload skipped: server has no owning profile"
- .to_owned(),
- ),
- (Ok(upload_enabled), Some(profile)) => {
- // The upload is a blocking HTTP call; it stays off
- // the native worker that runs shutdown.
- let profile_root = profile.data_dir().to_path_buf();
- let saved = tracedecay_runtime_core::native_runtime::spawn_blocking(
- move || {
- super::ledger::persist_worldwide_delta(
- &profile_root,
- delta,
- upload_enabled,
- )
- .map_err(|error| error.to_string())
- },
- )
- .await
- .and_then(std::convert::identity);
- if let Err(error) = saved {
- failures
- .push(format!("worldwide counter delta not saved: {error}"));
- }
- }
- }
- }
- }
}
// Checkpoint WAL to merge it into the main database file
diff --git a/crates/tracedecay/src/mcp/server/ledger.rs b/crates/tracedecay/src/mcp/server/ledger.rs
index d7231f1000..31a0070a02 100644
--- a/crates/tracedecay/src/mcp/server/ledger.rs
+++ b/crates/tracedecay/src/mcp/server/ledger.rs
@@ -55,15 +55,9 @@ fn derive_hook_span_git_context(
Some((worktree, branch))
}
-fn configuration_authority_unavailable(detail: impl std::fmt::Display) -> TraceDecayError {
- TraceDecayError::Config {
- message: format!("configuration authority unavailable: {detail}"),
- }
-}
-
-// Global accounting (savings ledger + worldwide-counter flushes) is enabled
-// by default; see `tracedecay_global_db::global_accounting_mode` for the env
-// override precedence.
+// Global accounting (the savings ledger) is enabled by default; see
+// `tracedecay_global_db::global_accounting_mode` for the env override
+// precedence.
/// Where this server's savings-ledger and analytics writes land.
///
@@ -111,37 +105,6 @@ impl McpServer {
matches!(self.ledger_sink(), LedgerSink::Mounted(_))
}
- /// Reads the upload policy from the owning profile's configuration store.
- /// There is deliberately no `config.toml` fallback: without the canonical
- /// authority, the upload decision is unavailable.
- #[tracing::instrument(name = "mcp.ledger.read_upload_policy", level = "trace", skip_all)]
- pub(super) async fn canonical_upload_enabled(&self) -> Result {
- let (Some(database), Some(identity)) =
- (self.profile_session_db.clone(), self.profile_identity())
- else {
- return Err(configuration_authority_unavailable(
- "server has no registered profile configuration store",
- ));
- };
- let profile = tracedecay_project::config::read_or_initialize_profile_configuration(
- database,
- identity.profile_id(),
- )
- .await
- .map_err(|error| {
- configuration_authority_unavailable(format!("cannot read upload setting: {error}"))
- })?;
- match profile.value(tracedecay_domain::configuration::USER_UPLOAD_ENABLED_SETTING_KEY) {
- Ok(tracedecay_domain::configuration::ConfigurationValueV1::Boolean(enabled)) => {
- Ok(*enabled)
- }
- Ok(_) => Err(configuration_authority_unavailable(
- "upload setting is not boolean",
- )),
- Err(error) => Err(configuration_authority_unavailable(error)),
- }
- }
-
/// Estimates the raw-file token cost ("before") for the given file
/// paths from the cached file-token map (indexed file bytes / 4).
/// Pure lookup, persists nothing.
@@ -311,74 +274,6 @@ impl McpServer {
crate::mcp::server::requests::recover_lock(&self.file_token_map).clone()
}
- /// Admits one background worldwide-counter flush at each 30-second boundary.
- ///
- /// The complete flush runs on the observed ledger-write path so responses
- /// never await configuration or cloud I/O and shutdown still drains it.
- #[tracing::instrument(name = "mcp.ledger.flush_worldwide", level = "trace", skip_all)]
- pub(crate) fn maybe_flush_worldwide(self: &Arc) {
- let now = tracedecay_runtime_core::tracedecay::current_timestamp();
- let last = self.last_flush_at.load(Ordering::Relaxed);
- if now - last < 30 {
- return;
- }
- if !claim_worldwide_flush(&self.last_flush_at, last, now) {
- return;
- }
-
- let (Some(tokens_saved), Some(last_flushed_tokens)) =
- (&self.tokens_saved, &self.last_flushed_tokens)
- else {
- return;
- };
- let current = tokens_saved.load(Ordering::Relaxed);
- let last_flushed = last_flushed_tokens.load(Ordering::Relaxed);
- if current <= last_flushed {
- return;
- }
- let delta = current - last_flushed;
-
- if !self.ledger_sink_is_mounted() {
- return;
- }
- let Some(profile_root) = self
- .owner_profile()
- .map(|profile| profile.data_dir().to_path_buf())
- else {
- tracing::warn!("worldwide counter flush skipped: server has no owning profile");
- return;
- };
-
- let server = Arc::clone(self);
- self.spawn_observed_ledger_write(async move {
- let upload_enabled = match server.canonical_upload_enabled().await {
- Ok(enabled) => enabled,
- Err(error) => {
- tracing::warn!(
- error = %error,
- "worldwide counter upload skipped because configuration authority is unavailable"
- );
- return;
- }
- };
- let saved = tracedecay_runtime_core::native_runtime::spawn_blocking(move || {
- persist_worldwide_delta(&profile_root, delta, upload_enabled)
- .inspect_err(|error| tracing::warn!(%error, "could not save upload config"))
- .is_ok()
- })
- .await
- .unwrap_or_else(|error| {
- tracing::warn!(%error, "worldwide counter flush task failed");
- false
- });
- if saved
- && let Some(last_flushed_tokens) = server.last_flushed_tokens.as_ref()
- {
- last_flushed_tokens.store(current, Ordering::Release);
- }
- });
- }
-
#[tracing::instrument(name = "mcp.ledger.record_error_analytics", level = "trace", skip_all)]
pub(crate) fn record_mcp_tool_error_analytics(
&self,
@@ -602,163 +497,58 @@ impl McpServer {
}
}
-/// Adds `delta` to the profile's pending worldwide-counter count, uploading it
-/// when enabled, and saves the profile config.
-pub(super) fn persist_worldwide_delta(
- profile_root: &Path,
- delta: u64,
- upload_enabled: bool,
-) -> std::result::Result<(), tracedecay_session_memory::user_config::ConfigSaveError> {
- let mut config = tracedecay_session_memory::user_config::UserConfig::load(profile_root)?;
- config.pending_upload = config.pending_upload.saturating_add(delta);
- if upload_enabled
- && tracedecay_dashboard_api::cloud::flush_pending(config.pending_upload).is_some()
- {
- config.pending_upload = 0;
- config.last_upload_at = tracedecay_runtime_core::tracedecay::current_timestamp();
- }
- config.save(profile_root)
-}
-
-fn claim_worldwide_flush(last_flush_at: &AtomicI64, expected: i64, now: i64) -> bool {
- last_flush_at
- .compare_exchange(expected, now, Ordering::AcqRel, Ordering::Acquire)
- .is_ok()
-}
-
#[cfg(test)]
mod tests {
- use std::sync::atomic::AtomicU64;
+ use serde_json::json;
+ use tracedecay_mcp::transport::JsonRpcRequest;
use super::*;
- #[test]
- fn concurrent_boundary_calls_claim_exactly_one_worldwide_flush() {
- let last_flush_at = Arc::new(AtomicI64::new(10));
- let barrier = Arc::new(std::sync::Barrier::new(16));
- let claimed = Arc::new(AtomicU64::new(0));
- let workers = (0..16)
- .map(|_| {
- let last_flush_at = Arc::clone(&last_flush_at);
- let barrier = Arc::clone(&barrier);
- let claimed = Arc::clone(&claimed);
- std::thread::spawn(move || {
- barrier.wait();
- if claim_worldwide_flush(&last_flush_at, 10, 40) {
- claimed.fetch_add(1, Ordering::AcqRel);
- }
- })
- })
- .collect::>();
- for worker in workers {
- worker.join().expect("flush claimant");
- }
-
- assert_eq!(claimed.load(Ordering::Acquire), 1);
- assert_eq!(last_flush_at.load(Ordering::Acquire), 40);
- }
-
- #[test]
- fn disabled_upload_records_each_delta_once_after_durable_save() {
- let profile = tempfile::tempdir().expect("profile");
- let profile = profile.path();
- let mut config = tracedecay_session_memory::user_config::UserConfig::load(profile).unwrap();
- config.pending_upload = 0;
- config
- .save(profile)
- .expect("initialize isolated user config");
- let current = 37_u64;
- let last_flushed = AtomicU64::new(0);
-
- for _ in 0..2 {
- let previous = last_flushed.load(Ordering::Acquire);
- if current > previous
- && persist_worldwide_delta(profile, current - previous, false).is_ok()
- {
- last_flushed.store(current, Ordering::Release);
- }
- }
-
- let persisted = tracedecay_session_memory::user_config::UserConfig::load(profile).unwrap();
- assert_eq!(persisted.pending_upload, current);
- assert_eq!(last_flushed.load(Ordering::Acquire), current);
- }
-
+ /// A completed tool response persists its tokens-saved accounting locally
+ /// and leaves the profile `config.toml` alone. The retired worldwide
+ /// counter used to write its pending-upload amount there after a response.
#[tokio::test]
- async fn concurrent_response_boundary_admits_one_observed_background_flush() {
+ async fn completed_response_persists_local_accounting_without_counter_state() {
let (cg, _project, authority) =
super::super::writer_test_support::init_indexed_repo().await;
+ let config_path =
+ tracedecay_session_memory::user_config::config_path(authority.profile_root());
+ let config_before = std::fs::read(&config_path).ok();
+ let project_root = cg.project_root().to_path_buf();
let context = super::super::writer_test_support::registered_context(cg, &authority);
let server = super::super::McpServer::new_with_context(context).await;
let tokens_saved = server.tokens_saved.as_ref().expect("fixture token counter");
- let last_flushed = server
- .last_flushed_tokens
- .as_ref()
- .expect("fixture flushed counter");
- let baseline = last_flushed.load(Ordering::Acquire);
+ let baseline = tokens_saved.load(Ordering::Acquire);
tokens_saved.store(baseline + 41, Ordering::Release);
- server.last_flush_at.store(0, Ordering::Release);
- let started_before = server.ledger_writes_started.load(Ordering::SeqCst);
- let barrier = Arc::new(tokio::sync::Barrier::new(16));
- let callers = (0..16)
- .map(|_| {
- let server = Arc::clone(&server);
- let barrier = Arc::clone(&barrier);
- tokio::spawn(async move {
- barrier.wait().await;
- server.maybe_flush_worldwide();
- })
- })
- .collect::>();
- for caller in callers {
- caller.await.expect("boundary caller");
- }
+ let request = JsonRpcRequest {
+ jsonrpc: "2.0".to_string(),
+ id: Some(json!(1)),
+ method: "tools/call".to_string(),
+ params: Some(json!({
+ "name": "tracedecay_search",
+ "arguments": { "query": "main" }
+ })),
+ };
+ server
+ .handle_request(&request)
+ .await
+ .expect("tool call should produce a response");
+ server.ledger_writes_settled().await;
assert_eq!(
- server.ledger_writes_started.load(Ordering::SeqCst),
- started_before + 1,
- "concurrent completed responses must admit exactly one flush owner"
+ std::fs::read(&config_path).ok(),
+ config_before,
+ "a response must not write counter state into the profile config"
);
+
+ let accounted = tokens_saved.load(Ordering::Acquire);
+ server.spawn_token_accounting_persist(&project_root, "tracedecay_search", 41, 100);
server.ledger_writes_settled().await;
assert_eq!(
- last_flushed.load(Ordering::Acquire),
- baseline + 41,
- "shutdown-observed settlement must advance durable token accounting"
+ server.cg_snapshot().await.get_tokens_saved().await.unwrap(),
+ accounted + 41,
+ "local tokens-saved accounting must stay persisted"
);
server.shutdown_background_tasks().await;
}
-
- /// The observed ledger write runs where no Tokio runtime exists and still
- /// advances durable accounting.
- #[test]
- fn worldwide_flush_settles_without_a_tokio_runtime() {
- let hazards = crate::test_support::native_runtime_hazards();
- tracedecay_runtime_core::native_runtime::block_on(async {
- assert!(tokio::runtime::Handle::try_current().is_err());
- let (cg, _project, authority) =
- super::super::writer_test_support::init_indexed_repo().await;
- let context = super::super::writer_test_support::registered_context(cg, &authority);
- let server = super::super::McpServer::new_with_context(context).await;
- let tokens_saved = server.tokens_saved.as_ref().expect("fixture token counter");
- let last_flushed = server
- .last_flushed_tokens
- .as_ref()
- .expect("fixture flushed counter");
- let baseline = last_flushed.load(Ordering::Acquire);
- tokens_saved.store(baseline + 7, Ordering::Release);
- server.last_flush_at.store(0, Ordering::Release);
-
- server.maybe_flush_worldwide();
-
- assert!(
- server
- .ledger_writes_settled_within(Duration::from_secs(30))
- .await
- );
- assert_eq!(last_flushed.load(Ordering::Acquire), baseline + 7);
- server.shutdown_background_tasks().await;
- })
- .expect("native runtime");
- assert_eq!(crate::test_support::native_runtime_hazards(), hazards);
- }
}
diff --git a/crates/tracedecay/src/mcp/server/requests.rs b/crates/tracedecay/src/mcp/server/requests.rs
index d7b090349c..0dfcc44f9c 100644
--- a/crates/tracedecay/src/mcp/server/requests.rs
+++ b/crates/tracedecay/src/mcp/server/requests.rs
@@ -996,7 +996,6 @@ impl McpServer {
accounting.net_saved_tokens(),
accounting.raw_file_tokens,
);
- self.maybe_flush_worldwide();
accounting
}
diff --git a/crates/tracedecay/tests/dashboard_api_test/settings.rs b/crates/tracedecay/tests/dashboard_api_test/settings.rs
index e7957a8596..8b4fa7527d 100644
--- a/crates/tracedecay/tests/dashboard_api_test/settings.rs
+++ b/crates/tracedecay/tests/dashboard_api_test/settings.rs
@@ -86,7 +86,10 @@ fn settings_dashboard_api_aggregates_and_updates_config() {
"settings must expose the profile worker snapshot: {settings}"
);
- assert_eq!(settings["user"]["upload_enabled"], false);
+ assert!(
+ settings["user"].get("upload_enabled").is_none(),
+ "the retired worldwide-counter upload setting must not be served: {settings}"
+ );
assert_eq!(
settings["user"]["code_index_workers"],
json!({ "mode": "automatic" }),
@@ -334,7 +337,7 @@ fn settings_dashboard_api_aggregates_and_updates_config() {
&json!({
"expected_revision_id": user_revision,
"idempotency_key": "configuration.idempotency.dashboard-user-unmounted",
- "upload_enabled": false,
+ "extraction_timeout_secs": 90,
"watcher_debounce": "15s"
}),
);
@@ -353,13 +356,31 @@ fn settings_dashboard_api_aggregates_and_updates_config() {
&user_url,
&json!({
"expected_revision_id": user_revision,
- "upload_enabled": false
+ "extraction_timeout_secs": 90
}),
);
assert_eq!(
status, 400,
"missing user idempotency must be rejected: {absent_user_idempotency}"
);
+
+ let (status, retired_upload) = patch_json_body(
+ &agent,
+ &user_url,
+ &json!({
+ "expected_revision_id": user_revision,
+ "idempotency_key": "configuration.idempotency.dashboard-user-retired-upload",
+ "upload_enabled": true
+ }),
+ );
+ assert_eq!(
+ status, 400,
+ "the retired upload field must be refused, not silently accepted: {retired_upload}"
+ );
+ assert_eq!(
+ retired_upload["validation_errors"][0]["field"],
+ "upload_enabled"
+ );
assert_eq!(
absent_user_idempotency["validation_errors"][0]["field"],
"idempotency_key"
@@ -437,7 +458,7 @@ fn settings_dashboard_api_aggregates_and_updates_config() {
assert_eq!(reloaded["project"]["config"]["max_file_size"], 1_048_576);
assert_eq!(reloaded["project"]["configuration_revision_id"], revision);
assert_eq!(reloaded["user"]["configuration_revision_id"], user_revision);
- assert_eq!(reloaded["user"]["upload_enabled"], false);
+ assert!(reloaded["user"].get("upload_enabled").is_none());
assert_eq!(reloaded["user"]["watcher_debounce"], "2s");
});
}
diff --git a/crates/tracedecay/tests/mcp_suite/mcp_cli_serve_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_cli_serve_test.rs
index a1a682ce91..9a2f7b770c 100644
--- a/crates/tracedecay/tests/mcp_suite/mcp_cli_serve_test.rs
+++ b/crates/tracedecay/tests/mcp_suite/mcp_cli_serve_test.rs
@@ -972,13 +972,6 @@ async fn projectless_session_serves_user_settings_and_refuses_project_keys() {
let home = TempDir::new().unwrap();
let home = canonical_existing_path(home.path());
let _daemon = common::spawn_tracedecay_daemon(&home);
- let enable = tracedecay_command_with_home(&home)
- .current_dir(&home)
- .arg("enable-upload-counter")
- .stdin(Stdio::null())
- .output()
- .expect("enable-upload-counter should run");
- assert!(enable.status.success(), "{enable:?}");
// The home directory is the ambient root, so the handshake names no project.
let output = run_serve_requests(
@@ -987,7 +980,7 @@ async fn projectless_session_serves_user_settings_and_refuses_project_keys() {
None,
&[
json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} }),
- configuration_get_request(2, "user.upload_enabled.v1"),
+ configuration_get_request(2, "user.repeated_query_hints.v1"),
json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/list" }),
configuration_get_request(4, "index.max_file_size.v1"),
],
@@ -1001,8 +994,8 @@ async fn projectless_session_serves_user_settings_and_refuses_project_keys() {
&setting["outcome"]["value"]["payload"]["effective_value"],
),
(
- &json!("user.upload_enabled.v1"),
- &json!({ "kind": "boolean", "value": true }),
+ &json!("user.repeated_query_hints.v1"),
+ &json!({ "kind": "boolean", "value": false }),
),
"{setting}"
);
@@ -1053,13 +1046,6 @@ async fn unenrolled_directory_session_serves_user_settings() {
let home = canonical_existing_path(home.path());
let unenrolled = TempDir::new().unwrap();
let _daemon = common::spawn_tracedecay_daemon(&home);
- let enable = tracedecay_command_with_home(&home)
- .current_dir(unenrolled.path())
- .arg("enable-upload-counter")
- .stdin(Stdio::null())
- .output()
- .expect("enable-upload-counter should run");
- assert!(enable.status.success(), "{enable:?}");
let output = run_serve_requests(
&home,
@@ -1067,7 +1053,7 @@ async fn unenrolled_directory_session_serves_user_settings() {
None,
&[
json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} }),
- configuration_get_request(2, "user.upload_enabled.v1"),
+ configuration_get_request(2, "user.repeated_query_hints.v1"),
configuration_get_request(3, "index.max_file_size.v1"),
],
);
@@ -1075,7 +1061,7 @@ async fn unenrolled_directory_session_serves_user_settings() {
let setting = json_rpc_tool_payload(&output.stdout, 2);
assert_eq!(
setting["outcome"]["value"]["payload"]["effective_value"],
- json!({ "kind": "boolean", "value": true }),
+ json!({ "kind": "boolean", "value": false }),
"{setting}"
);
let refusal = json_rpc_response(&output.stdout, 3);
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 1e4dbbfce7..5c8930b28c 100644
--- a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs
+++ b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs
@@ -1155,13 +1155,13 @@ async fn dashboard_user_settings_replay_through_application_restart() {
let request = serde_json::json!({
"expected_revision_id": initial_revision.clone(),
"idempotency_key": idempotency_key,
- "upload_enabled": true,
+ "extraction_timeout_secs": 90,
"watcher_debounce": "15s"
});
let (status, applied) = patch_dashboard_json(&agent, &user_url, &request);
assert_eq!(status, 200, "profile mutation failed: {applied}");
assert_eq!(applied["payload"]["restart_recommended"], true);
- assert_eq!(applied["payload"]["user"]["upload_enabled"], true);
+ assert_eq!(applied["payload"]["user"]["extraction_timeout_secs"], 90);
assert_eq!(applied["payload"]["user"]["watcher_debounce"], "15s");
let applied_revision = applied["payload"]["user"]["configuration_revision_id"]
.as_str()
@@ -1214,7 +1214,7 @@ async fn dashboard_user_settings_replay_through_application_restart() {
let conflicting_request = serde_json::json!({
"expected_revision_id": initial_revision.clone(),
"idempotency_key": idempotency_key,
- "upload_enabled": false,
+ "extraction_timeout_secs": 30,
"watcher_debounce": "15s"
});
let (status, conflict) =
diff --git a/dashboard/codegen/schemas/dashboard-contracts.schema.json b/dashboard/codegen/schemas/dashboard-contracts.schema.json
index e65724f847..ee47a131a9 100644
--- a/dashboard/codegen/schemas/dashboard-contracts.schema.json
+++ b/dashboard/codegen/schemas/dashboard-contracts.schema.json
@@ -26581,12 +26581,6 @@
"idempotency_key": {
"type": "string"
},
- "upload_enabled": {
- "type": [
- "boolean",
- "null"
- ]
- },
"watcher_debounce": {
"type": [
"string",
@@ -26640,9 +26634,6 @@
},
"type": "array"
},
- "upload_enabled": {
- "type": "boolean"
- },
"watcher_debounce": {
"type": "string"
}
@@ -26652,7 +26643,6 @@
"configuration_revision_id",
"code_index_worker_configuration_snapshot_id",
"code_index_worker_configuration_revision_id",
- "upload_enabled",
"code_index_workers",
"code_index_worker_status",
"watcher_debounce",
diff --git a/dashboard/src/contracts/decoders.ts b/dashboard/src/contracts/decoders.ts
index 67b2c136d4..38737d6a67 100644
--- a/dashboard/src/contracts/decoders.ts
+++ b/dashboard/src/contracts/decoders.ts
@@ -9455,13 +9455,11 @@ export const UserSettingsPatchSchema: z.ZodObject<{
expected_revision_id: z.ZodType;
extraction_timeout_secs: z.ZodOptional>;
idempotency_key: z.ZodType;
- upload_enabled: z.ZodOptional>;
watcher_debounce: z.ZodOptional>;
}, z.core.$strict> = z.object({
expected_revision_id: z.string(),
extraction_timeout_secs: z.number().int().min(0).nullable().optional(),
idempotency_key: z.string(),
- upload_enabled: z.boolean().nullable().optional(),
watcher_debounce: z.string().nullable().optional(),
}).strict();
@@ -9474,7 +9472,6 @@ export const UserSettingsPayloadV1Schema: z.ZodObject<{
configuration_snapshot_id: z.ZodType;
extraction_timeout_secs: z.ZodType;
installed_agents: z.ZodType, unknown>;
- upload_enabled: z.ZodType;
watcher_debounce: z.ZodType;
}> = z.object({
code_index_worker_configuration_revision_id: z.string(),
@@ -9485,7 +9482,6 @@ export const UserSettingsPayloadV1Schema: z.ZodObject<{
configuration_snapshot_id: z.string(),
extraction_timeout_secs: z.number().int().min(0),
installed_agents: z.array(z.string()),
- upload_enabled: z.boolean(),
watcher_debounce: z.string(),
});
diff --git a/dashboard/src/contracts/types.ts b/dashboard/src/contracts/types.ts
index 7eceeb6733..b9e3fcaa73 100644
--- a/dashboard/src/contracts/types.ts
+++ b/dashboard/src/contracts/types.ts
@@ -5423,7 +5423,6 @@ export type UserSettingsPatch = {
expected_revision_id: string;
extraction_timeout_secs?: number | null | undefined;
idempotency_key: string;
- upload_enabled?: boolean | null | undefined;
watcher_debounce?: string | null | undefined;
};
@@ -5436,7 +5435,6 @@ export type UserSettingsPayloadV1 = {
configuration_snapshot_id: string;
extraction_timeout_secs: number;
installed_agents: Array;
- upload_enabled: boolean;
watcher_debounce: string;
};
diff --git a/dashboard/src/workspaces/observatory/AnalyticsControls.dom.test.tsx b/dashboard/src/workspaces/observatory/AnalyticsControls.dom.test.tsx
index 1c19e01ba7..a2fc69ec56 100644
--- a/dashboard/src/workspaces/observatory/AnalyticsControls.dom.test.tsx
+++ b/dashboard/src/workspaces/observatory/AnalyticsControls.dom.test.tsx
@@ -11,12 +11,11 @@ import type { ObservatoryAccountingReads } from './accountingReads.ts';
/**
* The Plan 26 `analytics-controls` view.
*
- * Two real reads back parts of it, `/api/settings` for the profile upload
- * setting and `/api/doctor/findings?family=storage` for the typed retention-backlog status,
- * and they are read independently. The assertions pin the two reassuring
- * falsehoods this surface could most easily tell: that an unpublished
- * collection mode is `Off`, and that an absent exporter means zero egress
- * failures.
+ * `/api/doctor/findings?family=storage` backs the typed retention-backlog
+ * status and is read independently of the observatory projection. The
+ * assertions pin the two reassuring falsehoods this surface could most easily
+ * tell: that an unpublished collection mode is `Off`, and that an absent
+ * exporter means zero egress failures.
*/
const NOW_MICROS = 1_753_003_600_000_000;
@@ -26,30 +25,29 @@ afterEach(() => {
});
describe('Observatory analytics controls', () => {
-
- it('keeps a failed settings read from blanking the retention evidence', async () => {
- // Independent sources: one refusing must not take the other down with it.
- renderControls({ settingsStatus: 503 });
+ it('states retention evidence and no longer reads or shows an upload setting', async () => {
+ const fetchMock = renderControls();
await screen.findByText('retention sweep observed 4 entries');
- const upload = screen.getByRole('region', { name: 'profile upload setting' });
- expect(upload.textContent).toContain('the profile settings read did not resolve');
- expect(upload.textContent).not.toContain('disabled');
+ expect(screen.queryByRole('region', { name: 'profile upload setting' })).toBeNull();
+ expect(
+ fetchMock.mock.calls.some(([input]) => String(input).includes('/api/settings')),
+ ).toBe(false);
});
- it('keeps a failed findings read from blanking the profile setting', async () => {
+ it('keeps a failed findings read from blanking the collection mode', async () => {
renderControls({ findingsStatus: 503 });
- await screen.findByText('user.upload_enabled.v1');
- const retention = screen.getByRole('region', { name: 'retention and deletion' });
- expect(retention.textContent).toContain('could not be read');
+ const retention = await screen.findByRole('region', { name: 'retention and deletion' });
+ await screen.findByText('could not be read', { exact: false });
expect(retention.textContent).not.toContain('retention sweep observed');
+ const mode = screen.getByRole('region', { name: 'collection mode' });
+ expect(mode.textContent).not.toContain('could not be read');
});
});
function renderControls(
options: {
- settingsStatus?: number;
findingsStatus?: number;
retentionStatuses?: unknown[];
} = {},
@@ -62,31 +60,24 @@ function renderControls(
reason: 'retention sweep observed 4 entries',
},
];
- vi.stubGlobal(
- 'fetch',
- vi.fn(async (input: RequestInfo | URL) => {
- const url = String(input);
- if (url.includes('/api/settings')) {
- if (options.settingsStatus != null) {
- return new Response('{}', { status: options.settingsStatus });
- }
- return new Response(JSON.stringify(envelope(settingsPayload())), { status: 200 });
- }
- if (url.includes('/api/doctor/findings?family=storage')) {
- if (options.findingsStatus != null) {
- return new Response('{}', { status: options.findingsStatus });
- }
- return new Response(JSON.stringify(envelope(findingsPayload(statuses))), { status: 200 });
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url.includes('/api/doctor/findings?family=storage')) {
+ if (options.findingsStatus != null) {
+ return new Response('{}', { status: options.findingsStatus });
}
- return new Response('{}', { status: 404 });
- }),
- );
+ return new Response(JSON.stringify(envelope(findingsPayload(statuses))), { status: 200 });
+ }
+ return new Response('{}', { status: 404 });
+ });
+ vi.stubGlobal('fetch', fetchMock);
const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
render(
,
);
+ return fetchMock;
}
function accountingReads(): ObservatoryAccountingReads {
@@ -183,74 +174,6 @@ function findingsPayload(kindStatuses: unknown[]) {
};
}
-function settingsPayload() {
- return {
- automation: {
- availability: { available: true, reason: null, required_authority: null },
- backend: null,
- config_endpoint: '/api/plugins/holographic/curation/config',
- enabled: false,
- host_mode: null,
- source_coverage: { effective: 'global', global: 'default', project: 'absent' },
- },
- environment: {
- global_accounting_enabled: false,
- global_accounting_mode: 'off',
- pricing_offline: true,
- variables: [],
- },
- project: {
- config: {
- exclude: [],
- extract_docstrings: true,
- context_scout: false,
- include: [],
- max_file_size: 1_048_576,
- sync: { auto_track_pr_branches: false, auto_track_pr_poll_secs: 300 },
- telemetry: { timings: false },
- track_call_sites: true,
- },
- configuration_revision_id: 'revision-1',
- configuration_snapshot_id: 'snapshot-1',
- pr_autotrack: { tracked: [] },
- },
- restart_recommended: null,
- resync_recommended: null,
- storage: {
- dashboard_root: '/store/dashboard',
- graph_db: '/store/graph.db',
- lcm_db: '/store/lcm.db',
- lcm_scope: 'project',
- memory_db: '/store/memory.db',
- project_id: 'tracedecay',
- project_root: '/repo',
- savings_db: '/store/savings.db',
- storage_mode: 'profile_sharded',
- store_root: '/store',
- },
- user: {
- code_index_worker_status: {
- available_logical_cpus: 4,
- configured: { mode: 'automatic' },
- effective_workers: 4,
- environment_override_workers: null,
- limiting_reason: 'automatic_all_cores',
- memory_safe_workers: 6,
- },
- code_index_workers: { mode: 'automatic' },
- code_index_worker_configuration_revision_id: 'worker-revision-1',
- code_index_worker_configuration_snapshot_id: 'worker-snapshot-1',
- configuration_revision_id: 'revision-1',
- configuration_snapshot_id: 'snapshot-1',
- extraction_timeout_secs: 30,
- installed_agents: ['claude'],
- upload_enabled: false,
- watcher_debounce: '500ms',
- },
- version: { cached_latest_version: null, channel: 'stable', version: '0.0.0' },
- };
-}
-
function envelope(payload: unknown) {
return {
schema_revision: 1,
diff --git a/dashboard/src/workspaces/observatory/AnalyticsControls.tsx b/dashboard/src/workspaces/observatory/AnalyticsControls.tsx
index 0859ad798e..1324084eb2 100644
--- a/dashboard/src/workspaces/observatory/AnalyticsControls.tsx
+++ b/dashboard/src/workspaces/observatory/AnalyticsControls.tsx
@@ -3,19 +3,15 @@
* analytics: local mode, share staging age, retention/deletion, and egress
* failures.
*
- * Two real reads back parts of this surface, `GET /api/settings` for the
- * `user.upload_enabled.v1` profile setting, and `GET /api/doctor/findings?family=storage` for
- * the typed `retention_backlog` status. They are read independently: a failed
- * settings read must not blank the retention evidence, and neither may stand in
- * for the analytics collection mode, which nothing publishes.
+ * `GET /api/doctor/findings?family=storage` backs the typed `retention_backlog`
+ * status. It is read independently of the observatory projection, and neither
+ * may stand in for the analytics collection mode, which nothing publishes.
*
* The rule this surface exists to hold is the one in `analyticsControls.ts`: an
* unread mode is not `Off`, and an absent exporter is not zero egress failures.
* Both would be reassuring, and both would be false.
*/
import type { ComponentProps, ReactNode } from 'react';
-import { SettingsPayloadV1Schema } from '../../contracts/generated.ts';
-import { useEnvelope } from '../../data/query/useEnvelope.ts';
import { useStorageFindings } from '../../data/query/storageFindings.ts';
import { envelopeReadState } from '../../ui/ReadSection.tsx';
import { StateChip } from '../../ui/StateChip';
@@ -26,20 +22,13 @@ import {
egressFailureReading,
retentionBacklogReading,
shareStagingReading,
- uploadSettingReading,
type RetentionBacklogReading,
- type UploadSettingReading,
} from './analyticsControls.ts';
import type { ObservatoryAccountingReads } from './accountingReads.ts';
export function AnalyticsControls({ reads }: { reads: ObservatoryAccountingReads }) {
- const settings = useEnvelope(['settings', 'analytics-controls'], '/api/settings', SettingsPayloadV1Schema);
const findings = useStorageFindings();
- const settingsState = envelopeReadState(settings.isPending, settings.data, {
- loading: 'requesting profile settings',
- transport: 'profile settings could not be read',
- });
const findingsState = envelopeReadState(findings.isPending, findings.data, {
loading: 'requesting doctor storage findings',
transport: 'doctor storage findings could not be read',
@@ -175,26 +164,6 @@ export function AnalyticsControls({ reads }: { reads: ObservatoryAccountingReads
: `${egress.failures.toLocaleString()} failures were recorded by the canonical projection.`}
-
-
- {settingsState.kind === 'ready' ? (
-
- ) : (
-
- the profile settings read did not resolve, so the setting is not stated
-
- )}
-
);
@@ -258,26 +227,3 @@ function RetentionBacklog({ reading }: { reading: RetentionBacklogReading }) {
);
}
-
-function UploadSetting({ reading }: { reading: UploadSettingReading }) {
- return (
- <>
-
-
-
setting
- {reading.settingKey}
-
-
-
value
-
- {reading.enabled == null ? 'not reported' : reading.enabled ? 'enabled' : 'disabled'}
-
-
-
- {reading.disclaimer}
- >
- );
-}
diff --git a/dashboard/src/workspaces/observatory/analyticsControls.test.ts b/dashboard/src/workspaces/observatory/analyticsControls.test.ts
index c90a60e70d..db09173a6b 100644
--- a/dashboard/src/workspaces/observatory/analyticsControls.test.ts
+++ b/dashboard/src/workspaces/observatory/analyticsControls.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { AnalyticsModeReadModelV1, MetricValueV1, StorageFindingKindStatusV1 } from '../../contracts/generated.ts';
-import { analyticsModeReading, egressFailureReading, retentionBacklogReading, shareStagingReading, uploadSettingReading } from './analyticsControls.ts';
+import { analyticsModeReading, egressFailureReading, retentionBacklogReading, shareStagingReading } from './analyticsControls.ts';
describe('analyticsModeReading', () => {
it('reports an unavailable mode as unknown and explicitly not Off', () => {
@@ -78,16 +78,6 @@ describe('retentionBacklogReading', () => {
});
});
-describe('uploadSettingReading', () => {
-
- it('reports an unread setting as unknown rather than as disabled', () => {
- const reading = uploadSettingReading(undefined);
- expect(reading.enabled).toBeNull();
- expect(reading.enabled).not.toBe(false);
- expect(reading.state).toBe('unknown');
- });
-});
-
function status(
kind: StorageFindingKindStatusV1['kind'],
state: StorageFindingKindStatusV1['state'],
diff --git a/dashboard/src/workspaces/observatory/analyticsControls.ts b/dashboard/src/workspaces/observatory/analyticsControls.ts
index 79f30d6c29..f2df3bc539 100644
--- a/dashboard/src/workspaces/observatory/analyticsControls.ts
+++ b/dashboard/src/workspaces/observatory/analyticsControls.ts
@@ -1,9 +1,8 @@
-/** Plan 26 analytics controls over canonical settings and Observatory reads. */
+/** Plan 26 analytics controls over Observatory reads. */
import type {
AnalyticsModeV1,
AnalyticsModeReadModelV1,
MetricValueV1,
- SettingsPayloadV1,
StorageFindingKindStatusV1,
} from '../../contracts/generated.ts';
import type { DomainStateKind } from '../../ui/StateChip.tsx';
@@ -230,28 +229,3 @@ export function shareStagingReading(metrics: readonly MetricValueV1[]): ShareSta
}
return { ageSeconds: metric.value, state: 'ready', reason: '' };
}
-
-export interface UploadSettingReading {
- /** The profile setting value, or `null` when the settings read carried none. */
- enabled: boolean | null;
- settingKey: string;
- state: DomainStateKind;
- /** The one sentence that keeps this from being mistaken for the analytics
- * collection mode. */
- disclaimer: string;
-}
-
-/** The `user.upload_enabled.v1` profile setting from `/api/settings`. Real,
- * wire-published, and explicitly not `AnalyticsModeV1`. */
-export function uploadSettingReading(
- settings: SettingsPayloadV1 | undefined,
-): UploadSettingReading {
- const enabled = settings?.user.upload_enabled ?? null;
- return {
- enabled,
- settingKey: 'user.upload_enabled.v1',
- state: enabled == null ? 'unknown' : 'ready',
- disclaimer:
- 'This is the profile upload setting, not the Plan 26 analytics collection mode. It does not select Off, LocalOnly, or AggregateShare and does not govern adoption analytics.',
- };
-}
diff --git a/dashboard/src/workspaces/observatory/evidence.ts b/dashboard/src/workspaces/observatory/evidence.ts
index 0239ee618f..c77adf2779 100644
--- a/dashboard/src/workspaces/observatory/evidence.ts
+++ b/dashboard/src/workspaces/observatory/evidence.ts
@@ -209,7 +209,7 @@ export const SOURCE_IDENTITY: Record = {
id: 'analytics',
title: 'Analytics controls',
route: '/api/observatory',
- authority: 'canonical analytics mode, retention lifecycle, egress and upload settings',
+ authority: 'canonical analytics mode, retention lifecycle and egress',
},
telemetry: {
id: 'telemetry',
diff --git a/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx b/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx
index 370012bae5..084c53beb7 100644
--- a/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx
+++ b/dashboard/src/workspaces/settings/SettingsPage.dom.test.tsx
@@ -51,7 +51,6 @@ const FIXTURE_SETTINGS: ReadonlyArray = [
'user.configuration_snapshot_id',
'user.extraction_timeout_secs',
'user.installed_agents',
- 'user.upload_enabled',
'user.watcher_debounce',
],
],
diff --git a/dashboard/src/workspaces/settings/settingsEditorMachine.ts b/dashboard/src/workspaces/settings/settingsEditorMachine.ts
index c893afde88..f48313af5b 100644
--- a/dashboard/src/workspaces/settings/settingsEditorMachine.ts
+++ b/dashboard/src/workspaces/settings/settingsEditorMachine.ts
@@ -728,7 +728,6 @@ function sameProjectValues(
function sameUserValues(left: UserSettingsValues, right: UserSettingsValues): boolean {
return (
- left.upload_enabled === right.upload_enabled &&
left.watcher_debounce === right.watcher_debounce &&
left.extraction_timeout_secs === right.extraction_timeout_secs
);
diff --git a/dashboard/src/workspaces/settings/settingsModel.ts b/dashboard/src/workspaces/settings/settingsModel.ts
index 19cc9f3337..752e2e47dc 100644
--- a/dashboard/src/workspaces/settings/settingsModel.ts
+++ b/dashboard/src/workspaces/settings/settingsModel.ts
@@ -12,7 +12,7 @@
* The daemon really does merge two layers there, and then ships only the
* result. The winning layer is not on the wire.
* - `user` and `project.config` do not even address the same keys
- * (`upload_enabled`/`watcher_debounce` vs `include`/`exclude`/…), so they
+ * (`watcher_debounce`/`extraction_timeout_secs` vs `include`/`exclude`/…), so they
* are not competing layers over a shared namespace.
*
* Therefore this model does NOT rank groups into a resolution stack and does
@@ -150,7 +150,6 @@ export interface ProjectSettingsValues {
}
export interface UserSettingsValues {
- readonly upload_enabled: boolean;
readonly watcher_debounce: string;
readonly extraction_timeout_secs: string;
}
@@ -205,7 +204,6 @@ export interface ProjectSettingsChangeSet {
/** The user-scope counterpart of `ProjectSettingsChangeSet`. */
export interface UserSettingsChangeSet {
- upload_enabled?: boolean;
watcher_debounce?: string;
extraction_timeout_secs?: number;
}
@@ -443,7 +441,6 @@ export function buildSettingsEditor(payload: SettingsPayloadV1): SettingsEditor
auto_track_pr_poll_secs: pollSecs,
},
user: {
- upload_enabled: payload.user.upload_enabled,
watcher_debounce: payload.user.watcher_debounce,
extraction_timeout_secs: extractionTimeout,
},
@@ -551,9 +548,6 @@ export function planUserChangeAgainst(
if (errors.length > 0) return { outcome: 'invalid', errors };
const patch: UserSettingsChangeSet = {};
- if (values.upload_enabled !== current.user.upload_enabled) {
- patch.upload_enabled = values.upload_enabled;
- }
if (values.watcher_debounce !== current.user.watcher_debounce) {
patch.watcher_debounce = values.watcher_debounce;
}
diff --git a/dashboard/src/workspaces/settings/settingsRows.test.ts b/dashboard/src/workspaces/settings/settingsRows.test.ts
index 3288aa2625..9a3b80e00c 100644
--- a/dashboard/src/workspaces/settings/settingsRows.test.ts
+++ b/dashboard/src/workspaces/settings/settingsRows.test.ts
@@ -162,7 +162,7 @@ describe('write bindings', () => {
expect(applyRequirement(bindingFor('project.config.track_call_sites')!)).toEqual({
kind: 'reported_on_apply',
});
- expect(applyRequirement(bindingFor('user.upload_enabled')!)).toEqual({
+ expect(applyRequirement(bindingFor('user.watcher_debounce')!)).toEqual({
kind: 'reported_on_apply',
});
});
diff --git a/dashboard/src/workspaces/settings/settingsRows.ts b/dashboard/src/workspaces/settings/settingsRows.ts
index c96383879c..44d9336aaf 100644
--- a/dashboard/src/workspaces/settings/settingsRows.ts
+++ b/dashboard/src/workspaces/settings/settingsRows.ts
@@ -114,7 +114,6 @@ const BINDINGS: Readonly> = {
field: 'auto_track_pr_poll_secs',
input: 'integer',
},
- 'user.upload_enabled': { scope: 'user', field: 'upload_enabled', input: 'boolean' },
'user.watcher_debounce': { scope: 'user', field: 'watcher_debounce', input: 'duration' },
'user.extraction_timeout_secs': {
scope: 'user',
@@ -360,8 +359,6 @@ function withUserField(
value: unknown,
): UserSettingsValues {
switch (field) {
- case 'upload_enabled':
- return typeof value === 'boolean' ? { ...values, [field]: value } : values;
case 'watcher_debounce':
case 'extraction_timeout_secs':
return typeof value === 'string' ? { ...values, [field]: value } : values;
diff --git a/dashboard/stories/fixtures/data.ts b/dashboard/stories/fixtures/data.ts
index 39c4289291..5801b79b90 100644
--- a/dashboard/stories/fixtures/data.ts
+++ b/dashboard/stories/fixtures/data.ts
@@ -3241,7 +3241,6 @@ const settingsPayload: Record = {
configuration_revision_id: 'user-rev-7',
code_index_worker_configuration_snapshot_id: 'profile-worker-snap-7',
code_index_worker_configuration_revision_id: 'profile-worker-rev-7',
- upload_enabled: false,
code_index_workers: { mode: 'automatic' },
code_index_worker_status: {
configured: { mode: 'automatic' },
diff --git a/docs/REBRAND-COMPATIBILITY-POLICY.md b/docs/REBRAND-COMPATIBILITY-POLICY.md
index c082d932b7..2c8c8e9e30 100644
--- a/docs/REBRAND-COMPATIBILITY-POLICY.md
+++ b/docs/REBRAND-COMPATIBILITY-POLICY.md
@@ -71,11 +71,12 @@ benchmark reports, and daemon-removal instructions should remain unless it is
factually wrong. These references should be labeled as historical when helpful, not
mechanically rewritten.
-### A5. Worldwide counter endpoint
+### A5. Worldwide counter endpoint (retired)
-The existing counter endpoint name may remain `tracedecay-counter` while the service
-continues to be best-effort and documented. A future endpoint rename is allowed only
-when the replacement worker is deployed and continuity/failure behavior is preserved.
+The worldwide counter was removed from the client (issue #3619); no TraceDecay
+binary calls the `tracedecay-counter` Cloudflare Worker anymore. Shutting the
+hosted Worker and its Upstash Redis store down is a maintainer action outside
+this repository, see the worldwide token counter note in `docs/USER-GUIDE.md`.
## Category B, automatically migrated or cleaned up
@@ -285,7 +286,7 @@ Every audited surface maps to exactly one policy category below.
| Antigravity legacy CLI plugin path | B | Remove generated `tracedecay.json` on uninstall/reconcile. |
| Claude legacy hook/config migration | B | Repair/remove known generated legacy hook and config shapes. |
| `tracedecay-large-treesitters` package/dependency name | E | Keep exact upstream name until upstream rename or explicit maintained-fork policy. |
-| Worldwide counter endpoint `tracedecay-counter` | A | Retain until replacement endpoint is deployed with preserved behavior. |
+| Worldwide counter endpoint `tracedecay-counter` | A | Retired from the client; the hosted Worker is decommissioned by a maintainer. |
| Legacy release asset names for explicit requested versions | B | Probe after current asset names for explicit old-version installs/upgrades. |
| Latest-version filtering of legacy-only releases | D | Reject legacy-only/pre-reset releases as automatic latest candidates. |
| Extracted binary name `tracedecay` in old archives | B | Accept only as fallback while extracting explicit old archives. |
diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md
index a148361d5f..64dc8bf90c 100644
--- a/docs/USER-GUIDE.md
+++ b/docs/USER-GUIDE.md
@@ -825,11 +825,10 @@ It verifies:
exact worktree/ref/commit/generation, freshness, coverage, and typed authority
state
- **Global registry**, daemon-owned project/profile enrollment and availability
-- **User config**, `~/.tracedecay/config.toml` and upload settings
+- **User config**, `~/.tracedecay/config.toml` and the GitHub review sources it names
- **Agent integrations**. MCP server registration, hook installation, tool permissions, prompt rules
-- **Network**, the configured worldwide counter and GitHub releases API; each
- reports its own available or unavailable state, and a failed release lookup
- names its outcome (see [Updating TraceDecay](#updating-tracedecay))
+- **Network**, the GitHub releases API; it reports its own available or
+ unavailable state, and a failed release lookup names its outcome (see [Updating TraceDecay](#updating-tracedecay))
If any tool permissions are missing after an upgrade, Doctor reports the missing
capability and the supported install/update operation. Doctor only reports
@@ -1123,33 +1122,25 @@ Network effects are separate from local indexing and retrieval. They can be
disabled, unavailable, or denied without turning those states into successful
local results. The available effects are described below.
-### Worldwide token counter
+### Worldwide token counter (retired)
-TraceDecay tracks how many tokens it has saved locally. If you opt in, the
-daemon's token-savings status path uploads that aggregate count to the worldwide
-counter. Repository content, file names, and project names are not part of the
-counter payload. The counter service still receives ordinary transport metadata
-such as the source IP and may derive aggregate geography from it; submitting an
-aggregate count is not an anonymity guarantee.
+TraceDecay no longer has a worldwide token counter and makes no counter network
+call. Token savings, costs, and usage stay local: `tracedecay current-counter`,
+`tracedecay reset-counter`, `tracedecay gain`, `tracedecay cost`, and the
+dashboard are unchanged.
-`tracedecay status` does not display the counter. When you opt in, `tracedecay doctor`
-reads the public total only to report whether the counter service is reachable.
+`tracedecay enable-upload-counter` and `tracedecay disable-upload-counter` no
+longer exist and fail as unknown subcommands. The `user.upload_enabled.v1`
+setting and the dashboard's `upload_enabled` field are gone, so a configuration
+read or patch that names them is refused. An older profile that still stores the
+opt-in, a pending upload amount, or flush timestamps loads normally: the next
+write erases them, and the pending amount is never uploaded.
-**To opt in:**
-
-```bash
-tracedecay enable-upload-counter
-```
-
-Disable it again at any time:
-
-```bash
-tracedecay disable-upload-counter
-```
-
-The setting belongs to your profile, not to a project: it applies everywhere
-and can be changed or read (`tracedecay tool configuration_get --args
-'{"key":"user.upload_enabled.v1"}'`) from any directory.
+The hosted counter service was a Cloudflare Worker named `tracedecay-counter`,
+backed by Upstash Redis. Nothing in this repository deploys or operates it, so
+removing the client does not shut it down. Decommissioning it is a separate
+maintainer action: delete the Worker and its Redis store from the Cloudflare
+and Upstash accounts that host them. Its status is tracked on issue #3619.
### Version check
@@ -1370,7 +1361,7 @@ tracedecay memory status --path /path/to/project --json
Created in your home directory. Contains:
-- `config.toml`, user preferences (cached version info, pending upload count)
+- `config.toml`, user preferences (cached version info, installed agents)
- `global.db`, daemon-owned registry/usage metadata for enrolled projects; it is
not a fact authority and clients never open it directly
- `projects//`, daemon-owned project authority data when profile storage is enabled
@@ -1378,13 +1369,11 @@ Created in your home directory. Contains:
The `config.toml` is plain TOML and fully transparent:
```toml
-pending_upload = 4823 # tokens waiting to be uploaded
-last_upload_at = 1711375200 # last successful upload timestamp
+cached_latest_version = "1.2.3" # latest release seen by the version check
+last_version_check_at = 1711375200 # last version check timestamp
+installed_agents = ["claude"] # hosts TraceDecay has integrated
```
-The upload opt-in is the `user.upload_enabled.v1` setting, not a key in this
-file.
-
#### Mandatory structured sanitization of ingested transcripts
Agent transcripts can contain credentials or other sensitive values. TraceDecay
diff --git a/evals/agent_adoption/run.sh b/evals/agent_adoption/run.sh
index c85245e7db..084418e08b 100755
--- a/evals/agent_adoption/run.sh
+++ b/evals/agent_adoption/run.sh
@@ -358,7 +358,6 @@ echo "query hints: $REPEATED_QUERY_HINTS"
if [[ -n "$LEARNED_SKILL_WORKFLOW" ]]; then
echo "learned: $LEARNED_SKILL_WORKFLOW variants=[${ACTIVE_VARIANTS# }]"
fi
-"$TD" disable-upload-counter >/dev/null 2>&1 || true
# ---- build fixtures -------------------------------------------------------- #
gitc() { git -C "$1" -c user.email=eval@tracedecay -c user.name=eval "${@:2}"; }
diff --git a/scripts/mcp-conformance-smoke.sh b/scripts/mcp-conformance-smoke.sh
index 22205c6de5..187bdc7ae4 100755
--- a/scripts/mcp-conformance-smoke.sh
+++ b/scripts/mcp-conformance-smoke.sh
@@ -116,7 +116,6 @@ NODE
echo "error: tracedecay init failed" >&2
return 1
fi
- "$TRACEDECAY_BIN" disable-upload-counter >/dev/null 2>&1 || true
# Resolve and cache the inspector package before any timed call: on a cold
# runner `npx` spends its first invocation downloading the package, and