Skip to main content

mbx_cache_core/
agent.rs

1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3    ActionPrediction, ActionPromiseCompletion, ActionPromiseState, CacheDigest, CacheDirectory,
4    LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES, ManifestPutOutcome, RemoteActionResult,
5    RemoteCacheClient, RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk,
6    canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::{info, warn};
11use std::collections::{BTreeMap, BTreeSet};
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, Mutex, Weak};
16use std::time::{Duration, Instant, SystemTime};
17
18/// Fleet claims are leases rather than correctness locks. This only bounds how
19/// long one shim waits through repeated pending responses before degrading to
20/// an ordinary compilation; the server independently expires abandoned claims.
21const ACTION_PROMISE_WAIT: Duration = Duration::from_secs(60 * 60);
22use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
23
24mod file_digest;
25mod manifest;
26mod prefetch;
27mod stats;
28mod wire;
29
30#[cfg(test)]
31pub(crate) use prefetch::select_prefetch_actions;
32
33pub use file_digest::{
34    FileDigestCache, FileDigestScope, FileIdentity, NoFileDigestCache, RecordedFileDigest,
35};
36pub use manifest::{is_task_identity, task_manifest_actions};
37use manifest::{
38    merge_remote_task_manifest, merge_task_manifests, task_manifest_dir, validate_task_identity,
39    validate_task_manifest,
40};
41pub use stats::{AgentStats, CompilerStats};
42use wire::MAX_REQUEST_BYTES;
43pub use wire::{
44    AGENT_PROTOCOL_VERSION, ActionDiagnostic, AgentEvent, AgentEventObserver, AgentRequest,
45    AgentResponse, RestoreStats,
46};
47
48const MAX_EXECUTABLE_IDENTITIES: usize = 64;
49const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
50const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
51const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
52const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
53/// Longest shim diagnostic the agent accepts, in bytes.
54const MAX_WARNING_BYTES: usize = 4 * 1024;
55/// Most distinct shim diagnostics one session surfaces before going quiet.
56///
57/// A failure mode that repeats across a build tends to repeat with the same
58/// message, which deduplication already collapses; the cap only guards
59/// against a message that embeds something unique per compilation.
60const MAX_WARNINGS: usize = 128;
61const ACTION_DIAGNOSTIC_PREFIX: &str = "@mbx-action-diagnostic\t";
62/// Most file identities one digest lookup or record may carry.
63const MAX_FILE_DIGEST_BATCH: usize = 16 * 1024;
64/// Most recorded file digests one session retains across every scope.
65///
66/// Roughly two orders of magnitude above the largest workspace measured; the
67/// cap exists so a pathological build bounds the agent instead of growing it.
68const MAX_FILE_DIGEST_ENTRIES: usize = 1024 * 1024;
69const MAX_REMOTE_TRANSFERS: usize = 64;
70const MAX_PREFETCH_TRANSFERS: usize = 48;
71/// Most blob packs downloaded at once.
72///
73/// Packs are individually bounded to keep their staging footprint predictable.
74/// Allowing a few independent streams prevents one large Azure object stream
75/// from serializing an entire workspace restore.
76const MAX_CONCURRENT_BLOB_PACKS: usize = 8;
77/// Most predicted actions whose complete output closures are downloaded
78/// speculatively for one task.
79///
80/// Manifests deliberately retain predictions across compatible builds, so a
81/// large workspace can describe far more actions than the next invocation
82/// will request. Keep the most expensive recorded actions warm and let
83/// foreground lookups fetch only unusually large tails on demand. The remote
84/// download-byte budget remains the primary bound on speculative transfer. A
85/// 1,024-action ceiling covers the measured large-workspace manifests while
86/// retaining a guard against pathological manifests and stale prediction sets.
87const MAX_PREFETCH_ACTIONS: usize = 1024;
88// Resolve speculative actions progressively so the most valuable predictions
89// become usable first and cancellation at the end of a build abandons a small
90// tail instead of one workspace-sized transfer wave. Action-result lookup is
91// still batched independently; this only bounds each output-closure download.
92const MAX_PREFETCH_ACTION_WAVE: usize = 32;
93/// Batched action lookups issued at once.
94///
95/// One request already asks about hundreds of actions. Serial batches keep a
96/// fleet of concurrent CI jobs from multiplying metadata pressure while blob
97/// transfers from earlier answers are also underway.
98const MAX_PREFETCH_BATCH_LOOKUPS: usize = 1;
99const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
100const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
101const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
102const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
103
104/// Names the identities a task may inherit predictions from, consulted only
105/// once nothing has been recorded under its own.
106pub type TaskFallbacks = Arc<dyn Fn() -> Vec<String> + Send + Sync>;
107
108/// How many of the store's most recently written manifests a task with
109/// fallbacks tries after the named identities yield nothing.
110///
111/// A runner that restored a cache bundle holds the manifests of the builds
112/// that produced it and no version-control history to name them by, so what
113/// was written last is the best remaining guess at the lockfile before this
114/// one. Each candidate costs one parse, and a manifest from an unrelated
115/// workspace only fails to match. One is skipped when it fills more than half
116/// the prediction limit: an inherited manifest is kept under the new identity,
117/// and a foreign one that large would leave this workspace's own recordings
118/// no room.
119const NEWEST_MANIFEST_CANDIDATES: usize = 8;
120
121/// Remote action-cache access owned by one task session.
122pub struct AgentRemoteCache {
123    /// Remote protocol client used by the agent.
124    pub client: RemoteCacheClient,
125    /// Permitted remote read/write operations.
126    pub mode: RemoteCacheMode,
127    /// Directory used for verified downloads before CAS ingestion.
128    pub staging_dir: PathBuf,
129}
130
131#[derive(Default)]
132struct AtomicAgentStats {
133    lookups: AtomicU64,
134    unconsulted: AtomicU64,
135    hits: AtomicU64,
136    stores: AtomicU64,
137    stored_bytes: AtomicU64,
138    verifications: AtomicU64,
139    divergences: AtomicU64,
140    downloaded_bytes: AtomicU64,
141    uploaded_bytes: AtomicU64,
142    background_uploads: AtomicU64,
143    background_upload_failures: AtomicU64,
144    remote_blob_pack_uploads: AtomicU64,
145    remote_blob_pack_upload_blobs: AtomicU64,
146    upload_drain_duration_ns: AtomicU64,
147    prefetched_actions: AtomicU64,
148    predictions_loaded: AtomicU64,
149    remote_failures: AtomicU64,
150    remote_manifest_lookups: AtomicU64,
151    remote_manifest_lookup_duration_ns: AtomicU64,
152    remote_action_lookups: AtomicU64,
153    remote_action_lookup_duration_ns: AtomicU64,
154    remote_blob_requests: AtomicU64,
155    remote_blob_pack_requests: AtomicU64,
156    remote_blob_pack_blobs: AtomicU64,
157    remote_blob_transfer_duration_ns: AtomicU64,
158    local_cas_write_duration_ns: AtomicU64,
159    prefetch_runs: AtomicU64,
160    prefetch_duration_ns: AtomicU64,
161    materialization_duration_ns: AtomicU64,
162    bypasses: Mutex<BTreeMap<String, u64>>,
163    avoided_compiler_duration_ns: AtomicU64,
164    compiler: Mutex<BTreeMap<String, CompilerStats>>,
165    slow_compilations: Mutex<BTreeMap<String, u64>>,
166    restored_output_files: AtomicU64,
167    restored_output_bytes: AtomicU64,
168    reflinked_output_files: AtomicU64,
169    reflinked_output_bytes: AtomicU64,
170    copied_output_files: AtomicU64,
171    copied_output_bytes: AtomicU64,
172    reused_output_files: AtomicU64,
173    reused_output_bytes: AtomicU64,
174}
175
176struct AtomicDurationTimer<'a> {
177    started: Instant,
178    target: &'a AtomicU64,
179}
180
181impl<'a> AtomicDurationTimer<'a> {
182    fn start(target: &'a AtomicU64) -> Self {
183        Self {
184            started: Instant::now(),
185            target,
186        }
187    }
188}
189
190impl Drop for AtomicDurationTimer<'_> {
191    fn drop(&mut self) {
192        atomic_saturating_add(self.target, duration_ns(self.started));
193    }
194}
195
196fn duration_ns(started: Instant) -> u64 {
197    started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
198}
199
200fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
201    if let Some(crate_name) = crate_name
202        && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
203    {
204        bail!("invalid compiler crate name");
205    }
206    Ok(())
207}
208
209#[derive(serde::Deserialize)]
210struct ActionDiagnosticEnvelope {
211    outcome: String,
212    crate_name: Option<String>,
213    diagnostic: ActionDiagnostic,
214}
215
216fn parse_action_diagnostic(
217    message: &str,
218) -> Option<Result<(String, Option<String>, ActionDiagnostic)>> {
219    let payload = message.strip_prefix(ACTION_DIAGNOSTIC_PREFIX)?;
220    Some((|| {
221        let envelope: ActionDiagnosticEnvelope = serde_json::from_str(payload)?;
222        if !matches!(envelope.outcome.as_str(), "hit" | "miss") {
223            bail!("invalid action diagnostic outcome");
224        }
225        validate_crate_name(envelope.crate_name.as_deref())?;
226        Ok((envelope.outcome, envelope.crate_name, envelope.diagnostic))
227    })())
228}
229
230fn atomic_saturating_add(target: &AtomicU64, value: u64) {
231    let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
232        Some(current.saturating_add(value))
233    });
234}
235
236fn queue_prefetch_digest(
237    verified: &BTreeMap<CacheDigest, PathBuf>,
238    pending: &mut BTreeMap<CacheDigest, ()>,
239    digest: CacheDigest,
240) {
241    if verified.contains_key(&digest) || pending.contains_key(&digest) {
242        return;
243    }
244    pending.insert(digest, ());
245}
246
247fn queue_prefetch_directory(
248    seen: &BTreeMap<CacheDigest, ()>,
249    pending: &mut BTreeMap<CacheDigest, ()>,
250    digest: CacheDigest,
251    limit: usize,
252) -> bool {
253    if seen.contains_key(&digest) || pending.contains_key(&digest) {
254        return true;
255    }
256    if seen.len().saturating_add(pending.len()) >= limit {
257        return false;
258    }
259    pending.insert(digest, ());
260    true
261}
262
263/// See [`CacheAgent::with_task_loader`].
264pub type TaskLoader = dyn for<'a> Fn(&'a CacheAgent, &'a str) -> BoxFuture<'a, ()> + Send + Sync;
265
266/// Shared state for an agent hosted by the process that owns a build session.
267///
268/// Transport listeners deliberately live in the embedder so the session
269/// lifecycle owns them. This type only contains ecosystem-independent CAS and
270/// protocol logic.
271#[derive(Clone)]
272pub struct CacheAgent {
273    cas: LocalCas,
274    actions: LocalActionCache,
275    verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
276    version: Arc<str>,
277    write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
278    action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
279    stats: Arc<AtomicAgentStats>,
280    observer: Option<Arc<dyn AgentEventObserver>>,
281    observer_emission: Arc<Mutex<()>>,
282    /// Loads a task the first time a request names it, so a connection that
283    /// only reports a bypass never waits for a manifest it will not read.
284    task_loader: Option<Arc<TaskLoader>>,
285    executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
286    manifest_dir: Arc<PathBuf>,
287    task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
288    /// Where a task may inherit predictions from, by task identity.
289    task_fallbacks: Arc<Mutex<BTreeMap<String, TaskFallbacks>>>,
290    next_task_run: Arc<AtomicU64>,
291    manifest_write_lock: Arc<Mutex<()>>,
292    remote: Option<Arc<RemoteCacheClient>>,
293    remote_mode: RemoteCacheMode,
294    remote_staging_dir: Arc<PathBuf>,
295    remote_download_limit: u64,
296    remote_download_bytes: Arc<AtomicU64>,
297    pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
298    remote_transfers: Arc<tokio::sync::Semaphore>,
299    prefetch_transfers: Arc<tokio::sync::Semaphore>,
300    prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
301    /// Distinct shim diagnostics already surfaced, so a warning that fires
302    /// once per compilation is printed once per session.
303    warnings: Arc<Mutex<BTreeSet<String>>>,
304    /// Digests of files shims hashed or wrote this session, keyed by scope and
305    /// path, each entry standing while its recorded identity matches the disk.
306    file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
307    /// Deferred remote publication, present only when the session may write.
308    uploads: Option<UploadQueue>,
309}
310
311/// What every file-digest record must satisfy before the ledger will answer
312/// with it, whether a shim sent it or an earlier session left it behind.
313fn validate_file_digest_record(entry: &RecordedFileDigest) -> Result<()> {
314    if !entry.file.path.is_absolute() {
315        bail!("file-digest records need absolute paths");
316    }
317    entry.digest.validate()?;
318    if entry.file.len != entry.digest.size {
319        bail!("file-digest record length does not match its digest");
320    }
321    Ok(())
322}
323
324/// Records background upload activity against a session's statistics.
325struct AgentUploadSink {
326    stats: Arc<AtomicAgentStats>,
327}
328
329impl UploadSink for AgentUploadSink {
330    fn record_blob_uploaded(&self, bytes: u64) {
331        self.stats
332            .background_uploads
333            .fetch_add(1, Ordering::Relaxed);
334        self.stats
335            .uploaded_bytes
336            .fetch_add(bytes, Ordering::Relaxed);
337    }
338
339    fn record_action_uploaded(&self) {
340        self.stats
341            .background_uploads
342            .fetch_add(1, Ordering::Relaxed);
343    }
344
345    fn record_blob_pack_uploaded(&self, blobs: u64) {
346        self.stats
347            .remote_blob_pack_uploads
348            .fetch_add(1, Ordering::Relaxed);
349        self.stats
350            .remote_blob_pack_upload_blobs
351            .fetch_add(blobs, Ordering::Relaxed);
352    }
353
354    fn record_upload_failure(&self) {
355        self.stats
356            .background_upload_failures
357            .fetch_add(1, Ordering::Relaxed);
358        self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
359    }
360}
361
362/// A CAS blob whose contents this session hashed, and the file identity that
363/// says the hash still describes what is on disk.
364///
365/// Length alone would miss an overwrite that keeps the size; the modification
366/// time is what makes a rewrite visible without reading the bytes again.
367#[derive(Debug, Clone)]
368struct VerifiedBlob {
369    path: PathBuf,
370    len: u64,
371    modified: SystemTime,
372}
373
374impl VerifiedBlob {
375    /// Describe a file that was just verified, or nothing when the filesystem
376    /// will not report a modification time to compare against later.
377    fn describe(path: &Path) -> Option<Self> {
378        let metadata = std::fs::metadata(path).ok()?;
379        Some(Self {
380            path: path.to_path_buf(),
381            len: metadata.len(),
382            modified: metadata.modified().ok()?,
383        })
384    }
385
386    /// Whether the file still has the identity it had when it was verified.
387    fn is_unchanged(&self) -> bool {
388        let Ok(metadata) = std::fs::metadata(&self.path) else {
389            return false;
390        };
391        metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
392    }
393}
394
395#[derive(Debug, Clone, Default)]
396struct TaskActionState {
397    manifest: String,
398    baseline_loaded: bool,
399    predictions: BTreeMap<CacheDigest, ActionPrediction>,
400    pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
401    prefetched_adapters: BTreeSet<String>,
402    remote_etag: Option<String>,
403}
404
405/// Match an invocation and, on an adapter's first match, select its prefetch wave.
406fn activate_prediction_adapter(
407    state: &mut TaskActionState,
408    invocation: &CacheDigest,
409) -> (Option<ActionPrediction>, Option<Vec<ActionPrediction>>) {
410    let prediction = state.predictions.get(invocation).cloned();
411    let prefetch = prediction.as_ref().and_then(|prediction| {
412        if !state.prefetched_adapters.insert(prediction.adapter.clone()) {
413            return None;
414        }
415        Some(
416            state
417                .predictions
418                .values()
419                .filter(|candidate| candidate.adapter == prediction.adapter)
420                .cloned()
421                .collect(),
422        )
423    });
424    (prediction, prefetch)
425}
426
427struct PrefetchedAction {
428    adapter: String,
429    result: RemoteActionResult,
430}
431
432struct RemoteDownloadReservation {
433    counter: Arc<AtomicU64>,
434    reserved: u64,
435    committed: bool,
436}
437
438impl RemoteDownloadReservation {
439    fn bytes(&self) -> u64 {
440        self.reserved
441    }
442
443    fn commit(mut self, bytes: u64) {
444        debug_assert!(bytes <= self.reserved);
445        self.counter
446            .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
447        self.committed = true;
448    }
449}
450
451impl Drop for RemoteDownloadReservation {
452    fn drop(&mut self) {
453        if !self.committed {
454            self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
455        }
456    }
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
460struct ExecutableIdentityKey {
461    executable: PathBuf,
462    environment: BTreeMap<String, Option<String>>,
463}
464
465impl CacheAgent {
466    /// Create an agent backed by the cache rooted at `cache_dir`.
467    pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
468        Self::build(cache_dir.into(), version.into(), None, 0)
469    }
470
471    /// Create an agent with local-first access to a remote action cache.
472    pub fn new_remote(
473        cache_dir: impl Into<PathBuf>,
474        version: impl Into<Arc<str>>,
475        remote: AgentRemoteCache,
476    ) -> Self {
477        Self::build(
478            cache_dir.into(),
479            version.into(),
480            Some(remote),
481            DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
482        )
483    }
484
485    /// Create a remote agent with a cumulative download budget for the session.
486    pub fn new_remote_with_download_limit(
487        cache_dir: impl Into<PathBuf>,
488        version: impl Into<Arc<str>>,
489        remote: AgentRemoteCache,
490        max_remote_download_bytes: u64,
491    ) -> Self {
492        Self::build(
493            cache_dir.into(),
494            version.into(),
495            Some(remote),
496            max_remote_download_bytes,
497        )
498    }
499
500    fn build(
501        cache_dir: PathBuf,
502        version: Arc<str>,
503        remote: Option<AgentRemoteCache>,
504        remote_download_limit: u64,
505    ) -> Self {
506        let remote_mode = remote
507            .as_ref()
508            .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
509        let remote_staging_dir = remote.as_ref().map_or_else(
510            || cache_dir.join("remote"),
511            |remote| remote.staging_dir.clone(),
512        );
513        let remote = remote.map(|remote| Arc::new(remote.client));
514        let stats = Arc::new(AtomicAgentStats::default());
515        let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
516        // A session that cannot publish never queues an upload, so it does not
517        // need somewhere to queue one.
518        let uploads = remote
519            .clone()
520            .filter(|_| remote_mode.writes())
521            .map(|client| {
522                UploadQueue::new(
523                    client,
524                    Arc::new(AgentUploadSink {
525                        stats: stats.clone(),
526                    }),
527                    remote_transfers.clone(),
528                )
529            });
530        Self {
531            cas: LocalCas::new(cache_dir.clone()),
532            actions: LocalActionCache::new(cache_dir.clone()),
533            verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
534            version,
535            write_locks: Arc::new(Mutex::new(BTreeMap::new())),
536            action_locks: Arc::new(Mutex::new(BTreeMap::new())),
537            stats,
538            observer: None,
539            observer_emission: Arc::new(Mutex::new(())),
540            task_loader: None,
541            executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
542            manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
543            task_actions: Arc::new(Mutex::new(BTreeMap::new())),
544            task_fallbacks: Arc::new(Mutex::new(BTreeMap::new())),
545            next_task_run: Arc::new(AtomicU64::new(0)),
546            manifest_write_lock: Arc::new(Mutex::new(())),
547            remote,
548            remote_mode,
549            remote_staging_dir: Arc::new(remote_staging_dir),
550            remote_download_limit,
551            remote_download_bytes: Arc::new(AtomicU64::new(0)),
552            pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
553            remote_transfers,
554            prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
555            prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
556            warnings: Arc::new(Mutex::new(BTreeSet::new())),
557            file_digests: Arc::new(Mutex::new(BTreeMap::new())),
558            uploads,
559        }
560    }
561
562    /// Report each accounted cache decision to `observer` as it happens.
563    #[must_use]
564    pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
565        self.observer = Some(observer);
566        self
567    }
568
569    /// Load tasks on demand.
570    ///
571    /// The loader is called with the agent and the task's identity before the
572    /// first request that names a task this agent has not begun, and is
573    /// expected to begin it; a second call for the same task must be a no-op.
574    /// Requests that name no task never wait on it.
575    pub fn with_task_loader(mut self, loader: Arc<TaskLoader>) -> Self {
576        self.task_loader = Some(loader);
577        self
578    }
579
580    /// Make sure `task` has been begun before a request reads or records
581    /// under it.
582    async fn ensure_task_loaded(&self, task: &str) {
583        let Some(loader) = &self.task_loader else {
584            return;
585        };
586        if self.task_actions.lock().unwrap().contains_key(task) {
587            return;
588        }
589        loader(self, task).await;
590    }
591
592    fn emit(&self, event: impl FnOnce() -> AgentEvent) {
593        if let Some(observer) = &self.observer {
594            let _emission = self.observer_emission.lock().unwrap();
595            observer.event(event());
596        }
597    }
598
599    fn emit_action(&self, diagnostic: Option<AgentEvent>, action: AgentEvent) {
600        if let Some(observer) = &self.observer {
601            let _emission = self.observer_emission.lock().unwrap();
602            if let Some(diagnostic) = diagnostic {
603                observer.event(diagnostic);
604            }
605            observer.event(action);
606        }
607    }
608
609    fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
610        let mut current = self.remote_download_bytes.load(Ordering::Acquire);
611        loop {
612            let next = current
613                .checked_add(bytes)
614                .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
615            if next > self.remote_download_limit {
616                bail!(
617                    "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
618                    bytes,
619                    current,
620                    self.remote_download_limit
621                );
622            }
623            match self.remote_download_bytes.compare_exchange_weak(
624                current,
625                next,
626                Ordering::AcqRel,
627                Ordering::Acquire,
628            ) {
629                Ok(_) => {
630                    return Ok(RemoteDownloadReservation {
631                        counter: self.remote_download_bytes.clone(),
632                        reserved: bytes,
633                        committed: false,
634                    });
635                }
636                Err(observed) => current = observed,
637            }
638        }
639    }
640
641    fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
642        let mut current = self.remote_download_bytes.load(Ordering::Acquire);
643        loop {
644            let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
645            let next = current + reserved;
646            match self.remote_download_bytes.compare_exchange_weak(
647                current,
648                next,
649                Ordering::AcqRel,
650                Ordering::Acquire,
651            ) {
652                Ok(_) => {
653                    return Ok(RemoteDownloadReservation {
654                        counter: self.remote_download_bytes.clone(),
655                        reserved,
656                        committed: false,
657                    });
658                }
659                Err(observed) => current = observed,
660            }
661        }
662    }
663
664    /// Load the last committed action manifest for a task into this session.
665    pub async fn begin_task(&self, task: &str) -> Result<String> {
666        self.begin_task_with_remote_errors(task, false, None, true)
667            .await
668    }
669
670    /// Load a task but defer each adapter's speculative downloads until its
671    /// first prediction matches the current build.
672    pub async fn begin_task_on_prediction(&self, task: &str) -> Result<String> {
673        self.begin_task_with_remote_errors(task, false, None, false)
674            .await
675    }
676
677    /// Load a task using its identity as the run key.
678    ///
679    /// A task-scoped process has only one run for an identity, so it can defer
680    /// this work until its first client connects while putting the identity in
681    /// the client's environment ahead of time.
682    pub async fn begin_session_task(&self, task: &str) -> Result<()> {
683        self.begin_task_with_remote_errors(task, false, Some(task.to_string()), false)
684            .await?;
685        Ok(())
686    }
687
688    /// Name where a task's predictions may come from when nothing has been
689    /// recorded under its own identity.
690    ///
691    /// The identities are produced on demand rather than taken up front,
692    /// because finding them can cost a few version-control commands and most
693    /// builds never need them. The first manifest found, locally and then on
694    /// the remote, is adopted under the task's own identity, so the commands
695    /// that follow, tests and lints included, start from it too, and a trusted
696    /// build publishes it there. When none of the named identities has one,
697    /// the store's newest manifests are tried.
698    pub fn register_task_fallbacks(
699        &self,
700        task: &str,
701        fallbacks: impl Fn() -> Vec<String> + Send + Sync + 'static,
702    ) -> Result<()> {
703        validate_task_identity(task)?;
704        self.task_fallbacks
705            .lock()
706            .unwrap()
707            .insert(task.to_string(), Arc::new(fallbacks));
708        Ok(())
709    }
710
711    /// Load a task and finish its prefetch, surfacing remote lookup failures.
712    pub async fn prefetch_task(&self, task: &str) -> Result<String> {
713        let run = self
714            .begin_task_with_remote_errors(task, true, None, true)
715            .await?;
716        self.wait_for_prefetches().await;
717        Ok(run)
718    }
719
720    async fn begin_task_with_remote_errors(
721        &self,
722        task: &str,
723        strict: bool,
724        run: Option<String>,
725        eager_prefetch: bool,
726    ) -> Result<String> {
727        validate_task_identity(task)?;
728        // The local manifest is already enough to start useful work. Do not
729        // leave those predictions idle while a high-latency remote answers the
730        // manifest lookup; the authoritative snapshot is loaded again below
731        // before it is merged, so another process can still update it while
732        // this request is in flight.
733        let early_manifest = {
734            let _write_guard = self.manifest_write_lock.lock().unwrap();
735            let _file_guard = self.lock_task_manifest(task)?;
736            self.load_task_manifest(task)?
737        };
738        let early_actions: BTreeSet<_> = early_manifest
739            .iter()
740            .flat_map(|manifest| manifest.predictions.iter())
741            .map(|prediction| prediction.action.clone())
742            .collect();
743        if eager_prefetch {
744            self.spawn_prefetch_predictions(
745                early_manifest
746                    .as_ref()
747                    .map(|manifest| manifest.predictions.clone())
748                    .unwrap_or_default(),
749            );
750        }
751        let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
752            match self.get_remote_task_manifest(task).await {
753                Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
754                Ok(None) => (None, None),
755                Err(error) => {
756                    if strict {
757                        return Err(error).wrap_err_with(|| {
758                            format!("remote task action manifest lookup failed for {task}")
759                        });
760                    }
761                    self.note_remote_failure();
762                    warn!("remote task action manifest lookup failed for {task}: {error}");
763                    (None, None)
764                }
765            }
766        } else {
767            (None, None)
768        };
769        // Without a remote there is nothing to reconcile: the manifest just
770        // read is the whole truth, and writing it back byte for byte would
771        // serialize every prediction for nothing.
772        let manifest = if self.remote.is_some() && self.remote_mode.reads() {
773            let _write_guard = self.manifest_write_lock.lock().unwrap();
774            let _file_guard = self.lock_task_manifest(task)?;
775            let local_manifest = self.load_task_manifest(task)?;
776            let manifest = match (remote_manifest, local_manifest) {
777                (Some(remote), Some(local)) => {
778                    let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
779                    if !merged {
780                        remote_etag = None;
781                    }
782                    Some(manifest)
783                }
784                (Some(remote), None) => Some(remote),
785                (None, local) => local,
786            };
787            if let Some(manifest) = &manifest {
788                self.persist_task_manifest(manifest)?;
789            }
790            manifest
791        } else {
792            early_manifest
793        };
794        let (manifest, remote_etag) = match manifest {
795            Some(manifest) => (Some(manifest), remote_etag),
796            // Inherited from another identity, so the remote's copy of that
797            // one is no precondition for publishing under this one.
798            None => (self.inherit_task_manifest(task, strict).await?, None),
799        };
800        let mut state = if let Some(manifest) = manifest {
801            TaskActionState {
802                manifest: task.to_string(),
803                baseline_loaded: true,
804                predictions: manifest
805                    .predictions
806                    .into_iter()
807                    .map(|prediction| (prediction.invocation.clone(), prediction))
808                    .collect(),
809                pending_predictions: BTreeMap::new(),
810                prefetched_adapters: BTreeSet::new(),
811                remote_etag,
812            }
813        } else {
814            TaskActionState {
815                manifest: task.to_string(),
816                baseline_loaded: true,
817                remote_etag,
818                ..TaskActionState::default()
819            }
820        };
821        let run = run.unwrap_or_else(|| {
822            let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
823            CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
824                .hash
825        });
826        // The largest baseline rather than a sum: beginning the same task again
827        // in one session reloads the same manifest, and counting it twice would
828        // overstate what there was to match.
829        self.stats.predictions_loaded.fetch_max(
830            state.predictions.len().try_into().unwrap_or(u64::MAX),
831            Ordering::Relaxed,
832        );
833        if eager_prefetch {
834            state.prefetched_adapters.extend(
835                state
836                    .predictions
837                    .values()
838                    .map(|prediction| prediction.adapter.clone()),
839            );
840        }
841        // The early local wave already owns these actions. Only launch a
842        // second wave for predictions learned from the remote or from a local
843        // writer that committed while its lookup was in flight.
844        let predictions = state
845            .predictions
846            .values()
847            .filter(|prediction| !early_actions.contains(&prediction.action))
848            .cloned()
849            .collect();
850        self.task_actions.lock().unwrap().insert(run.clone(), state);
851        if eager_prefetch {
852            self.spawn_prefetch_predictions(predictions);
853        }
854        Ok(run)
855    }
856
857    /// Adopt the manifest of a fallback identity for a task that has none.
858    ///
859    /// The named identities are tried in the order given, each in the local
860    /// store and then on the remote, and then the store's newest manifests,
861    /// locally only. The first one found is persisted under `task` so the
862    /// next command sees a complete baseline rather than only what this one
863    /// recorded, and a later session finds it without asking again.
864    async fn inherit_task_manifest(
865        &self,
866        task: &str,
867        strict: bool,
868    ) -> Result<Option<TaskActionManifest>> {
869        let fallbacks = self.task_fallbacks.lock().unwrap().get(task).cloned();
870        let Some(fallbacks) = fallbacks else {
871            return Ok(None);
872        };
873        let named = tokio::task::spawn_blocking(move || fallbacks()).await?;
874        let mut tried = BTreeSet::from([task.to_string()]);
875        let candidates = named.into_iter().map(|identity| (identity, true)).chain(
876            self.newest_task_identities()
877                .into_iter()
878                .map(|identity| (identity, false)),
879        );
880        for (identity, named) in candidates {
881            if !tried.insert(identity.clone()) || validate_task_identity(&identity).is_err() {
882                continue;
883            }
884            let mut found = self.load_task_manifest(&identity)?;
885            if found.is_none() && named && self.remote_mode.reads() {
886                found = match self.get_remote_task_manifest(&identity).await {
887                    Ok(manifest) => manifest.map(|(manifest, _)| manifest),
888                    Err(error) => {
889                        if strict {
890                            return Err(error).wrap_err_with(|| {
891                                format!("remote task action manifest lookup failed for {identity}")
892                            });
893                        }
894                        self.note_remote_failure();
895                        warn!("remote task action manifest lookup failed for {identity}: {error}");
896                        None
897                    }
898                };
899            }
900            let Some(mut manifest) = found else {
901                continue;
902            };
903            if manifest.predictions.is_empty()
904                || (!named && manifest.predictions.len() > MAX_TASK_ACTION_PREDICTIONS / 2)
905            {
906                continue;
907            }
908            info!(
909                "no predictions were recorded for {task}; inheriting {} from {identity}",
910                manifest.predictions.len()
911            );
912            manifest.task = task.to_string();
913            validate_task_manifest(&manifest, task)?;
914            let _write_guard = self.manifest_write_lock.lock().unwrap();
915            let _file_guard = self.lock_task_manifest(task)?;
916            // Another process may have recorded this identity while the
917            // fallbacks were being found. What it wrote describes this
918            // lockfile and wins over an inheritance.
919            if let Some(recorded) = self.load_task_manifest(task)? {
920                return Ok(Some(recorded));
921            }
922            self.persist_task_manifest(&manifest)?;
923            return Ok(Some(manifest));
924        }
925        Ok(None)
926    }
927
928    /// The identities of the store's most recently written manifests.
929    fn newest_task_identities(&self) -> Vec<String> {
930        let Ok(entries) = fs::read_dir(self.manifest_dir.as_path()) else {
931            return Vec::new();
932        };
933        let mut manifests: Vec<(std::time::SystemTime, String)> = entries
934            .filter_map(|entry| {
935                let entry = entry.ok()?;
936                let name = entry.file_name();
937                let identity = name.to_str()?.strip_suffix(".json")?.to_string();
938                validate_task_identity(&identity).ok()?;
939                let modified = entry.metadata().ok()?.modified().ok()?;
940                Some((modified, identity))
941            })
942            .collect();
943        manifests.sort_by(|left, right| right.cmp(left));
944        manifests
945            .into_iter()
946            .take(NEWEST_MANIFEST_CANDIDATES)
947            .map(|(_, identity)| identity)
948            .collect()
949    }
950
951    /// Cancel speculative downloads before the owning session exits.
952    pub async fn cancel_prefetches(&self) {
953        let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
954        for task in &tasks {
955            task.abort();
956        }
957        for task in tasks {
958            if let Err(error) = task.await
959                && !error.is_cancelled()
960            {
961                warn!("remote action prefetch task failed: {error}");
962            }
963        }
964    }
965
966    /// Publish everything a build queued, before the session stops.
967    ///
968    /// Store requests return once an object is durable locally, so at this point
969    /// the remote cache may still be behind the local one. Uploads run on the
970    /// session's runtime and are abandoned if it goes away, so a session that
971    /// wants them published has to wait here.
972    pub async fn wait_for_uploads(&self) {
973        let Some(uploads) = &self.uploads else {
974            return;
975        };
976        let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
977        uploads.drain().await;
978    }
979
980    async fn wait_for_prefetches(&self) {
981        let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
982        for task in tasks {
983            if let Err(error) = task.await {
984                warn!("remote action prefetch task failed: {error}");
985            }
986        }
987    }
988
989    /// Atomically publish the completed actions collected by a task run.
990    pub async fn commit_task(&self, run: &str) -> Result<()> {
991        self.commit_task_actions(run).await.map(|_| ())
992    }
993
994    /// Publish a task run and return exactly the predictions completed by it.
995    ///
996    /// The persisted task manifest also carries predictions inherited from
997    /// earlier runs. Callers that need a receipt for this one run must not
998    /// mistake that cumulative manifest for the work the run completed.
999    pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
1000        validate_task_identity(run)?;
1001        let state = {
1002            let mut runs = self.task_actions.lock().unwrap();
1003            let state = runs
1004                .get(run)
1005                .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
1006            if !state.baseline_loaded {
1007                bail!("task action manifest baseline was not loaded");
1008            }
1009            // A run that predicted nothing new leaves the manifest as it found
1010            // it. Rewriting it would serialize every inherited prediction to
1011            // say so, and a remote that is only read has nothing to learn
1012            // either. Decided before the baseline is cloned: the baseline is
1013            // every prediction the manifest holds.
1014            if state.pending_predictions.is_empty()
1015                && (self.remote.is_none() || !self.remote_mode.writes())
1016            {
1017                runs.remove(run);
1018                return Ok(Vec::new());
1019            }
1020            state.clone()
1021        };
1022        let task = state.manifest;
1023        validate_task_identity(&task)?;
1024        let completed = state
1025            .pending_predictions
1026            .values()
1027            .cloned()
1028            .collect::<Vec<_>>();
1029        let (manifest, introduced) = {
1030            let _write_guard = self.manifest_write_lock.lock().unwrap();
1031            let _file_guard = self.lock_task_manifest(&task)?;
1032            let mut predictions = self
1033                .load_task_manifest(&task)?
1034                .map(|manifest| {
1035                    manifest
1036                        .predictions
1037                        .into_iter()
1038                        .map(|prediction| (prediction.invocation.clone(), prediction))
1039                        .collect::<BTreeMap<_, _>>()
1040                })
1041                .unwrap_or_default();
1042            // Which predictions this run adds, as opposed to inherits. Only a
1043            // new one may be withheld for a failed upload: retracting an
1044            // inherited one would un-advertise a result that is plausibly still
1045            // on the server from whichever session put it there.
1046            let introduced: BTreeSet<CacheDigest> = state
1047                .pending_predictions
1048                .keys()
1049                .filter(|invocation| !predictions.contains_key(*invocation))
1050                .cloned()
1051                .collect();
1052            // Only publish predictions recorded by this run. `predictions`
1053            // also contains the baseline loaded by `begin_task`; extending
1054            // with that snapshot would overwrite newer entries committed by
1055            // another agent process after this run began.
1056            predictions.extend(state.pending_predictions);
1057            let manifest = TaskActionManifest {
1058                version: TASK_ACTION_MANIFEST_VERSION,
1059                task: task.clone(),
1060                predictions: predictions.into_values().collect(),
1061            };
1062            validate_task_manifest(&manifest, &task)?;
1063            self.persist_task_manifest(&manifest)?;
1064            (manifest, introduced)
1065        };
1066        self.task_actions.lock().unwrap().remove(run);
1067        if self.remote_mode.writes() {
1068            // A manifest advertises the actions it predicts, so it must not
1069            // reach the remote cache before the results a reader would then go
1070            // looking for -- nor name a result that never got there at all.
1071            let mut manifest = manifest;
1072            if let Some(uploads) = &self.uploads {
1073                let actions: Vec<CacheDigest> = manifest
1074                    .predictions
1075                    .iter()
1076                    .map(|prediction| prediction.action.clone())
1077                    .collect();
1078                let unpublished = uploads.wait_for_actions(&actions).await;
1079                let withheld = manifest
1080                    .predictions
1081                    .iter()
1082                    .filter(|prediction| {
1083                        introduced.contains(&prediction.invocation)
1084                            && unpublished.contains(&prediction.action)
1085                    })
1086                    .count();
1087                if withheld > 0 {
1088                    // The local manifest keeps them: this checkout can still use
1089                    // what it built, and a later session can publish it.
1090                    warn!(
1091                        "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
1092                        manifest.predictions.len()
1093                    );
1094                    manifest.predictions.retain(|prediction| {
1095                        !(introduced.contains(&prediction.invocation)
1096                            && unpublished.contains(&prediction.action))
1097                    });
1098                }
1099            }
1100            match self
1101                .put_remote_task_manifest(&task, manifest, state.remote_etag)
1102                .await
1103            {
1104                Ok(remote_manifest) => {
1105                    let _write_guard = self.manifest_write_lock.lock().unwrap();
1106                    let reconciliation = (|| {
1107                        let _file_guard = self.lock_task_manifest(&task)?;
1108                        let manifest = match self.load_task_manifest(&task)? {
1109                            Some(local) => {
1110                                merge_remote_task_manifest(&task, remote_manifest, local).0
1111                            }
1112                            None => remote_manifest,
1113                        };
1114                        self.persist_task_manifest(&manifest)
1115                    })();
1116                    if let Err(error) = reconciliation {
1117                        warn!(
1118                            "remote task action manifest reconciliation failed for {task}: {error}"
1119                        );
1120                    }
1121                }
1122                Err(error) => {
1123                    self.note_remote_failure();
1124                    warn!("remote task action manifest upload failed for {task}: {error}");
1125                }
1126            }
1127        }
1128        Ok(completed)
1129    }
1130
1131    fn task_manifest_path(&self, task: &str) -> PathBuf {
1132        self.manifest_dir.join(format!("{task}.json"))
1133    }
1134
1135    fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
1136        self.manifest_dir.join("locks").join(format!("{task}.lock"))
1137    }
1138
1139    fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
1140        let path = self.task_manifest_lock_path(task);
1141        fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
1142        let mut lock = fslock::LockFile::open(&path)?;
1143        lock.lock()?;
1144        Ok(lock)
1145    }
1146
1147    fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
1148        match fs::read(self.task_manifest_path(task)) {
1149            Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
1150            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1151            Err(error) => Err(error.into()),
1152        }
1153    }
1154
1155    fn parse_task_manifest(
1156        &self,
1157        task: &str,
1158        contents: &[u8],
1159        require_canonical: bool,
1160    ) -> Result<TaskActionManifest> {
1161        let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
1162        validate_task_manifest(&manifest, task)?;
1163        if require_canonical && canonical_json(&manifest)? != contents {
1164            bail!("task action manifest is not canonical JSON");
1165        }
1166        Ok(manifest)
1167    }
1168
1169    fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
1170        TaskActionManifest::selector(task)
1171    }
1172
1173    fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
1174        let bytes = canonical_json(manifest)?;
1175        fs::create_dir_all(self.manifest_dir.as_path())?;
1176        let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
1177        std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
1178        temporary.as_file_mut().sync_all()?;
1179        temporary
1180            .persist(self.task_manifest_path(&manifest.task))
1181            .map_err(|error| error.error)?;
1182        Ok(())
1183    }
1184
1185    async fn get_remote_task_manifest(
1186        &self,
1187        task: &str,
1188    ) -> Result<Option<(TaskActionManifest, String)>> {
1189        let Some(remote) = &self.remote else {
1190            return Ok(None);
1191        };
1192        let (_, selector) = Self::task_manifest_selector(task)?;
1193        let _permit = self.remote_transfers.acquire().await?;
1194        self.stats
1195            .remote_manifest_lookups
1196            .fetch_add(1, Ordering::Relaxed);
1197        let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
1198        let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
1199            return Ok(None);
1200        };
1201        let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
1202        Ok(Some((manifest, remote_manifest.etag)))
1203    }
1204
1205    async fn put_remote_task_manifest(
1206        &self,
1207        task: &str,
1208        mut manifest: TaskActionManifest,
1209        mut expected_etag: Option<String>,
1210    ) -> Result<TaskActionManifest> {
1211        let Some(remote) = &self.remote else {
1212            return Ok(manifest);
1213        };
1214        let (_, selector) = Self::task_manifest_selector(task)?;
1215        for _ in 0..4 {
1216            let bytes = canonical_json(&manifest)?;
1217            let outcome = {
1218                let _permit = self.remote_transfers.acquire().await?;
1219                remote
1220                    .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
1221                    .await?
1222            };
1223            match outcome {
1224                ManifestPutOutcome::Stored => return Ok(manifest),
1225                ManifestPutOutcome::PreconditionFailed => {
1226                    let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
1227                    else {
1228                        expected_etag = None;
1229                        continue;
1230                    };
1231                    manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
1232                    expected_etag = Some(etag);
1233                }
1234            }
1235        }
1236        bail!("remote task action manifest changed too frequently")
1237    }
1238
1239    /// Return a snapshot of this session's cache activity.
1240    pub fn stats(&self) -> AgentStats {
1241        AgentStats {
1242            session_duration_ns: 0,
1243            lookups: self.stats.lookups.load(Ordering::Relaxed),
1244            unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
1245            hits: self.stats.hits.load(Ordering::Relaxed),
1246            stores: self.stats.stores.load(Ordering::Relaxed),
1247            stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1248            verifications: self.stats.verifications.load(Ordering::Relaxed),
1249            divergences: self.stats.divergences.load(Ordering::Relaxed),
1250            downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1251            uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1252            background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1253            background_upload_failures: self
1254                .stats
1255                .background_upload_failures
1256                .load(Ordering::Relaxed),
1257            remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1258            remote_blob_pack_upload_blobs: self
1259                .stats
1260                .remote_blob_pack_upload_blobs
1261                .load(Ordering::Relaxed),
1262            upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1263            prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1264            predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
1265            bypasses: self.stats.bypasses.lock().unwrap().clone(),
1266            avoided_compiler_duration_ns: self
1267                .stats
1268                .avoided_compiler_duration_ns
1269                .load(Ordering::Relaxed),
1270            compiler: self.stats.compiler.lock().unwrap().clone(),
1271            slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1272            remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1273            remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1274            remote_manifest_lookup_duration_ns: self
1275                .stats
1276                .remote_manifest_lookup_duration_ns
1277                .load(Ordering::Relaxed),
1278            remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1279            remote_action_lookup_duration_ns: self
1280                .stats
1281                .remote_action_lookup_duration_ns
1282                .load(Ordering::Relaxed),
1283            remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1284            remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1285            remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1286            remote_blob_transfer_duration_ns: self
1287                .stats
1288                .remote_blob_transfer_duration_ns
1289                .load(Ordering::Relaxed),
1290            local_cas_write_duration_ns: self
1291                .stats
1292                .local_cas_write_duration_ns
1293                .load(Ordering::Relaxed),
1294            prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1295            prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1296            materialization_duration_ns: self
1297                .stats
1298                .materialization_duration_ns
1299                .load(Ordering::Relaxed),
1300            restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1301            restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1302            reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1303            reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1304            copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1305            copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1306            reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
1307            reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
1308        }
1309    }
1310
1311    /// Handle requests without a transport connection.
1312    ///
1313    /// Persistent wrappers use this entry point when Cargo invokes mbx outside
1314    /// an orchestrated session. It intentionally has the same response
1315    /// semantics as [`Self::handle_connection`], while leaving framing and the
1316    /// version handshake to callers that actually cross a process boundary.
1317    pub async fn handle_requests(
1318        &self,
1319        requests: impl IntoIterator<Item = AgentRequest>,
1320    ) -> Vec<AgentResponse> {
1321        let mut connection = ConnectionUploads::default();
1322        let mut responses = Vec::new();
1323        for request in requests {
1324            responses.push(self.respond_on(request, &mut connection).await);
1325        }
1326        responses
1327    }
1328
1329    fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1330        Self::digest_lock(&self.write_locks, digest)
1331    }
1332
1333    fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1334        Self::digest_lock(&self.action_locks, digest)
1335    }
1336
1337    fn digest_lock(
1338        locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1339        digest: &CacheDigest,
1340    ) -> Arc<tokio::sync::Mutex<()>> {
1341        let mut locks = locks.lock().unwrap();
1342        locks.retain(|_, lock| lock.strong_count() > 0);
1343        if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1344            return lock;
1345        }
1346        let lock = Arc::new(tokio::sync::Mutex::new(()));
1347        locks.insert(digest.clone(), Arc::downgrade(&lock));
1348        lock
1349    }
1350
1351    /// Answer one request outside any connection.
1352    ///
1353    /// An upload queued this way has no sibling requests to order against, so
1354    /// the queue falls back to treating every blob it holds as a prerequisite.
1355    #[cfg(test)]
1356    async fn respond(&self, request: AgentRequest) -> AgentResponse {
1357        self.respond_on(request, &mut ConnectionUploads::default())
1358            .await
1359    }
1360
1361    async fn respond_on(
1362        &self,
1363        request: AgentRequest,
1364        connection: &mut ConnectionUploads,
1365    ) -> AgentResponse {
1366        match &request {
1367            AgentRequest::FindActionPrediction { task, .. }
1368            | AgentRequest::RecordActionPrediction { task, .. } => {
1369                self.ensure_task_loaded(task).await;
1370            }
1371            _ => {}
1372        }
1373        let result = match request {
1374            AgentRequest::BeginTask { task } => self
1375                .begin_task(&task)
1376                .await
1377                .map(|run| AgentResponse::TaskBegun { run }),
1378            AgentRequest::CommitTask { run } => self
1379                .commit_task(&run)
1380                .await
1381                .map(|()| AgentResponse::TaskCommitted),
1382            AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1383            AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1384            AgentRequest::StoreBlob { digest, source } => {
1385                self.store_blob(&digest, &source, connection).await
1386            }
1387            AgentRequest::FindActionResult { action } => {
1388                self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1389                self.find_action_result(&action).await
1390            }
1391            AgentRequest::RecordActionHit {
1392                action,
1393                restore,
1394                crate_name,
1395            } => {
1396                let diagnostic = connection
1397                    .take_action_diagnostic("hit", crate_name.as_deref())
1398                    .map(|diagnostic| AgentEvent::ActionDiagnostic {
1399                        outcome: "hit".into(),
1400                        crate_name: crate_name.clone(),
1401                        diagnostic,
1402                    });
1403                self.record_action_hit(&action, restore, crate_name, diagnostic)
1404            }
1405            AgentRequest::RecordBypass { kind } => {
1406                *self
1407                    .stats
1408                    .bypasses
1409                    .lock()
1410                    .unwrap()
1411                    .entry(kind.clone())
1412                    .or_insert(0) += 1;
1413                self.emit(|| AgentEvent::Bypass { kind });
1414                Ok(AgentResponse::BypassRecorded)
1415            }
1416            AgentRequest::RecordUnconsulted => {
1417                self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1418                self.emit(|| AgentEvent::Unconsulted);
1419                Ok(AgentResponse::UnconsultedRecorded)
1420            }
1421            AgentRequest::RecordWarning { message } => match parse_action_diagnostic(&message) {
1422                Some(Ok((outcome, crate_name, diagnostic))) => {
1423                    connection.record_action_diagnostic(outcome, crate_name, diagnostic);
1424                    Ok(AgentResponse::WarningRecorded)
1425                }
1426                Some(Err(error)) => Err(error),
1427                None => self.record_warning(message),
1428            },
1429            AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1430            AgentRequest::JoinActionPromise {
1431                adapter,
1432                invocation,
1433            } => self.join_action_promise(&adapter, &invocation).await,
1434            AgentRequest::CompleteActionPromise { claim, prediction } => {
1435                self.complete_action_promise(&claim, &prediction).await
1436            }
1437            AgentRequest::RecordFileDigests { scope, entries } => {
1438                self.record_file_digests(scope, entries)
1439            }
1440            AgentRequest::RecordCompilerInvocation {
1441                outcome,
1442                crate_name,
1443                duration_ns,
1444            } => {
1445                let diagnostic = connection
1446                    .take_action_diagnostic(&outcome, crate_name.as_deref())
1447                    .map(|diagnostic| AgentEvent::ActionDiagnostic {
1448                        outcome: outcome.clone(),
1449                        crate_name: crate_name.clone(),
1450                        diagnostic,
1451                    });
1452                self.record_compiler_invocation(
1453                    &outcome,
1454                    crate_name.as_deref(),
1455                    duration_ns,
1456                    diagnostic,
1457                )
1458            }
1459            AgentRequest::RecordActionVerification { matched, restore } => {
1460                self.record_materialization(restore);
1461                self.stats.verifications.fetch_add(1, Ordering::Relaxed);
1462                if !matched {
1463                    self.stats.divergences.fetch_add(1, Ordering::Relaxed);
1464                }
1465                self.emit(|| AgentEvent::Verification { matched, restore });
1466                Ok(AgentResponse::ActionVerificationRecorded)
1467            }
1468            AgentRequest::StoreActionResult { result } => {
1469                self.store_action_result(&result, connection).await
1470            }
1471            AgentRequest::FindActionPrediction { task, invocation } => {
1472                self.find_action_prediction(&task, &invocation)
1473            }
1474            AgentRequest::RecordActionPrediction { task, prediction } => {
1475                self.record_action_prediction(&task, prediction)
1476            }
1477            AgentRequest::FindExecutableIdentity {
1478                executable,
1479                environment,
1480            } => self.find_executable_identity(executable, environment),
1481            AgentRequest::StoreExecutableIdentity {
1482                executable,
1483                environment,
1484                stdout,
1485            } => self.store_executable_identity(executable, environment, stdout),
1486            AgentRequest::Hello { .. } => {
1487                Err(eyre::eyre!("hello is only valid as the first request"))
1488            }
1489        };
1490        result.unwrap_or_else(|error| AgentResponse::Error {
1491            message: error.to_string(),
1492        })
1493    }
1494
1495    async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
1496        if let Some(path) = self.find_verified_blob(digest)? {
1497            return Ok(AgentResponse::Blob { path: Some(path) });
1498        }
1499        if !self.remote_mode.reads() {
1500            return Ok(AgentResponse::Blob { path: None });
1501        }
1502        let Some(remote) = &self.remote else {
1503            return Ok(AgentResponse::Blob { path: None });
1504        };
1505        match self.fetch_remote_blob(remote, digest).await {
1506            Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
1507            Err(error) => {
1508                warn!(
1509                    "remote cache blob lookup failed for {}: {error}",
1510                    digest.hash
1511                );
1512                Ok(AgentResponse::Blob { path: None })
1513            }
1514        }
1515    }
1516
1517    async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
1518        let mut paths = BTreeMap::new();
1519        let mut missing = Vec::new();
1520        for digest in &digests {
1521            match self.find_verified_blob(digest)? {
1522                Some(path) => {
1523                    paths.insert(digest.clone(), path);
1524                }
1525                None => {
1526                    missing.push(digest.clone());
1527                }
1528            }
1529        }
1530
1531        if !missing.is_empty()
1532            && self.remote_mode.reads()
1533            && let Some(remote) = &self.remote
1534        {
1535            paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
1536        }
1537
1538        Ok(AgentResponse::Blobs {
1539            paths: digests
1540                .into_iter()
1541                .map(|digest| paths.get(&digest).cloned())
1542                .collect(),
1543        })
1544    }
1545
1546    async fn store_blob(
1547        &self,
1548        digest: &CacheDigest,
1549        source: &Path,
1550        connection: &mut ConnectionUploads,
1551    ) -> Result<AgentResponse> {
1552        let path = {
1553            let lock = self.write_lock(digest);
1554            let _guard = lock.lock().await;
1555            if let Some(path) = self.find_verified_blob(digest)? {
1556                path
1557            } else {
1558                let path = self.cas.store_file(digest, source)?;
1559                self.remember_verified_blob(digest, &path);
1560                self.stats.stores.fetch_add(1, Ordering::Relaxed);
1561                self.stats
1562                    .stored_bytes
1563                    .fetch_add(digest.size, Ordering::Relaxed);
1564                path
1565            }
1566        };
1567        // The object is durable locally, so the build has what it needs and the
1568        // remote publication can happen after this request returns.
1569        if let Some(uploads) = &self.uploads {
1570            uploads.queue_blob(digest, path.clone(), connection);
1571        }
1572        Ok(AgentResponse::Stored { path })
1573    }
1574
1575    fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
1576        let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
1577        if let Some(remembered) = remembered {
1578            // The contents behind this digest were hashed in full when the
1579            // session first reached for them. Hashing again on every later
1580            // lookup would re-read each dependency's rlib once per crate that
1581            // links it, so what is rechecked here is that the file has not
1582            // been written since: an overwrite moves the modification time,
1583            // and truncation or eviction changes the length or removes it.
1584            // Only a replacement that reproduces both -- which no writer in
1585            // the store does, since blobs are published by rename under a
1586            // content-derived name -- would go unnoticed until `mbx cache
1587            // verify` reads the store back in full.
1588            if remembered.is_unchanged() {
1589                return Ok(Some(remembered.path));
1590            }
1591            self.verified_blobs.lock().unwrap().remove(digest);
1592        }
1593        let path = self.cas.find(digest)?;
1594        if let Some(path) = &path {
1595            self.remember_verified_blob(digest, path);
1596        }
1597        Ok(path)
1598    }
1599
1600    fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
1601        let Some(verified) = VerifiedBlob::describe(path) else {
1602            // Without an identity to compare against there is nothing to
1603            // shortcut safely, so the digest stays unremembered and every
1604            // lookup re-reads it.
1605            return;
1606        };
1607        self.verified_blobs
1608            .lock()
1609            .unwrap()
1610            .insert(digest.clone(), verified);
1611    }
1612
1613    async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
1614        if let Some(result) = self.actions.find(action)? {
1615            return Ok(AgentResponse::ActionResult {
1616                result: Some(result),
1617            });
1618        }
1619        if !self.remote_mode.reads() {
1620            return Ok(AgentResponse::ActionResult { result: None });
1621        }
1622        let Some(remote) = &self.remote else {
1623            return Ok(AgentResponse::ActionResult { result: None });
1624        };
1625        let lock = self.action_lock(action);
1626        let _guard = lock.lock().await;
1627        if let Some(result) = self.actions.find(action)? {
1628            return Ok(AgentResponse::ActionResult {
1629                result: Some(result),
1630            });
1631        }
1632        if let Some(result) = self
1633            .pending_remote_actions
1634            .lock()
1635            .unwrap()
1636            .get(action)
1637            .cloned()
1638        {
1639            return Ok(AgentResponse::ActionResult {
1640                result: Some(result),
1641            });
1642        }
1643        let _permit = self.remote_transfers.acquire().await?;
1644        match self.get_remote_action_result(remote, action).await {
1645            Ok(Some(result)) => {
1646                self.pending_remote_actions
1647                    .lock()
1648                    .unwrap()
1649                    .insert(action.clone(), result.clone());
1650                Ok(AgentResponse::ActionResult {
1651                    result: Some(result),
1652                })
1653            }
1654            Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
1655            Err(error) => {
1656                self.note_remote_failure();
1657                warn!(
1658                    "remote cache action lookup failed for {}: {error}",
1659                    action.hash
1660                );
1661                Ok(AgentResponse::ActionResult { result: None })
1662            }
1663        }
1664    }
1665
1666    async fn store_action_result(
1667        &self,
1668        result: &RemoteActionResult,
1669        connection: &ConnectionUploads,
1670    ) -> Result<AgentResponse> {
1671        let path = self.actions.store(result)?;
1672        if let Some(uploads) = &self.uploads {
1673            uploads.queue_action_result(result, connection);
1674        }
1675        Ok(AgentResponse::ActionStored { path })
1676    }
1677
1678    async fn join_action_promise(
1679        &self,
1680        adapter: &str,
1681        invocation: &CacheDigest,
1682    ) -> Result<AgentResponse> {
1683        if !self.remote_mode.reads() || !self.remote_mode.writes() {
1684            return Ok(AgentResponse::ActionPromise {
1685                claim: None,
1686                prediction: None,
1687            });
1688        }
1689        let Some(remote) = &self.remote else {
1690            return Ok(AgentResponse::ActionPromise {
1691                claim: None,
1692                prediction: None,
1693            });
1694        };
1695        let deadline = Instant::now() + ACTION_PROMISE_WAIT;
1696        loop {
1697            let _permit = self.remote_transfers.acquire().await?;
1698            let state = remote.join_action_promise(invocation, adapter).await;
1699            drop(_permit);
1700            match state {
1701                Ok(Some(ActionPromiseState::Claimed { claim })) => {
1702                    return Ok(AgentResponse::ActionPromise {
1703                        claim: Some(claim),
1704                        prediction: None,
1705                    });
1706                }
1707                Ok(Some(ActionPromiseState::Complete { prediction })) => {
1708                    return Ok(AgentResponse::ActionPromise {
1709                        claim: None,
1710                        prediction: Some(prediction),
1711                    });
1712                }
1713                Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
1714                    if Instant::now() < deadline =>
1715                {
1716                    tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
1717                        .await;
1718                }
1719                Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
1720                    return Ok(AgentResponse::ActionPromise {
1721                        claim: None,
1722                        prediction: None,
1723                    });
1724                }
1725                Ok(Some(_)) => {
1726                    return Ok(AgentResponse::ActionPromise {
1727                        claim: None,
1728                        prediction: None,
1729                    });
1730                }
1731                Err(error) => {
1732                    self.note_remote_failure();
1733                    warn!(
1734                        "remote cache action promise failed for {}: {error}",
1735                        invocation.hash
1736                    );
1737                    return Ok(AgentResponse::ActionPromise {
1738                        claim: None,
1739                        prediction: None,
1740                    });
1741                }
1742            }
1743        }
1744    }
1745
1746    async fn complete_action_promise(
1747        &self,
1748        claim: &str,
1749        prediction: &ActionPrediction,
1750    ) -> Result<AgentResponse> {
1751        prediction.validate()?;
1752        let Some(remote) = &self.remote else {
1753            return Ok(AgentResponse::ActionPromiseCompleted);
1754        };
1755        let Some(uploads) = &self.uploads else {
1756            return Ok(AgentResponse::ActionPromiseCompleted);
1757        };
1758        if uploads
1759            .wait_for_actions(std::slice::from_ref(&prediction.action))
1760            .await
1761            .contains(&prediction.action)
1762        {
1763            // Never promise an action result the server does not hold. The
1764            // server lease expires and another runner gets to repair it.
1765            return Ok(AgentResponse::ActionPromiseCompleted);
1766        }
1767        let completion = ActionPromiseCompletion {
1768            claim: claim.to_string(),
1769            prediction: prediction.clone(),
1770        };
1771        let _permit = self.remote_transfers.acquire().await?;
1772        if let Err(error) = remote
1773            .complete_action_promise(&prediction.invocation, &completion)
1774            .await
1775        {
1776            self.note_remote_failure();
1777            warn!(
1778                "remote cache action promise completion failed for {}: {error}",
1779                prediction.invocation.hash
1780            );
1781        }
1782        Ok(AgentResponse::ActionPromiseCompleted)
1783    }
1784
1785    /// Record that a remote operation failed and the build carried on without it.
1786    fn note_remote_failure(&self) {
1787        self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
1788    }
1789
1790    async fn get_remote_action_result(
1791        &self,
1792        remote: &RemoteCacheClient,
1793        action: &CacheDigest,
1794    ) -> Result<Option<RemoteActionResult>> {
1795        self.stats
1796            .remote_action_lookups
1797            .fetch_add(1, Ordering::Relaxed);
1798        let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1799        remote.get_action_result(action).await
1800    }
1801
1802    fn record_action_hit(
1803        &self,
1804        action: &CacheDigest,
1805        restore: RestoreStats,
1806        crate_name: Option<String>,
1807        diagnostic: Option<AgentEvent>,
1808    ) -> Result<AgentResponse> {
1809        validate_crate_name(crate_name.as_deref())?;
1810        if self.actions.find(action)?.is_none() {
1811            let pending = self.pending_remote_actions.lock().unwrap().remove(action);
1812            if let Some(result) = pending {
1813                self.actions.store(&result)?;
1814            } else {
1815                bail!("cannot record a hit for a missing action result");
1816            }
1817        }
1818        self.record_restore(restore);
1819        self.stats.hits.fetch_add(1, Ordering::Relaxed);
1820        self.emit_action(
1821            diagnostic,
1822            AgentEvent::ActionHit {
1823                crate_name,
1824                restore,
1825            },
1826        );
1827        Ok(AgentResponse::ActionHitRecorded)
1828    }
1829
1830    fn record_restore(&self, restore: RestoreStats) {
1831        self.record_materialization(restore);
1832        atomic_saturating_add(
1833            &self.stats.avoided_compiler_duration_ns,
1834            restore.avoided_compiler_duration_ns,
1835        );
1836        atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
1837        atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
1838        atomic_saturating_add(
1839            &self.stats.reflinked_output_files,
1840            restore.reflinked_output_files,
1841        );
1842        atomic_saturating_add(
1843            &self.stats.reflinked_output_bytes,
1844            restore.reflinked_output_bytes,
1845        );
1846        atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
1847        atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
1848        atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
1849        atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
1850    }
1851
1852    fn record_compiler_invocation(
1853        &self,
1854        outcome: &str,
1855        crate_name: Option<&str>,
1856        duration_ns: u64,
1857        diagnostic: Option<AgentEvent>,
1858    ) -> Result<AgentResponse> {
1859        if !matches!(
1860            outcome,
1861            "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
1862        ) {
1863            bail!("invalid compiler invocation outcome");
1864        }
1865        validate_crate_name(crate_name)?;
1866        let mut compiler = self.stats.compiler.lock().unwrap();
1867        let stats = compiler.entry(outcome.to_string()).or_default();
1868        stats.invocations = stats.invocations.saturating_add(1);
1869        stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
1870        drop(compiler);
1871        if outcome != "verification"
1872            && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
1873        {
1874            let mut slow = self.stats.slow_compilations.lock().unwrap();
1875            let duration = slow.entry(crate_name.to_string()).or_default();
1876            *duration = duration.saturating_add(duration_ns);
1877        }
1878        self.emit_action(
1879            diagnostic,
1880            AgentEvent::CompilerInvocation {
1881                outcome: outcome.to_string(),
1882                crate_name: crate_name.map(str::to_string),
1883                duration_ns,
1884            },
1885        );
1886        Ok(AgentResponse::CompilerInvocationRecorded)
1887    }
1888
1889    fn record_materialization(&self, restore: RestoreStats) {
1890        atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
1891    }
1892
1893    /// Surface a shim diagnostic on this process's stderr.
1894    ///
1895    /// The agent lives in the process that owns the session, so printing here
1896    /// reaches the terminal running the build rather than the stderr of the
1897    /// compilation the shim stands in for -- which build scripts read as part
1898    /// of the compiler's answer. Deduplicated because one cause tends to fire
1899    /// once per compilation, and capped so a message unique per compilation
1900    /// cannot scroll the build away.
1901    fn record_warning(&self, message: String) -> Result<AgentResponse> {
1902        if message.is_empty()
1903            || message.len() > MAX_WARNING_BYTES
1904            || message.contains(['\n', '\r', '\0'])
1905        {
1906            bail!("invalid shim warning");
1907        }
1908        let mut warnings = self.warnings.lock().unwrap();
1909        if !warnings.contains(&message) && warnings.len() < MAX_WARNINGS {
1910            eprintln!("mbx[warning]: {message}");
1911            self.emit(|| AgentEvent::Warning {
1912                message: message.clone(),
1913            });
1914            warnings.insert(message);
1915        }
1916        Ok(AgentResponse::WarningRecorded)
1917    }
1918
1919    /// Answer file-digest lookups from the session ledger.
1920    ///
1921    /// A recorded digest is returned only when the requested identity matches
1922    /// the recorded one exactly. The caller statted the file to build the
1923    /// identity, so this is the [`VerifiedBlob`] freshness check with the
1924    /// stat moved to the side that was already making it.
1925    fn find_file_digests(
1926        &self,
1927        scope: FileDigestScope,
1928        files: Vec<FileIdentity>,
1929    ) -> Result<AgentResponse> {
1930        if files.len() > MAX_FILE_DIGEST_BATCH {
1931            bail!("too many file-digest lookups in one request");
1932        }
1933        let ledger = self.file_digests.lock().unwrap();
1934        let digests = files
1935            .into_iter()
1936            .map(|file| {
1937                let recorded = ledger.get(&(scope, file.path.clone()))?;
1938                (recorded.file == file).then(|| recorded.digest.clone())
1939            })
1940            .collect();
1941        Ok(AgentResponse::FileDigests { digests })
1942    }
1943
1944    /// Record digests of files a shim read in full, for later reuse.
1945    ///
1946    /// Capped rather than evicted: a session that outgrows the cap loses the
1947    /// shortcut for the overflow and nothing else, and no workload observed so
1948    /// far comes near it.
1949    fn record_file_digests(
1950        &self,
1951        scope: FileDigestScope,
1952        entries: Vec<RecordedFileDigest>,
1953    ) -> Result<AgentResponse> {
1954        if entries.len() > MAX_FILE_DIGEST_BATCH {
1955            bail!("too many file-digest records in one request");
1956        }
1957        for entry in &entries {
1958            validate_file_digest_record(entry)?;
1959        }
1960        let mut ledger = self.file_digests.lock().unwrap();
1961        for entry in entries {
1962            if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
1963                && !ledger.contains_key(&(scope, entry.file.path.clone()))
1964            {
1965                break;
1966            }
1967            ledger.insert((scope, entry.file.path.clone()), entry);
1968        }
1969        Ok(AgentResponse::FileDigestsRecorded)
1970    }
1971
1972    /// Start the file-digest ledger from entries an earlier session left
1973    /// behind, so a file nothing has touched since is not read again by the
1974    /// first compilation of this session that names it.
1975    ///
1976    /// Each entry stands only while its recorded identity still matches the
1977    /// disk, the same rule a lookup applies to what this session recorded, so
1978    /// a stale seed costs a hash and nothing else. Entries this session has
1979    /// already recorded are kept over seeded ones. Returns how many were taken.
1980    pub fn seed_file_digests(&self, entries: Vec<(FileDigestScope, RecordedFileDigest)>) -> usize {
1981        self.seed_file_digests_with(|| entries)
1982    }
1983
1984    /// Seed the ledger from entries produced under its lock.
1985    ///
1986    /// A lookup that arrives while `load` is still reading waits for it rather
1987    /// than missing, so a caller can start the read in the background and let
1988    /// whatever else the build is doing overlap it.
1989    pub fn seed_file_digests_with(
1990        &self,
1991        load: impl FnOnce() -> Vec<(FileDigestScope, RecordedFileDigest)>,
1992    ) -> usize {
1993        let mut ledger = self.file_digests.lock().unwrap();
1994        let entries = load();
1995        let mut seeded = 0;
1996        for (scope, entry) in entries {
1997            if ledger.len() >= MAX_FILE_DIGEST_ENTRIES {
1998                break;
1999            }
2000            if validate_file_digest_record(&entry).is_err() {
2001                continue;
2002            }
2003            if let std::collections::btree_map::Entry::Vacant(slot) =
2004                ledger.entry((scope, entry.file.path.clone()))
2005            {
2006                slot.insert(entry);
2007                seeded += 1;
2008            }
2009        }
2010        seeded
2011    }
2012
2013    /// Everything the file-digest ledger holds, for a later session to start
2014    /// from.
2015    pub fn file_digests(&self) -> Vec<(FileDigestScope, RecordedFileDigest)> {
2016        self.file_digests
2017            .lock()
2018            .unwrap()
2019            .iter()
2020            .map(|((scope, _), entry)| (*scope, entry.clone()))
2021            .collect()
2022    }
2023
2024    fn find_action_prediction(
2025        &self,
2026        task: &str,
2027        invocation: &CacheDigest,
2028    ) -> Result<AgentResponse> {
2029        validate_task_identity(task)?;
2030        invocation.validate()?;
2031        let (prediction, prefetch) = {
2032            let mut tasks = self.task_actions.lock().unwrap();
2033            let Some(state) = tasks.get_mut(task) else {
2034                return Ok(AgentResponse::ActionPrediction { prediction: None });
2035            };
2036            activate_prediction_adapter(state, invocation)
2037        };
2038        if let Some(prefetch) = prefetch {
2039            self.spawn_prefetch_predictions(prefetch);
2040        }
2041        Ok(AgentResponse::ActionPrediction { prediction })
2042    }
2043
2044    fn record_action_prediction(
2045        &self,
2046        task: &str,
2047        prediction: ActionPrediction,
2048    ) -> Result<AgentResponse> {
2049        validate_task_identity(task)?;
2050        prediction.validate()?;
2051        let mut tasks = self.task_actions.lock().unwrap();
2052        let state = tasks.entry(task.to_string()).or_default();
2053        if !state.predictions.contains_key(&prediction.invocation)
2054            && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
2055        {
2056            bail!("task action manifest contains too many predictions");
2057        }
2058        state
2059            .predictions
2060            .insert(prediction.invocation.clone(), prediction.clone());
2061        state
2062            .pending_predictions
2063            .insert(prediction.invocation.clone(), prediction);
2064        Ok(AgentResponse::ActionPredictionRecorded)
2065    }
2066
2067    fn executable_identity_key(
2068        &self,
2069        executable: PathBuf,
2070        environment: BTreeMap<String, Option<String>>,
2071    ) -> Result<ExecutableIdentityKey> {
2072        // Restricted to the variables that actually select what an identity
2073        // probe reports: the toolchain rustup resolves, the SDK a linker
2074        // driver builds against, and a `-fuse-ld` linker selection. Anything
2075        // else would let one key stand for two different compilers.
2076        if !environment.keys().all(|name| {
2077            matches!(
2078                name.as_str(),
2079                "MBX_FUSE_LD"
2080                    | "RUSTUP_HOME"
2081                    | "RUSTUP_TOOLCHAIN"
2082                    | "SDKROOT"
2083                    | "MACOSX_DEPLOYMENT_TARGET"
2084                    | "LIB"
2085                    | "UCRTVersion"
2086                    | "UniversalCRTSdkDir"
2087                    | "VCToolsInstallDir"
2088                    | "VCToolsVersion"
2089                    | "WindowsSdkDir"
2090                    | "WindowsSDKVersion"
2091            )
2092        }) {
2093            bail!("executable identity contains an unsupported environment variable");
2094        }
2095        Ok(ExecutableIdentityKey {
2096            executable,
2097            environment,
2098        })
2099    }
2100
2101    fn find_executable_identity(
2102        &self,
2103        executable: PathBuf,
2104        environment: BTreeMap<String, Option<String>>,
2105    ) -> Result<AgentResponse> {
2106        let key = self.executable_identity_key(executable, environment)?;
2107        let stdout = self
2108            .executable_identities
2109            .lock()
2110            .unwrap()
2111            .get(&key)
2112            .cloned();
2113        Ok(AgentResponse::ExecutableIdentity { stdout })
2114    }
2115
2116    fn store_executable_identity(
2117        &self,
2118        executable: PathBuf,
2119        environment: BTreeMap<String, Option<String>>,
2120        stdout: Vec<u8>,
2121    ) -> Result<AgentResponse> {
2122        if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
2123            bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
2124        }
2125        let key = self.executable_identity_key(executable, environment)?;
2126        let mut identities = self.executable_identities.lock().unwrap();
2127        let is_new = !identities.contains_key(&key);
2128        let previous_size = identities.get(&key).map_or(0, Vec::len);
2129        if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
2130            bail!("executable identity cache contains too many entries");
2131        }
2132        let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
2133        if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
2134            bail!("executable identity cache contains too many bytes");
2135        }
2136        identities.insert(key, stdout.clone());
2137        Ok(AgentResponse::ExecutableIdentity {
2138            stdout: Some(stdout),
2139        })
2140    }
2141
2142    /// Serve newline-delimited protocol requests on an authenticated session stream.
2143    pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
2144    where
2145        S: AsyncRead + AsyncWrite + Unpin,
2146    {
2147        let (reader, mut writer) = tokio::io::split(stream);
2148        let mut reader = BufReader::new(reader);
2149        let hello = read_request(&mut reader)
2150            .await?
2151            .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
2152        let request: AgentRequest = serde_json::from_str(&hello)?;
2153        match request {
2154            AgentRequest::Hello {
2155                protocol,
2156                client_version,
2157            } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
2158            AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
2159                send_response(
2160                    &mut writer,
2161                    &AgentResponse::Error {
2162                        message: format!(
2163                            "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
2164                        ),
2165                    },
2166                )
2167                .await?;
2168                return Ok(());
2169            }
2170            AgentRequest::Hello { client_version, .. } => {
2171                send_response(
2172                    &mut writer,
2173                    &AgentResponse::Error {
2174                        message: format!(
2175                            "cache client {client_version} does not match agent {}",
2176                            self.version
2177                        ),
2178                    },
2179                )
2180                .await?;
2181                return Ok(());
2182            }
2183            _ => bail!("the first agent request must be hello"),
2184        }
2185        send_response(
2186            &mut writer,
2187            &AgentResponse::Hello {
2188                protocol: AGENT_PROTOCOL_VERSION,
2189                agent_version: self.version.to_string(),
2190            },
2191        )
2192        .await?;
2193
2194        // Tickets accumulate for the life of the connection because a shim
2195        // publishes a compilation's blobs and the action result naming them over
2196        // one connection, in that order.
2197        let mut connection = ConnectionUploads::default();
2198        while let Some(line) = read_request(&mut reader).await? {
2199            let response = match serde_json::from_str(&line) {
2200                Ok(request) => self.respond_on(request, &mut connection).await,
2201                Err(error) => AgentResponse::Error {
2202                    message: format!("invalid agent request: {error}"),
2203                },
2204            };
2205            send_response(&mut writer, &response).await?;
2206        }
2207        Ok(())
2208    }
2209}
2210
2211/// Read one newline-delimited request, refusing one that grows past the cap.
2212///
2213/// Any process running as this user can open the session socket, so a request
2214/// that never terminates its line must not be able to grow the agent's memory
2215/// without bound.
2216async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
2217where
2218    R: AsyncBufRead + Unpin,
2219{
2220    let mut line = Vec::new();
2221    loop {
2222        let available = reader.fill_buf().await?;
2223        if available.is_empty() {
2224            break;
2225        }
2226        let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
2227            Some(index) => (index, true),
2228            None => (available.len(), false),
2229        };
2230        if line.len() + consumed > MAX_REQUEST_BYTES {
2231            bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
2232        }
2233        line.extend_from_slice(&available[..consumed]);
2234        // The newline itself is consumed but never kept.
2235        reader.consume(consumed + usize::from(complete));
2236        if complete {
2237            return Ok(Some(String::from_utf8(line)?));
2238        }
2239    }
2240    if line.is_empty() {
2241        Ok(None)
2242    } else {
2243        Ok(Some(String::from_utf8(line)?))
2244    }
2245}
2246
2247async fn send_response(
2248    writer: &mut (impl AsyncWrite + Unpin),
2249    response: &AgentResponse,
2250) -> Result<()> {
2251    let mut encoded = serde_json::to_vec(response)?;
2252    encoded.push(b'\n');
2253    writer.write_all(&encoded).await?;
2254    writer.flush().await?;
2255    Ok(())
2256}
2257
2258#[cfg(test)]
2259#[path = "agent_tests.rs"]
2260mod tests;