A single-record audit replay reports a genuine delete as an empty put. The delete signal is lost, and what the client receives instead is { type: 'put', value: undefined } — a phantom write that never happened.
Mechanism on main
In the single-record (thisId specified) startTime audit-tail replay branch, resources/Table.ts:4600-4636, the loop walks a record's full audit history backward via auditRecord.previousVersion / previousNodeId. Deletes are part of that chain; nothing filters them out. Then, at resources/Table.ts:4632:
if (getFullRecord) auditRecord.type = 'put';
This is unconditional — it stomps the type of every entry in the walk. getFullRecord is !request.rawEvents, i.e. true by default.
A delete entry has no record payload to restore: delete: DELETE (resources/auditStore.ts:465-469) carries neither HAS_RECORD nor HAS_PARTIAL_RECORD, so auditRecord.getValue() (resources/auditStore.ts:750-772) returns undefined. The historyEntry then spreads the already-mutated auditRecord, so the consumer sees a put with no value.
Why this is a defect and not the intended contract
The two sibling replay branches in the same file make the identical auditRecord.getValue(primaryStore, getFullRecord, …) call and both emit type: auditRecord.type verbatim, preserving delete/patch/invalidate:
- collection-level
startTime branch — resources/Table.ts:4464
previousCount branch — resources/Table.ts:4513
Only the per-record branch overwrites the type. The inconsistency between three otherwise-parallel branches is the argument.
Not a duplicate
(qa-explorer campaign, QA-682. The finding was recorded UNVERIFIED because the ?startTime= replay path was never engaged live; this filing rests on a source read of origin/main 2026-09-04, which confirms the claim and locates it at :4632 — drifted from the original :3869. A replay-reaching regression test is worth adding with the fix.)
A single-record audit replay reports a genuine delete as an empty
put. The delete signal is lost, and what the client receives instead is{ type: 'put', value: undefined }— a phantom write that never happened.Mechanism on
mainIn the single-record (
thisIdspecified)startTimeaudit-tail replay branch,resources/Table.ts:4600-4636, the loop walks a record's full audit history backward viaauditRecord.previousVersion/previousNodeId. Deletes are part of that chain; nothing filters them out. Then, atresources/Table.ts:4632:This is unconditional — it stomps the type of every entry in the walk.
getFullRecordis!request.rawEvents, i.e. true by default.A delete entry has no record payload to restore:
delete: DELETE(resources/auditStore.ts:465-469) carries neitherHAS_RECORDnorHAS_PARTIAL_RECORD, soauditRecord.getValue()(resources/auditStore.ts:750-772) returnsundefined. ThehistoryEntrythen spreads the already-mutatedauditRecord, so the consumer sees aputwith no value.Why this is a defect and not the intended contract
The two sibling replay branches in the same file make the identical
auditRecord.getValue(primaryStore, getFullRecord, …)call and both emittype: auditRecord.typeverbatim, preserving delete/patch/invalidate:startTimebranch —resources/Table.ts:4464previousCountbranch —resources/Table.ts:4513Only the per-record branch overwrites the type. The inconsistency between three otherwise-parallel branches is the argument.
Not a duplicate
type: "delete"when the entry is already gone #2439 (OPEN) — live subscription can deliver a CREATE audit record astype: delete. Different mechanism (a real-time listener racing current primary-store state,Table.ts:4230-4265) and the opposite direction of misclassification.getRecordAtTimecrash / silent-wrong family, inresources/crdt.ts, on theread_audit_log/hash_valuereconstruction path, not the subscribe-replay path.(qa-explorer campaign, QA-682. The finding was recorded UNVERIFIED because the
?startTime=replay path was never engaged live; this filing rests on a source read oforigin/main2026-09-04, which confirms the claim and locates it at:4632— drifted from the original:3869. A replay-reaching regression test is worth adding with the fix.)