Skip to content

mcpx(cache): Put's own eviction can delete the entry it just stored — it returns success for a path that no longer exists #2560

Closed

Description

@dezren39

Problem

cache.Put can delete the entry it just stored, then return success for a path that no longer exists.

In pkgs/mcpx/internal/cache/cache.go (main 674c4c6a):

  • Put (line 117) renames the filled temp directory into place, then runs _ = c.Evict(kind, lim) (line 141) before returning (dest, false, nil).
  • Evict (line 272) calls SelectVictims(entries, lim, "") (line 277) — the keep argument is empty, so the just-stored entry is an ordinary eviction candidate. SelectVictims (line 305) only ever exempts keep and leased entries.
  • By contrast Prune (lines 284-289), the runner-facing form of the same mechanism, passes its keep entry through to SelectVictims — the mechanism already has the exemption concept; Put/Evict do not use it for the entry being stored.

So whenever the bounds cannot be satisfied by evicting older unleased entries — one entry larger than MaxBytes alone, or every older entry leased while the total is over MaxBytes — the newest victim is the entry Put just wrote. The caller receives a path, hit=false, and a nil error, and only discovers the loss later as a cache miss / missing directory at use time.

Evidence (verified this run, real module on the laptop)

Probe: laptop ~/git/fleet/probe-2026-10-29-laneA-cacheput/ — internal/cache/cache.go copied verbatim (md5 9f9ce95f29c82804fb56487e47a29cd7, matches main 674c4c6a), driven by a small program:

  • Control, Put with Limits{} for a 100-byte entry: hit=false err=<nil> exists=true.
  • Scenario A, same entry with Limits{MaxBytes: 10}: Put returns hit=false err=<nil>, but stat on the returned path reports no such file or directory, and a following Get for the key misses — the entry is gone.
  • Scenario B, a 100-byte older entry held under cache.Acquire, then Put of a second 100-byte entry with Limits{MaxBytes: 150} (total 200): newExists=false oldExists=true — the leased entry correctly survives and the just-stored entry is the one evicted, again with a nil error from Put.

No existing test covers Put under a binding limit: cache_test.go's writeEntry helper always passes cache.Limits{}, and the eviction tests call Evict directly.

Context: Put is the store path of the one content-addressed cache mechanism from #1908 (merged in PR #2555). Today it is exercised directly only by tests, but it is the documented API the remaining kinds (prepared dependencies, import graphs, flake resolutions — #1915/#1921) are being migrated onto; each new caller inherits a silent store-then-delete on exactly the oversized/pressured stores that most need a loud outcome.

Why it matters

A store API that reports success for data it has already deleted turns a configuration/size problem into a Heisenbug downstream: intermittent missing prepared deps and workdirs, retried work with no error anywhere in the log (the Evict result is discarded with _ =). It also makes the byte bound unenforceable as a diagnostic — an entry that can never fit should fail at Put, not vanish.

Acceptance criteria

  • Put never returns (path, false, nil) for a path that does not exist when the call returns: either the just-stored entry is exempt from the eviction pass its own store triggers (keep semantics, as Prune already has), or Put returns an explicit error when the entry cannot be retained under lim.
  • The chosen semantics are pinned by tests in internal/cache: (a) a single entry larger than MaxBytes; (b) a store made while all older entries are leased and the total is over MaxBytes.
  • Existing cache tests still pass (TestEvictByBytesKeepsNewest, TestLeasedEntrySurvivesEviction, TestPruneCountWithPatternAndKeep, round-trip/hit behaviour unchanged for unbounded stores).
  • Triage pins which of the two semantics (exempt-just-stored vs error) callers of the mcpx: one content-addressed cache for prepared dependencies, import graphs and flake resolutions #1908 migration should rely on, and the package doc comment for Put states it.

Activity

  1. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

    • Agent: new-issue-creator (new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a run 2026-10-30T04:41:05Z
    • Found via: filed this run
    • Task: Filed mcpx(cache): Put's own eviction can delete the entry it just stored — it returns success for a path that no longer exists #2560 — cache.Put's post-store Evict (keep="") can delete the just-stored entry and Put still returns success for the missing path; verified with the real cache.go on the laptop (single entry > MaxBytes, and store while all older entries are leased).
    • Expectation: effort/xs triage — pin exempt-just-stored vs explicit-error semantics, then route the fix.
    • Links: probe laptop ~/git/fleet/probe-2026-10-29-laneA-cacheput/ (real cache.go, md5 9f9ce95f29c82804fb56487e47a29cd7)
  3. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: claimed

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions