{
  "sha": "9b51f95ef609a219e211e37b082cd2e6913190e0",
  "model": "anthropic/claude-opus-5",
  "effort": "high",
  "found_per_pass": {
    "agent": 10
  },
  "findings": [
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 140,
      "end_line": 140,
      "label": "bug",
      "severity": "critical",
      "explanation": "forget(&index, &gone)` runs unconditionally, and `gone` is filled at line 137 even when `dry_run` is true — so `POST /v1/admin/cache/sweep?dry_run=true` deletes every index entry naming a blob the…\n\nforget(&index, &gone)` runs unconditionally, and `gone` is filled at line 137 even when `dry_run` is true — so `POST /v1/admin/cache/sweep?dry_run=true` deletes every index entry naming a blob the sweep *would* have removed (`fs::remove_file(path)` at line 252 has no dry-run guard). The route documented as \"reports what one would reclaim without removing anything\" (docs/operations.md, admin.rs:87) silently unlinks the name index, turning the next build's hits into a full refetch storm against upstreams.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 232,
      "end_line": 232,
      "label": "bug",
      "severity": "critical",
      "explanation": "clear_partials` unlinks *every* file in `incoming` with no age or liveness test, but that directory holds the in-flight temporary of each running fetch (`BlobStore::temp_path`, store.rs:204) and of…\n\nclear_partials` unlinks *every* file in `incoming` with no age or liveness test, but that directory holds the in-flight temporary of each running fetch (`BlobStore::temp_path`, store.rs:204) and of each index write (store.rs:147). A sweep — automatic every `CAIRN_SWEEP_INTERVAL`, 15m by default — deletes the temp of a download that is proceeding fine; the writer's `fs::rename(&temp, &target)` at store.rs:259 then fails with ENOENT and the client gets a 500 on a perfectly good artifact. The doc comment's premise (\"a fetch that is not coming back\") is untrue for anything currently streaming.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 102,
      "end_line": 102,
      "label": "bug",
      "severity": "high",
      "explanation": "sort_by_key(|candidate| candidate.age)` sorts *ascending*, i.e. newest first, the opposite of the comment above it and of the eviction policy documented in config.rs:63.\n\nCombined with the `age >= min_age` gate at line 120, the sweep removes the youngest blobs that have just cleared the grace period and keeps the oldest, so the cache evicts exactly what a pipeline is still using. Needs `sort_by_key(|c| std::cmp::Reverse(c.age))`.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/metrics.rs",
      "start_line": 94,
      "end_line": 94,
      "label": "bug",
      "severity": "high",
      "explanation": "values()` returns `sweep_bytes_reclaimed` in slot 4 and `sweep_blobs_removed` in slot 5, but `COUNTERS` (lines 50-57) names slot 4 `cairn_proxy_sweep_blobs_removed_total` and slot 5…\n\nvalues()` returns `sweep_bytes_reclaimed` in slot 4 and `sweep_blobs_removed` in slot 5, but `COUNTERS` (lines 50-57) names slot 4 `cairn_proxy_sweep_blobs_removed_total` and slot 5 `..._bytes_reclaimed_total`. The two sweep counters are exported under each other's names, so the metric docs/operations.md tells operators to alert on reports a blob count, not bytes. Swap the two loads.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/routes/admin.rs",
      "start_line": 76,
      "end_line": 76,
      "label": "performance",
      "severity": "high",
      "explanation": "reclaim` calls the synchronous `Sweeper::sweep` directly on the runtime thread, as does `Sweeper::run` at sweep.rs:84 inside the spawned task.\n\nThe module doc (sweep.rs:14-18) deliberately uses blocking `std::fs`, and routes/mod.rs:125 admits a sweep \"legitimately takes longer than ten seconds\" — that is a tokio worker blocked for the whole walk, stalling every download futures scheduled on it. Both call sites need `tokio::task::spawn_blocking`.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/routes/admin.rs",
      "start_line": 76,
      "end_line": 76,
      "label": "bug",
      "severity": "high",
      "explanation": "the HTTP route bypasses the `running` mutex by calling `sweep()` instead of `run()`, so the \"one sweep at a time\" invariant the lock exists to keep (sweep.rs:10-12, 65-67) does not hold for the…\n\nthe HTTP route bypasses the `running` mutex by calling `sweep()` instead of `run()`, so the \"one sweep at a time\" invariant the lock exists to keep (sweep.rs:10-12, 65-67) does not hold for the on-demand route. Two concurrent admin sweeps, or one racing the interval sweep, each compute `remaining` from a total the other is already shrinking and together take the store far below the ceiling.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 119,
      "end_line": 119,
      "label": "bug",
      "severity": "medium",
      "explanation": "an unreferenced blob is removed with no grace period at all, but a miss commits the blob (cache.rs:151) before it writes the index entry (cache.rs:163) — during that window nothing points at it.\n\nA sweep landing there deletes the blob the request just stored; if it lands between the commit and `open_blob` (cache.rs:90) the request fails with the \"already missing from the store\" 500. The unreferenced branch should still respect `min_age`.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 164,
      "end_line": 164,
      "label": "bug",
      "severity": "medium",
      "explanation": "fs::metadata` follows symlinks, contradicting the comment directly above it (a symlink is counted at its target's size, not the link's) and, worse, making `collect` recurse into symlinked…\n\nfs::metadata` follows symlinks, contradicting the comment directly above it (a symlink is counted at its target's size, not the link's) and, worse, making `collect` recurse into symlinked directories — a link pointing at an ancestor inside `CAIRN_BLOB_DIR` recurses until the stack overflows. Use `fs::symlink_metadata`.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 134,
      "end_line": 134,
      "label": "bug",
      "severity": "low",
      "explanation": "a failed `fs::remove_file` at line 126 only logs, then falls through to count the blob in `removed`/`bytes` and push it into `gone`.\n\nSpace that was never reclaimed is reported to the operator and added to `cairn_proxy_sweep_bytes_reclaimed_total`, and its index entries are dropped though the blob is still there — `clear_partials` gets this right with its `continue` at line 234.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 120,
      "end_line": 120,
      "label": "bug",
      "severity": "low",
      "explanation": "remaining >= self.max_bytes` evicts when the store is exactly at the ceiling, though `cache_max_bytes` is documented as \"the most the blob store may hold\" (config.rs:63); should be `>`.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    }
  ]
}
