Session summary

default

scored

afi / afi · cli reviewed proxy at 9b51f95e with Default profile / Z.AI · glm 5.3 high on afi 0.30.0

Measurements

Unavailable values retain their exported state and reason

Quality

Recall 40%
Model-judged precision N/A
F1 N/A
Tier 1 recall 33.3%
Tier 2 recall 57.1%
Tier 3 recall 33.3%
Tier 4 recall 25%
Security recall 50%
Defect recall 30.8%
Maintainability recall 0%
Performance recall 100%
Median anchor distance 0
Worst anchor distance 3
Refusals a judge overturned Not recorded

Review outcomes

Distinct matched defects 8
Defects missed 12
Findings 21
Unkeyed findings 13
Intended findings 0
Carried findings Not recorded

Cost, usage, and speed

Total bill $0.1832
Review bill $0.1832
Judge bill N/A
Tokens 74102
Duration 4m 13s

Execution and identity

Lifecycle, reviewer, build, settings, and normalized execution limits

Lifecycle
completed
Lifecycle reason
Not recorded
Started
Finished
Reviewer
afi
Reviewer tool
afi · cli
Configuration
default / 5ef3f0fa5235
Configuration ID
config-6c16b95b6f42b786
Build
afi 0.30.0 / b0f313c0b59a66ecc7612396dc8db0ea5da13a7a
Comparison
comparison-1a363c7589fb1425
Build ID
build-4ceb7cbd44e9619b
Cohort ID
cohort-fdab2adfb8a4bd39
Harness
bench 1 / 10b3b2068100b7ba429855e062500eaf83b90120 dirty
Adapter
afi 1 / sha256:a1a935298956020ee6d767a756847abb4c00694c88c2eb80d454886ebc4acf8c
Exit code
0
Wall time
253
Deadline exceeded
No
Budget
Not recorded
Billing limit
$2.00

Usage

Normalized reviewer-tool usage projection

Input tokens
62546
Output tokens
11556
Cached input
12096
Reasoning tokens
9469
Requests
3
Models counted
Not recorded

Judge

Normalized assessment summary

Judge data is N/A for this run

Findings

Review text is escaped; normalized decisions follow each finding

Finding f1

bug

says links counted at link size, but `fs::metadata` follows symlinks and returns the *target's* size, not the link's size. `fs::symlink_metadata` returns the link's own size. So the code contradicts its comment: a symlinked blob is counted at the target's size. Consequence: on a store where blobs are hardlinked (hardlinks don't matter — metadata is same file, fine) or symlinked from elsewhere, `held` counts target bytes, possibly double-counting... Actually if blobs are symlinked into the store from a shared volume, each blob's metadata is the target's length — the total counts real bytes, which is arguably what you want for space, but the comment says the opposite ("counted at the size of the link"). The consequence per their rationale: the total should be "the number of bytes this directory is answerable for" — with `fs::metadata`, symlink targets' sizes are counted, which for a moved store linked (hardlinked, likely — moving between volumes uses hardlinks, not symlinks; hardlink metadata len is the file size, so fine). If they meant symlink, metadata follows and gives file size. Hmm: for hardlinks, `fs::metadata` len == file size, correct. The comment says "A link is counted at the size of the link" — for a hardlink, size of link == size of file. So comment makes sense for hardlinks. For symlinks it follows. Is this a defect? If it followed a symlink out of the store... they'd delete a file outside the store? `fs::remove_file` on a symlink removes the symlink only, not the target — safe. And `meta.is_dir()` following a symlink into a directory outside the store would recurse outside the blob dir — collect would walk outside the blob directory via a symlinked dir! `fs::metadata` follows symlinks, so a symlink to a directory makes `meta.is_dir()` true and it recurses into it. Consequence: a symlink in the blobs dir pointing elsewhere causes the sweep to walk (and potentially unlink files in) a directory outside the blob store. But would an attacker plant a symlink? The blob store is written only by the proxy... A malicious upstream can't create symlinks (writes bytes). Local operator could. `BlobStore::writer`/commit never creates symlinks. Edge case. I might report as low: `collect` follows symlinks, so a symlinked directory under blobs is walked (and a symlink to a file counts at target size, contradicting the comment). This is a genuine TOCTOU-ish/symlink-traversal defect in code whose only job is filesystem walking. Severity low/medium. Given the deploy described ("store moved between volumes often has blobs linked into it") — if they symlink a directory (e.g. `blobs/sha256/ab/cd` → old store), the sweep would recurse into the old store and delete blobs there too — outside `CAIRN_BLOB_DIR`. That's concrete: line 164-168. Medium maybe. I'd rate yellow (medium) or blue (low). I'll say for the symlinked-directory traversal (deletes outside the blob dir), since their own comment anticipates linked stores. Hmm, but is symlinking a directory realistic? Their comment says "blobs linked into it rather than copied" — typical is hardlinks for files. With hardlinks, no traversal. With `cp -al` you get hardlinks. With symlinks (`ln -s`), `fs::metadata` follows. The defect: comment claims one behavior (size of the link), code does another (size of target); and directory symlinks are traversed, letting a sweep unlink files outside the blob directory. I'll report as one finding, medium or low. Let me be precise: lines 160-168. 10. **`forget` race**: index entries removed for digests that were swept; but between `referenced()` and `forget`, a fetch could have just linked an index entry for a digest the sweep decided was unreferenced? No — if the sweep decided unreferenced, that means at index-read time no entry existed; then a fetch concurrently commits blob + links entry. The blob is deleted (unreferenced at scan time... wait order: index read first, then blob walk. Fetch commit blob at T1 after blob walk already passed that leaf? If blob walk happens after commit, blob seen; index entry written at T2 after index read; sweep deletes blob, then `forget` — index map was read before T2, so `gone` hex not in map, entry not removed. Left: stale index entry pointing at deleted blob — handled by resolve (refetch). The request itself fails at open_blob with 500 though. That's finding 3. 11. **admin.rs:73 `reclaim` is sync in async handler** — same as finding 4; the blocking walk in an async fn without spawn_blocking. Report at admin.rs:76 or sweep.rs:90. I'll report once, pointing at sweep.rs:90 (the function) or admin.rs:76 (the call in async context). Both lines. Policy says name both lines. I'll cite sweep.rs:90 with mention of admin.rs:76 and main.rs:116.

bug
Category: bug Confidence: Not recorded

sweep.rs:160-163

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f2

bug

(COUNTERS order) or 88-97 (values order). The changed lines — diff shows both. I'll cite metrics.rs:94 (where values order diverges from COUNTERS). Actually cite the `values()` return lines 94-95. Severity: high? The metrics are wrong — blob count and byte count swapped. It misleads operators but doesn't break serving. high. 13. **`held` doesn't include partials**: `Reclaimed.held` is "Bytes of blob the store held when the sweep started" — blobs only; partials counted separately. Doc says fine. 14. **Sweep doesn't count `remaining >= self.max_bytes`**: when `remaining == max_bytes`, it removes — "back within ceiling" means ≤? `remaining >= max_bytes` removes when exactly at ceiling. Minor semantic: at exactly ceiling it evicts. The docs say "until the store is back within CAIRN_CACHE_MAX_BYTES" — within could include equal. Edge, off-by-one-ish. Not worth reporting.

bug
Category: bug Confidence: Not recorded

metrics.rs:51-57

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f3

bug

if remove fails (error), the code still does `remaining -= size`, `bytes += size`, `removed += 1`, `gone.push(hex)` — and `forget` then deletes the index entry for a blob that is still on disk! That's a real bug: a failed unlink (e.g. EACCES, or on Windows-ish, or a race where the file is open) causes the index entry to be removed, making the blob unreachable garbage that future sweeps will then see as unreferenced and delete. Wait — future sweep: blob unreferenced → remove (still failing?) counts it again. Hmm, but more importantly `forget` deletes the index entry even though the blob removal failed. The consequence: the cache entry is dropped while the bytes remain — a subsequent request refetches (fine, by design), but the bytes leak as unreferenced... until the next sweep removes them as unreferenced. So the leak self-heals next sweep. But also `bytes`/`removed` metrics overstate reclaimed bytes. The `forget` deleting index for a still-present blob: blob then looks unreferenced forever → next sweep removes it. So consequence: metrics wrong, index entry lost while blob exists (brief inconsistency, next sweep cleans). Medium-low. But is it a defect worth flagging? The comment says failure is "usually a blob that has already gone" — in that case fine. When failure is other (permissions), the code proceeds as if removed: double-counts bytes, removes index entries for a blob still present, and `remaining` decreases as if freed so the ceiling may not actually be enforced (disk still full!). That's the real consequence: on persistent unlink failures, the sweep reports reclaim and reduces `remaining` as if space was freed, so the store can stay over the ceiling while sweep reports success. Concrete: sweep.rs:125-137. Severity medium . Actually the more common failure is NotFound (already gone) where treating as removed is fine except the size accounting was already stale (the blob counted in `held` because the walk saw it). Eh — NotFound means it went between walk and unlink; counting as removed is harmless-ish. But EACCES etc: real bug. I'd report .

bug
Category: bug Confidence: Not recorded

sweep.rs:126-137

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f4

bug

is factually wrong ("a fetch that is not coming back") — there's no coordination with in-flight writers. Also note the store pre-existing comment at store.rs:213-215: "a fetch that dies halfway - a killed process ... leaves a file in incoming" — partials from a killed process have no owner. But live processes' partials are owned. The sweep cannot distinguish. Yes.

bug
Category: bug Confidence: Not recorded

sweep.rs:220-223

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f5

bug

comment shows the authors considered external deleters but not their own sweeper. I'd say high or medium. The failure: request fails with 500 after having done all the work; client (package manager) will likely error. Probability low per request but nonzero on every sweep. I'll rate medium. Hmm. Between 2, 3 — both are the same class (sweep vs concurrent fetch races) but distinct windows/mechanisms. I'll report separately: one for partials (clear_partials vs BlobWriter), one for blob walk order (referenced-before-collect vs commit-then-link). Actually, for finding 3 there's an even simpler statement: the sweep reads the index before walking blobs, so a blob committed after the index read but before the blob walk appears unreferenced and is deleted — including the one a request in flight just committed, whose `open_blob` then fails (cache.rs:90) and the request 500s. And also: even for fully completed entries? No — entry written before index read → referenced. Only the in-flight window. Wait, another subtlety for finding 3: the fetch order in cache.rs `fetch()`: commit blob (T1) → register_version (network, T2) → link index (T3). So the blob is unreferenced for T1..T3, which includes a registry roundtrip — easily 50-500ms. If the sweep's `referenced()` read happens in that window and the blob walk sees the blob (walk happens after), deletion. `forget` won't remove the entry (not in map), so the entry gets written at T3 pointing at a deleted blob → next request refetches (fine). But the current request 500s at open_blob. Consequence concrete. 18. **`.env.example` comment says 32 GiB and value 34359738368** — 32 GiB = 34359738368 bytes. Correct. 19. **docs/operations.md says sweep takes "the same bearer token as the purge route"** — true. 20. **Route method/path**: `POST /v1/admin/cache/sweep` and purge is `DELETE /v1/admin/cache/{digest}`. Conflict? `/v1/admin/cache/sweep` POST vs `/v1/admin/cache/{digest}` DELETE — different methods, no conflict in axum routing (path match is per-method). A POST to `/v1/admin/cache/sweep`... matches only the sweep route. A DELETE to `/v1/admin/cache/sweep` → matches purge route with digest="sweep" → Digest::parse fails → 400. Fine. 21. **`SweepQuery.dry_run` bool deserialization**: `?dry_run=true` → true. `?dry_run=1`? serde bool from "1"? With serde_urlencoded... axum Query uses serde_urlencoded; bool from "1" — serde_urlencoded parses via `FromStr`? Actually serde_urlencoded uses `serde` deserializer where bool expects "true"/"false". `?dry_run=1` would 400. Minor, not a defect per se. Skip. 22. **Interval task not aborted on shutdown**: spawn_sweeps task runs forever; on graceful shutdown, axum serve returns and main exits — fine, task dropped. Not a defect. The doc says "Not part of the graceful shutdown either" — deliberate. But note: tokio runtime shuts down when main returns; the sweep task is just dropped mid-unlink — they say that's fine. OK.

bug
Category: bug Confidence: Not recorded

cache.rs:90-96

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f6

bug

says the mutex is "Held for the whole of a sweep, so this module's one-at-a-time promise is something the type keeps rather than something every caller remembers" — but `sweep()` bypasses it, and… says the mutex is "Held for the whole of a sweep, so this module's one-at-a-time promise is something the type keeps rather than something every caller remembers" — but `sweep()` bypasses it, and the admin route calls `sweep()` directly. Two concurrent sweeps over one directory: exactly the scenario the module doc (lines 10-12) warns about ("Two of them over one directory would each decide what to remove from a total the other is already changing"). This is a concrete, definite defect: admin.rs:76 calls `sweeper.sweep()` which never locks `running`; sweep.rs:90 `sweep` is `pub` and lock-free while `run` locks. High severity . Wait — but the sync sweep can't await a tokio Mutex... that's presumably why they split: the route is sync. But they could have made the handler async and used `run()`-style locking or a sync `std::sync::Mutex`, or `try_lock`. As written: concurrent interval sweep + on-demand sweep both walk and delete. Also even two concurrent on-demand POSTs both bypass the lock. This is a strong finding. Let me double check admin.rs:76: `let reclaimed = app.sweeper.sweep(dry_run);` — yes, sync `sweep`, no mutex. And sweep.rs:82-85 `run()` takes `self.running.lock().await` then calls `self.sweep(false)`. `sweep` itself doesn't touch `running`. Confirmed. 24. **Interval sweep + on-demand dry_run interplay**: dry run doesn't remove, but it does call `clear_partials(dir, dry_run=true)` — no removal. OK.

bug
Category: bug Confidence: Not recorded

sweep.rs:65-67

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f7

bug

app.sweeper.run().await` blocks the tokio worker with sync fs walk — part of finding 4. The module doc justifies sync std fs, but running it on the async runtime without spawn_blocking blocks that worker thread for the entire walk. With rt-multi-thread default worker count = cores; one worker blocked is tolerable-ish, but the admin route also blocks a worker per request. And the walk reads every index entry file fully (`fs::read` in referenced()) — index can be huge. Blocking a worker for minutes. performance. Also blocking in the interval loop: while the sweep runs (say 30s), the tick awaits... fine. 26. **`forget` deletes index entries for blobs removed by ceiling-eviction** — intended (blob gone, entry stale). Fine. But it also deletes entries for blobs whose removal failed (finding 15) and for the "already gone" NotFound case — fine. But wait, there's another `forget` issue: index entries for digests removed by an *admin purge* between the index read and blob walk: blob gone → not in candidates → no action. Fine. 27. **`gone` includes hex of files that failed to parse file_name?** — `continue` skips them. Fine. 28. **Reclaimed.partials counted even in dry run as "removed"**: in dry_run, `clear_partials` counts files it *would* remove and labels them `partials` — the struct field says "Abandoned partial writes it removed" — in dry run it didn't remove. Minor semantics; the JSON says "partials" which in dry-run means "would remove". Consistent with dry-run reporting purpose. Not a defect worth flagging? The docs say dry run "reports what one would reclaim". OK. 29. **config validation: `cache_min_age` zero allowed** — skip as discussed. Hmm, actually let me reconsider: is there an argument it's a bug? `.env.example` documents it as grace period; zero means "no grace", and the eviction predicate `age >= min_age` with min_age=0 evicts new blobs under pressure — could evict a blob whose request is mid-flight after commit (same as race but by design). The code validates the other two new settings but not this one. Inconsistent validation. I'll report as low 🔵 maintainability: no zero-check for CAIRN_CACHE_MIN_AGE while its siblings are validated, and zero turns the grace period off so a blob fetched seconds ago can be evicted. Borderline — but the policy says don't pad. Hmm. I think there's a reasonable case. I'll include as 🔵 low, maybe. Let me decide at the end based on confidence. I'd rather include the strongest 6-7 findings. I'll include it as blue. 30. **metrics.rs comment line 31: "stops a fifth counter"** — stale comment (now six counters) — trivial doc nit, and ironically the order bug proves the comment's point. The line "Keeping the two side by side is what stops a fifth counter from being rendered under a fourth one's name" — it says "a fifth ... under a fourth one's name" — with six counters now the phrasing is stale but harmless. Skip. 31. **The COUNTERS/values order bug** — double-check carefully: COUNTERS: index 4 = ("cairn_proxy_sweep_blobs_removed_total", ...), index 5 = ("cairn_proxy_sweep_bytes_reclaimed_total", ...). values(): index 4 = sweep_bytes_reclaimed.load, index 5 = sweep_blobs_removed.load. render zips → HELP/TYPE/name from COUNTERS, value from values. So `cairn_proxy_sweep_blobs_removed_total` renders the BYTES value and `cairn_proxy_sweep_bytes_reclaimed_total` renders the BLOB COUNT. Confirmed swapped. Report 🟠 (or 🔴?). It's a monitoring correctness bug — wrong data, no service breakage. 🟠 high. Which line to cite? The defect spans COUNTERS (metrics.rs:51-57) and values (metrics.rs:94-95). I'll cite metrics.rs:94. 32. **`.env.example` value**: `CAIRN_CACHE_MAX_BYTES=34359738368` = 32 GiB ✓. 33. **`collect` on index dir in `referenced()` also uses Candidate with age — fine.** 34. **Potential unbounded `gone` Vec memory** — millions of blobs → gone could be huge (every unreferenced blob). BTreeMap index too. Memory blowup on large stores: `referenced` loads all index entries (file contents) into a BTreeMap<String, Vec<PathBuf>> — for a monorepo dependency cache with hundreds of thousands of entries, that's significant but bounded by store size. The doc says 32GiB ceiling; index entries maybe millions. Memory: each entry path ~100 bytes + digest 64 + vec overhead. 1M entries ~ 200-300MB. Hmm, borderline. Not clearly a defect; skip or low. Skip. 35. **`spawn_sweeps` first tick immediate (finding 7)**: tokio::time::interval "The first tick completes immediately". The comment claims "The fi

bug
Category: bug Confidence: Not recorded

**main.rs:116

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f8

bug

vs cache.rs:151-163) — request 500s. Maybe ? It's the same class as #2. I'll make it medium (narrower window than #2? Actually #2's window is the entire duration of any in-flight fetch, which is large. #3's window is commit→link which includes a registry roundtrip — also hundreds of ms per miss. Both plausible. I'll keep #4 as .) - 🟡 medium: - race #4 above (sweep.rs:92 ordering).

bug
Category: bug Confidence: Not recorded

(sweep.rs:92-95

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f9

bug

. Actually let me reconsider severity: on ENOENT (the "usual" case) treating as removed is right. On other errors (EACCES/EIO), remaining decreases without space freed → repeated sweeps keep "succeeding" while disk stays full; index entries deleted → blobs become unreferenced garbage (cleaned next sweep, again failing...). If unlink persistently fails, blobs become permanently unreferenced (every sweep retries, fails, counts them) and `forget` already removed the index entries — so cache misses forever while bytes remain. That's a real consequence. .

bug
Category: bug Confidence: Not recorded

(sweep.rs:125-137

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f10

bug

via admin.rs:76 and main.rs:116) — performance.

bug
Category: bug Confidence: Not recorded

(sweep.rs:90

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f11

bug

fs::metadata` follows symlinks, contradicting the comment; a directory symlink under blobs makes the sweep walk and unlink outside the blob dir. or . The comment explicitly anticipates linked stores; if those are symlinked, the sweep deletes files outside `CAIRN_BLOB_DIR`. I'll say . Hmm — but hardlinks (most likely for "moved between volumes") are fine, and `fs::metadata` on a hardlink gives file size. The comment says "A link is counted at the size of the link and not of whatever it points at" — for hardlink, size of link = size of file, comment's claim about "whatever it points at" doesn't apply (hardlinks don't point). So the comment really describes symlinks, and the code does the opposite for symlinks (follows → target size). And traversal: symlink-to-dir recurses outside. Concrete mismatch. medium? Given likelihood, low? The stated scenario (store moved between volumes with links) is in their own comment, so the mismatch is real in their anticipated scenario. For file-level symlinks: counted at target size — if the target is inside the same store (relocated blobs), counting target size is arguably more correct for space, but then `held` counts what... eh. And dir symlink → traversal outside blob dir + deletion outside. I'll go medium. Hmm, actually let me reconsider: is following symlink into a directory then `remove_file` on entries there dangerous? It deletes files in the symlinked directory — if operator symlinked `blobs/sha256` → `/mnt/oldstore/blobs/sha256`, sweep walks /mnt/oldstore and unlinks blobs there. "Outside the blob dir" — but operator symlinked deliberately, so maybe desired. The bigger defect is the comment/code mismatch and unbounded traversal. I'll rate low honestly... no wait. Let me think about who's affected: an operator who follows the codebase's own guidance (comment says linked stores are supported). The sweep would then count sizes wrong (following instead of link size) and possibly evict based on wrong totals. For hardlinks (the common case, `cp -al`), `fs::metadata` is correct (same inode, len = file size) and the comment's "size of the link" = file size = correct too. So hardlink case fine. Symlink case: mismatch + traversal. Symlinking a whole tree across volumes is plausible (`ln -s`). Then `held` = target sizes (could be same store, fine-ish), traversal walks the old store, and `remove_file` on old-store blobs — deleting from the old store, which is... the same blobs. Actually if it's a symlink of the whole blob tree to the old location, deleting through it deletes the same files — no harm beyond design. Harm case: symlink to something else entirely (not a blob store) — sweep unlinks arbitrary files there. E.g., operator symlinks one leaf dir to shared storage used by others. Realistic? Meh. I'll rate low: `fs::metadata` follows symlinks so the "counted at the size of the link" guarantee in the comment doesn't hold, and a symlinked directory is traversed (and unlinked from) even outside `CAIRN_BLOB_DIR`. Use `symlink_metadata`/`entry.file_type()`. Low-blue. - 🔵 low: - first tick fires immediately contradicting comment (main.rs:101/115).

bug
Category: bug Confidence: Not recorded

(sweep.rs:164-168

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f12

bug

explicitly claims the type keeps the promise "rather than something every caller remembers" — and the admin caller bypasses it. So yes, the invariant is broken by design of the route. Definitely report. Severity: consequence is two concurrent sweeps → both compute totals from stale walks → over-deletion below ceiling + duplicate counting; also concurrent with itself on repeated POSTs. Given the module itself documents why that's harmful, high. Also note: even `run()` and `sweep()` interplay: interval task holds lock through whole sweep including sync walk — while holding the tokio Mutex across a long blocking walk, an on-demand... `run` isn't called by admin. OK. Let me also double check finding "clear_partials deletes in-flight fetches": `incoming` contains `<pid>-<seq>` temp files. Sweep runs in the same process as in-flight writers. No age check, no exclusion of own pid. Unconditional `fs::remove_file` on every entry. In-flight fetch → its temp file unlinked → `commit()`'s rename fails ENOENT → Error::Storage → 500 to client. Also for `Drop` case irrelevant. Also multi-process: another proxy's in-flight partial — same problem (their comment acknowledges "another proxy over the same directory" for blobs but not partials). Solid 🟠. Wait, actually — one more check on unlink semantics on Linux: removing the temp file while writer holds it open: writer continues fine, then rename(temp→target) fails because temp path no longer exists → commit error. Yes. And could the sweep instead remove the file *after* the fetch completes? The window is any fetch in flight during the sweep's `clear_partials` — with 15-min sweeps and continuous CI fetches, this fires regularly. Also `clear_partials` runs on every sweep including dry-run? Dry-run doesn't remove. But the interval sweep (non-dry) does. High. Also the **admin sweep route concurrency**: two POSTs → two `sweep()` calls concurrently → both walk. Covered. Now the ordering-race finding (#4): is it worth reporting separately given clear_partials is already reported? Yes, distinct mechanism/lines. Let me also reconsider whether `remaining >= self.max_bytes` uses `held` which excludes partials and index — the ceiling is on blobs only, per config doc "The most the blob store may hold" — blobs only per Reclaimed doc "Bytes of blob". Index entries also consume disk but not counted. Ceiling is about blob bytes; index/index-entry bytes uncounted — minor, skip. Final findings list:

bug
Category: bug Confidence: Not recorded

(sweep.rs:65-67

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)
Finding f13

bug

— COUNTERS lists blobs_removed then bytes_reclaimed, values() reads bytes then blobs → swapped; the two new counters render each other's totals.

bug
Category: bug Confidence: Not recorded

services/proxy/src/metrics.rs:94

Evidence quote

Not recorded

Suggested fix

Not recorded

matchedDefect 13 (locality)
Finding f14

bug

— clear_partials unlinks every file in incoming unconditionally; a fetch in flight in this process loses its temp file and commit()'s rename fails, 500ing the request (every sweep does this to any… — clear_partials unlinks every file in incoming unconditionally; a fetch in flight in this process loses its temp file and commit()'s rename fails, 500ing the request (every sweep does this to any concurrent fetch).

bug
Category: bug Confidence: Not recorded

services/proxy/src/sweep.rs:232

Evidence quote

Not recorded

Suggested fix

Not recorded

matchedDefect 17 (locality)
Finding f15

bug

— on-demand sweep calls Sweeper::sweep() which never takes the running mutex (sweep.rs:90); concurrent with the interval sweep or another POST, two sweeps walk one store, breaking the one-at-a-time… — on-demand sweep calls Sweeper::sweep() which never takes the running mutex (sweep.rs:90); concurrent with the interval sweep or another POST, two sweeps walk one store, breaking the one-at-a-time invariant the module documents.

bug
Category: bug Confidence: Not recorded

services/proxy/src/routes/admin.rs:76

Evidence quote

Not recorded

Suggested fix

Not recorded

matchedDefect 6 (locality)
Finding f16

bug

— index is read before blobs are walked; a blob committed between the two appears unreferenced and is deleted mid-request, so cache.rs's open_blob fails and the request 500s.

bug
Category: bug Confidence: Not recorded

services/proxy/src/sweep.rs:92

Evidence quote

Not recorded

Suggested fix

Not recorded

matchedDefect 9 (locality)
Finding f17

bug

— a failed unlink (e.g. EACCES) is treated as a removal: remaining/bytes/removed advance and forget() drops the index entries, so the sweep reports reclaimed space it did not free and orphans blobs the index no longer names.

bug
Category: bug Confidence: Not recorded

services/proxy/src/sweep.rs:134

Evidence quote

Not recorded

Suggested fix

Not recorded

matchedDefect 2 (locality)
Finding f18

performance

— the synchronous full-tree walk runs inline on a tokio worker (admin.rs:76, main.rs:116) without spawn_blocking; a large store blocks a worker thread for the walk's duration.

performance
Category: performance Confidence: Not recorded

services/proxy/src/sweep.rs:90

Evidence quote

Not recorded

Suggested fix

Not recorded

matchedDefect 11 (locality)
Finding f19

bug

— fs::metadata follows symlinks, contradicting the adjacent comment ("counted at the size of the link"), and a symlinked directory is traversed and unlinked from even outside CAIRN_BLOB_DIR.

bug
Category: bug Confidence: Not recorded

services/proxy/src/sweep.rs:164

Evidence quote

Not recorded

Suggested fix

Not recorded

matchedDefect 4 (locality)
Finding f20

maintainability

— tokio interval's first tick completes immediately; the comment's claim that the first sweep is one interval away is false, so a fresh proxy sweeps at startup.

maintainability
Category: maintainability Confidence: Not recorded

services/proxy/src/main.rs:115

Evidence quote

Not recorded

Suggested fix

Not recorded

matchedDefect 8 (locality)
Finding f21

bug

(sweep_interval check) missing the min_age sibling. Or cite 107. I'll cite services/proxy/src/config.rs:131 — where sweep_interval is validated but cache_min_age is not. Hmm — is this a defect? Borderline. "Do not pad." The consequence: CAIRN_CACHE_MIN_AGE=0ms accepted → no grace → blob evictable the instant it lands. Given the docs emphasize the grace period as protection for pipeline jobs, accepting zero silently... it's a deliberate-ability vs oversight question. I'll include it as blue — the inconsistency is pointable. Hmm, let me trim: 9 findings, most concrete. Reviewers are compared; quality over quantity but each must be pointable. All are pointable. Keep. Double-check line numbers: - metrics.rs: values() return block lines 88-97; the swap is at 94-95. Cite 94. - sweep.rs:232 is the remove_file in clear_partials. ✓ (function starts 224). - admin.rs:76 `let reclaimed = app.sweeper.sweep(dry_run);` ✓. - sweep.rs:92 `let index = referenced(...)` before 95 collect. ✓ Cite 92. - sweep.rs:134 `remaining = remaining.saturating_sub(candidate.size);` after the error-handled remove at 126. Cite 134 (accounting proceeds regardless of remove outcome at 126). ✓ - sweep.rs:90 `pub fn sweep(&self, dry_run: bool)` — for the blocking-walk finding cite 90. ✓ - sweep.rs:164 `let Ok(meta) = fs::metadata(&path)` ✓. - main.rs:115 `ticks.tick().await;` ✓ (comment at 101-102). Cite 115? The false claim is the comment at 101; the behavior at 115. Policy: name both. One line per finding. I'll cite main.rs:115 and mention the comment. Format only allows one line — fine, cite 115. - config.rs:131-133 for missing min_age check. Cite 131. Severity sanity: should metrics swap be 🔴? No data loss/service outage — but it silently corrupts the primary operational signal the docs tell operators to alert on. 🟠 fits. Should lock bypass be 🟠? Consequence: over-eviction (extra fetches) and double-reported metrics; no permanent loss. 🟠 ok since it violates a documented core invariant and is trivially triggered (POST twice). clear_partials race: user-visible 500s during sweeps — 🟠. Now write up per format: one line each. Also brief context intro is fine? The format policy says report each defect on one line. I'll keep prose minimal — maybe a short lead then the lines. Prior guidance says write up findings; the format is the counted part. I'll output just the lines, maybe one intro sentence. Safer: just the lines. Order by severity.

bug
Category: bug Confidence: Not recorded

config.rs:131

Evidence quote

Not recorded

Suggested fix

Not recorded

rejectedNo keyed defect (scorer)

Source artifacts

Open a row to inspect or download evidence from this atomically published local run

findings present

Open verified source artifact

Open this path at the exported commit

The artifact is present; its content is available through the verified source link

meta present

Open verified source artifact

Open this path at the exported commit

The artifact is present; its content is available through the verified source link

record present

Open verified source artifact

Open this path at the exported commit

The artifact is present; its content is available through the verified source link

result present

Open verified source artifact

Open this path at the exported commit

The artifact is present; its content is available through the verified source link

spend present

Open verified source artifact

Open this path at the exported commit

The artifact is present; its content is available through the verified source link

stderr present

Open verified source artifact

Open this path at the exported commit

The artifact is present; its content is available through the verified source link

summary present

Open verified source artifact

Open this path at the exported commit

The artifact is present; its content is available through the verified source link

traffic present

Open verified source artifact

Open this path at the exported commit

The artifact is present; its content is available through the verified source link

benchee benchee-dashboard-1 built from 10f4ec58 Static benchmark evidence ·