After the IRC discussion I had with Mārtiņš, I ended up implementing a simpler open-addressed hash table that allocates a new table twice the size, and incrementally rehashes entries from the old to the new.
It only ever has 2 tables maximum in flight at once - if it needs to expand again then it'll rehash whatever is left in the old table immediately, but the incremental rehashing means that there will only be a maximum of a few entries left in there anyway.
I'm using malloc separately for each of the two tables to simplify freeing, but it would be possible to have them concatenated and use realloc, or a custom memory arena instead with some more work.
The code for it is here if you're interested:
https://github.com/baines/insobot/blob/master/src/inso_ht.h
EDIT: I realize now that this doesn't really address the concern of having a "hole" in an arena where the old table used to be... I guess if that is a major concern then the previous idea of having an increasing number of tables without any rehashing could work, at the cost of increasing lookup time slightly.