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