{
  "sha": "9b51f95ef609a219e211e37b082cd2e6913190e0",
  "model": "z-ai/glm-5.3-flash",
  "effort": "high",
  "found_per_pass": {
    "agent": 8
  },
  "findings": [
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 140,
      "end_line": 140,
      "label": "bug",
      "severity": "high",
      "explanation": "A dry run is destructive.\n\nThe blob unlink is guarded by `if !dry_run` (line 125), but every \"removed\" candidate is still pushed to `gone` and `forget(&index, &gone)` is called unconditionally, deleting index entry files (lines 249-257). `?dry_run=true`, documented as reporting \"without removing anything\" (sweep.rs:87-88, docs/operations.md), actually deletes index entries for every blob the hypothetical sweep would take — which, on an over-ceiling store, is most of the index.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/metrics.rs",
      "start_line": 94,
      "end_line": 94,
      "label": "bug",
      "severity": "high",
      "explanation": "The two sweep counters are swapped in the exposition.\n\n`COUNTERS` lists `sweep_blobs_removed` then `sweep_bytes_reclaimed` (metrics.rs:51-56), but `values()` returns `sweep_bytes_reclaimed` then `sweep_blobs_removed` (lines 94-95), and `render` zips them in order. `cairn_proxy_sweep_blobs_removed_total` reports bytes and vice versa, so an operator alerting on reclaimed bytes per docs/operations.md reads blob counts.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 232,
      "end_line": 232,
      "label": "bug",
      "severity": "high",
      "explanation": "clear_partials` unlinks every file in `incoming` with no age check, but `incoming` also holds the temp files of fetches in flight right now (`BlobStore::writer`, store.rs:171-180).\n\nThe fetch keeps writing to the unlinked inode and then fails at commit — `fs::rename` on the missing temp (store.rs:259) — turning a healthy download into a 500. A periodic sweep simply kills any download that happens to be running, which is exactly what `CAIRN_CACHE_MIN_AGE` exists to prevent.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 119,
      "end_line": 119,
      "label": "bug",
      "severity": "medium",
      "explanation": "Blobs with no index entry are removed with `min_age` deliberately bypassed, but a just-committed blob has no index entry for the whole commit → `register_version` (network round trip) → `link`…\n\nBlobs with no index entry are removed with `min_age` deliberately bypassed, but a just-committed blob has no index entry for the whole commit → `register_version` (network round trip) → `link` window (routes/cache.rs:151-163). A sweep in that window deletes the blob; the serve path then fails at cache.rs:90 with \"a blob committed by this request is already missing\" (500), and the client's fetch is wasted. The registry call makes the window wide, and the grace period the docs promise does not cover it.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 126,
      "end_line": 126,
      "label": "bug",
      "severity": "medium",
      "explanation": "A failed `fs::remove_file` is still counted as success: `remaining`, `bytes`, `removed` and `gone` are all updated on the error path, and `forget` then deletes the index entry for a blob that may…\n\nA failed `fs::remove_file` is still counted as success: `remaining`, `bytes`, `removed` and `gone` are all updated on the error path, and `forget` then deletes the index entry for a blob that may still be on disk. The metrics overstate reclamation, `remaining` drops for bytes never freed so the sweep stops early believing it is under the ceiling, and a persistent unlink failure (permissions, read-only remount) looks like a successful sweep.",
      "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 at lines 160-163 which claims links are \"counted at the size of the link\" (that needs `fs::symlink_metadata`).\n\nWorse, a symlink to a directory passes the `is_dir` check at line 167 and is recursed into, so a symlink cycle in `blobs/` or `index/` makes `collect` recurse forever — the store-moved-between-volumes scenario the comment anticipates is the one that hangs the sweep.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 84,
      "end_line": 84,
      "label": "performance",
      "severity": "medium",
      "explanation": "The synchronous full-store walk runs directly on a tokio worker thread — both from the interval task (`run().await`) and from the admin handler (routes/admin.rs:76).\n\nOn a store the module doc itself sizes at tens of thousands of directories, one sweep blocks that worker for the whole walk and stalls every request scheduled on it; nothing moves the walk to `spawn_blocking`.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    },
    {
      "path": "services/proxy/src/sweep.rs",
      "start_line": 189,
      "end_line": 189,
      "label": "bug",
      "severity": "low",
      "explanation": "A blob whose mtime is ahead of the local clock gets `age = 0` via `unwrap_or_default`, so it never satisfies `age >= min_age` (line 120) and is permanently immune to ceiling eviction.\n\nClock skew on a shared volume — the case the comment describes — can therefore make it impossible to bring the store under `CAIRN_CACHE_MAX_BYTES` at all.",
      "evidence": null,
      "suggested_fix": null,
      "deterministic": false
    }
  ]
}
