| … | … | @@ -47,369 +38,268 @@ export class ProjectionMaintenanceV2 extends Context.Service< | |
| 99 | + | head: number, | |
| 100 | + | betweenPages: Effect.Effect<void>, | |
| 101 | + | ) { | |
| 102 | + | const referenced = yield* readPromptReferences(throughSequence, head, betweenPages); | |
🟠 High orchestration-v2/ProjectionMaintenance.ts:102
Pruning deletes a prompt anchor still referenced by a surviving stream-head event, so replay restores that event with a null prompt instead of its original text. readPromptReferences(throughSequence, head, ...) excludes references from rows at or below the boundary, even though pruneEvents preserves each stream's newest row; include references from all rows that survive this pruning pass.
No longer relevant as of 0955847e6f138a012574634c0c9f818cc689f3ea
Fixed in 0955847: each pass now also collects promptSequence references from stream-head rows at or below the boundary (the only kept rows that can borrow a prompt) before deleting anything, so a retained head keeps its anchor. Covered by "keeps the anchor a retained stream head borrows from", which fails without the change.
| … | … | @@ -783,6 +783,7 @@ const canReplayPersistedRange = Effect.fnUntraced(function* ( | |
| 783 | 783 | if (replayGap < 0 || replayGap > maxGap) { | |
| 784 | 784 | return false; | |
| 785 | 785 | } | |
| 786 | + | if (afterSequence < (yield* applicationEvents.prunedThroughSequence)) return false; | |
🟠 High src/ws.ts:786
A resume can pass this pruning check and still receive an incomplete replay, leaving the client state stale instead of triggering a snapshot. Pruning can advance after this check but before or during the deferred, paginated readApplicationEvents/catchUpApplicationEvents read, which silently skips deleted events before the replay emits its synchronization marker and resumes live from highWater; protect the replay from concurrent pruning or recheck the boundary before accepting it.
No longer relevant as of e188f19585a2f3f9c34d2ad7cce5d256c6640ef8
Fixed in e188f19. Thread and shell resumes now read the replay first and then re-read prunedThroughSequence; if it moved past the cursor (the thread path also requires the cursor to be below the thread high-water), the resume falls back to a snapshot. Pruning publishes the boundary before deleting, so any pass that deleted rows in the replayed range during the read is caught. The shell replay is now collected before replying (it is already bounded by SHELL_RESUME_MAX_GAP and the payload budget). New test "%s resume loads a snapshot when pruning advances during the replay read" gates the replay query, prunes mid-read, and fails for both paths without the recheck.