diff --git a/mcp/event.go b/mcp/event.go index bbb3e6c8..1d86b703 100644 --- a/mcp/event.go +++ b/mcp/event.go @@ -342,6 +342,15 @@ func (s *MemoryEventStore) After(_ context.Context, sessionID, streamID string, return nil, fmt.Errorf("MemoryEventStore.After: index %d, stream ID %v, session %q: %w", index, streamID, sessionID, ErrEventsPurged) } + // An index at or beyond the latest stored event has nothing after it, so + // there is nothing to replay. The boundary start == dl.first+len(dl.data) + // (resume from the last event seen) already yields an empty slice; guard + // the strictly-greater case too, otherwise dl.data[start-dl.first:] + // panics with a slice-bounds-out-of-range on an index past the end (for + // example a client-supplied Last-Event-ID beyond any event ever sent). + if start-dl.first > len(dl.data) { + return nil, nil + } return slices.Clone(dl.data[start-dl.first:]), nil } diff --git a/mcp/event_test.go b/mcp/event_test.go index 91ddb9d7..9a902dab 100644 --- a/mcp/event_test.go +++ b/mcp/event_test.go @@ -299,7 +299,13 @@ func TestMemoryEventStoreAfter(t *testing.T) { {"S1", "1", 0, []string{"d2", "d3"}, ""}, {"S1", "1", 1, []string{"d3"}, ""}, {"S1", "1", 2, nil, ""}, + // An index past the latest stored event (highest here is 2) must yield an + // empty replay, not panic with a slice-bounds-out-of-range. This is + // reachable from a client-supplied Last-Event-ID beyond any event sent. + {"S1", "1", 3, nil, ""}, + {"S1", "1", 100, nil, ""}, {"S1", "2", 0, nil, ""}, + {"S1", "2", 5, nil, ""}, // past the end of stream "2" (highest is 0) {"S1", "3", 0, nil, "unknown stream ID"}, {"S2", "0", 0, nil, "unknown session ID"}, } {