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