Skip to content

A live attachment pointing at a grace-window tombstone 404s for up to 30 days, then silently starts working — the reap guard only revives files it is asked about, and it is never asked inside the window #10246

Description

@os-warren

Found and measured while implementing #10171 (PR #10241). Deliberately not fixed there, and filed unassigned rather than ridden along: the fix would be Clause-②: yes (it turns a currently-404 download into a 200), and the services lane has no contract-review tier available right now. The #10171 dev stopped and reported it rather than implementing or filing unilaterally; this card is the PM recording it.

The shape

Re-pointing a sys_attachment row onto a file that is inside its 30-day grace-window tombstone is byte-safe — that half is already owned by createSysFileReapGuard, whose sweep-time re-verification resolves current references, un-tombstones the file and vetoes the reap. #10241 measured exactly that and therefore added no revival leg, correctly.

But the guard only ever sees rows the sweep nominates, and the sweep nominates a tombstone only after its ttl { field: 'deleted_at', expireAfter: '30d' } has expired. Measured: candidates within the window = [].

So inside the window the file is not a sweep candidate at all, and simply stays status='deleted'. Meanwhile storage-routes.ts:606 and :653 refuse any file whose status !== 'committed' with 404 FILE_NOT_FOUND.

Net: a live attachment row can point at a file that 404s for up to 30 days, and then silently starts working once the sweep finally runs and un-tombstones it. Nothing is lost, and nothing tells the operator why.

Why this is worth a card rather than a comment

The revival mechanism is correct and the bytes are safe — what is wrong is when it runs. "Eventually correct, on a 30-day timer, with a 404 in between" is a defensible retention posture and an indefensible download posture, and today the system has no way to tell those two apart because one timer serves both.

⚠️#10241 enlarges this population. Before it, only the DELETE verb minted tombstones; after it, an UPDATE that re-points file_id mints them too. The scenario is pre-existing, but the set of ways to reach it just grew — which is why it is being recorded now rather than left as a curiosity.

Why the fix is Clause-② and needs the contract-review tier

Any fix makes a request that is refused today succeed — an endpoint's accept set widens. That is categorically different from #10241's own change, which applied an already-declared rule (an attachments-scope file with zero join rows is tombstoned, enforced on the delete verb since #2755) to the verb that was missing it, minting no new refusal category and no new accepted input.

⛔ So this must not be folded into a lower-tier card.

Directions (not a recommendation — this needs triage/maintainer grading)

  1. Revive at re-point time, not at sweep time: when an update points a row at a tombstoned file, un-tombstone it immediately. Closes the window entirely; costs a second implementation of the reference-resolution question the guard already answers — the duplicate-mechanism hazard fix(service-storage): tombstone the prior file when an attachment re-points file_id #10241 deliberately avoided.
  2. Let the download path resolve it: storage-routes.ts treats a tombstoned file with at least one live join row as downloadable. Keeps one revival mechanism, moves the judgement to the read side; the tombstone stays until the sweep tidies it.
  3. Accept and document: state that a re-attached file may 404 until the next sweep. Cheapest, and honest only if someone actually wants a 30-day 404 in a live attachment.

Refs: #10171 (the update-verb detach leg), PR #10241, #2755 (the orphan rule), createSysFileReapGuard, storage-routes.ts:606 / :653, system-file.object.ts (the ttl / retention lifecycle declaration).

⚠️domain:services assigned by the discovering lane; retriage if it belongs elsewhere.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions