Summary
Storage::persist_rebased_receipt_at says it touches the source receipt so that the source is not prematurely evicted by LRU, but the update refreshes only last_access_unix_millis and expires_unix_millis. It does not assign a new access_sequence.
Eviction is ordered exclusively by (access_sequence, id). At the 128-receipt quota, a successful rebase can therefore leave the refreshed source as the oldest LRU entry, and the very next receipt creation deterministically deletes it even though its TTL was just extended.
Affected revision: 85dd1b1b918e863f5535333cf435a5ac6163c30e.
Evidence
src/receipt.rs defines:
pub(crate) const MAX_RECEIPTS: usize = 128;
pub(crate) const RECEIPT_TTL_MILLIS: i64 = 24 * 60 * 60 * 1_000;
New/touched ordinary receipts obtain a monotonically increasing sequence through next_receipt_access_sequence.
src/storage/receipts.rs::evict_oldest_receipt_except selects:
SELECT id
FROM retrieval_receipts
WHERE access_sequence > 0
ORDER BY access_sequence, id
LIMIT 1
During rebase, the source is protected from quota eviction inside the current transaction. The function then contains this comment and update:
// Touch the source receipt so it is not prematurely evicted by LRU.
tx.execute(
"UPDATE retrieval_receipts
SET last_access_unix_millis = ?1,
expires_unix_millis = ?2
WHERE id = ?3",
params![now_unix_millis, source_expires_unix_millis, source_row_id],
)?;
Unlike the ordinary receipt touch path, this update does not modify access_sequence.
Formal counterexample
Let N = MAX_RECEIPTS = 128.
- Create source receipt
S first. Its LRU sequence is 1.
- Create
N - 1 additional receipts. The database is full and their sequences are 2..N.
- Rebase
S.
- The quota loop must make room for the rebased receipt.
- It calls
evict_oldest_receipt_except(..., Some(S)), so sequence 2 is deleted rather than S.
- The source timestamps are refreshed, but
S.access_sequence remains 1.
- The new rebased receipt is inserted with sequence
N + 1.
- Create one more ordinary receipt.
- The database is again at
N receipts.
- Creation calls
evict_oldest_receipt_except(..., None).
- The minimum
(access_sequence, id) is now S with sequence 1.
S is deleted.
Thus the source can be evicted on the first quota-producing operation after the code has explicitly “touched” it and extended its expiry by 24 hours.
This is deterministic; no race or clock anomaly is required.
Existing test gap
rebase_quota_eviction_never_selects_the_source_receipt verifies only that the source survives the rebase transaction itself. It does not create another receipt after the rebase. The sequence above therefore passes the current test while violating the stated LRU intent immediately afterward.
Impact
- A source receipt exposed as
source_receipt_id can become unreadable immediately after a successful rebase under normal quota pressure.
- Follow-up audit, resource-read, comparison, or another rebase flow can receive
UnknownReceipt despite the source having just been refreshed.
- The persisted expiry timestamp claims the source is live for another TTL while the independent LRU ordering still treats it as the oldest entry.
- The behavior becomes load-dependent: low-volume repositories preserve the source, while busy multi-agent repositories lose it at the first subsequent receipt allocation.
Suggested acceptance criteria
- A successful rebase refreshes the source's LRU position consistently with its refreshed access/expiry timestamps.
- At
MAX_RECEIPTS, execute the exact sequence above and assert that:
- the source survives the rebase;
- one subsequent receipt allocation evicts the next genuinely least-recently-used receipt, not the source;
- both the source and newly rebased receipt remain readable afterward.
- Preserve monotonic, overflow-checked access-sequence accounting and the current transaction/quota bounds.
- Add a clock-controlled regression test so the result does not depend on wall time.
Non-goals
- Do not increase the receipt quota or TTL as a workaround.
- Do not retain all historical receipts indefinitely.
- Do not weaken quota enforcement or the guarantee that the rebase source cannot be evicted during the rebase transaction itself.
Summary
Storage::persist_rebased_receipt_atsays it touches the source receipt so that the source is not prematurely evicted by LRU, but the update refreshes onlylast_access_unix_millisandexpires_unix_millis. It does not assign a newaccess_sequence.Eviction is ordered exclusively by
(access_sequence, id). At the 128-receipt quota, a successful rebase can therefore leave the refreshed source as the oldest LRU entry, and the very next receipt creation deterministically deletes it even though its TTL was just extended.Affected revision:
85dd1b1b918e863f5535333cf435a5ac6163c30e.Evidence
src/receipt.rsdefines:New/touched ordinary receipts obtain a monotonically increasing sequence through
next_receipt_access_sequence.src/storage/receipts.rs::evict_oldest_receipt_exceptselects:During rebase, the source is protected from quota eviction inside the current transaction. The function then contains this comment and update:
Unlike the ordinary receipt touch path, this update does not modify
access_sequence.Formal counterexample
Let
N = MAX_RECEIPTS = 128.Sfirst. Its LRU sequence is1.N - 1additional receipts. The database is full and their sequences are2..N.S.evict_oldest_receipt_except(..., Some(S)), so sequence2is deleted rather thanS.S.access_sequenceremains1.N + 1.Nreceipts.evict_oldest_receipt_except(..., None).(access_sequence, id)is nowSwith sequence1.Sis deleted.Thus the source can be evicted on the first quota-producing operation after the code has explicitly “touched” it and extended its expiry by 24 hours.
This is deterministic; no race or clock anomaly is required.
Existing test gap
rebase_quota_eviction_never_selects_the_source_receiptverifies only that the source survives the rebase transaction itself. It does not create another receipt after the rebase. The sequence above therefore passes the current test while violating the stated LRU intent immediately afterward.Impact
source_receipt_idcan become unreadable immediately after a successful rebase under normal quota pressure.UnknownReceiptdespite the source having just been refreshed.Suggested acceptance criteria
MAX_RECEIPTS, execute the exact sequence above and assert that:Non-goals