( streamId: string, afterCursor: string, requestId?: string )
| 19 | } |
| 20 | |
| 21 | export async function checkForReplayGap( |
| 22 | streamId: string, |
| 23 | afterCursor: string, |
| 24 | requestId?: string |
| 25 | ): Promise<ReplayGapResult | null> { |
| 26 | const requestedAfterSeq = Number(afterCursor || '0') |
| 27 | if (requestedAfterSeq <= 0) { |
| 28 | // Fast path: no cursor → nothing to check. Skip the span to avoid |
| 29 | // emitting zero-work spans on every stream connect. |
| 30 | return null |
| 31 | } |
| 32 | |
| 33 | return withCopilotSpan( |
| 34 | TraceSpan.CopilotRecoveryCheckReplayGap, |
| 35 | { |
| 36 | [TraceAttr.StreamId]: streamId, |
| 37 | [TraceAttr.CopilotRecoveryRequestedAfterSeq]: requestedAfterSeq, |
| 38 | ...(requestId ? { [TraceAttr.RequestId]: requestId } : {}), |
| 39 | }, |
| 40 | async (span) => { |
| 41 | const oldestSeq = await getOldestSeq(streamId) |
| 42 | const latestSeq = await getLatestSeq(streamId) |
| 43 | span.setAttributes({ |
| 44 | [TraceAttr.CopilotRecoveryOldestSeq]: oldestSeq ?? -1, |
| 45 | [TraceAttr.CopilotRecoveryLatestSeq]: latestSeq ?? -1, |
| 46 | }) |
| 47 | |
| 48 | if ( |
| 49 | latestSeq !== null && |
| 50 | latestSeq > 0 && |
| 51 | oldestSeq !== null && |
| 52 | requestedAfterSeq < oldestSeq - 1 |
| 53 | ) { |
| 54 | const resolvedRequestId = await resolveReplayGapRequestId(streamId, latestSeq, requestId) |
| 55 | logger.warn('Replay gap detected: requested cursor is below oldest available event', { |
| 56 | streamId, |
| 57 | requestedAfterSeq, |
| 58 | oldestAvailableSeq: oldestSeq, |
| 59 | latestSeq, |
| 60 | }) |
| 61 | span.setAttribute(TraceAttr.CopilotRecoveryOutcome, CopilotRecoveryOutcome.GapDetected) |
| 62 | |
| 63 | const gapEnvelope = createEvent({ |
| 64 | streamId, |
| 65 | cursor: String(latestSeq + 1), |
| 66 | seq: latestSeq + 1, |
| 67 | requestId: resolvedRequestId, |
| 68 | type: MothershipStreamV1EventType.error, |
| 69 | payload: { |
| 70 | message: 'Replay history is no longer available. Some events may have been lost.', |
| 71 | code: 'replay_gap', |
| 72 | data: { |
| 73 | oldestAvailableSeq: oldestSeq, |
| 74 | requestedAfterSeq, |
| 75 | }, |
| 76 | }, |
| 77 | }) |
| 78 |
no test coverage detected