Suppose you have a separate seed for level generation.
RNG(seed,depth) returns a deterministic random value used to generate a given level. IE, if you're moving on to depth 8, we use RNG(seed,8) to get a value that we can use to generate the next level. generateLevel(RNG(seed,currentDepth+1), currentDepth, ... ), or something.
This will make it so that your levels are deterministically generated from an original random seed, effectively allowing you to store all of the map data within the seed used by the algorithm (which it itself is generated from the original seed used by the RNG for level generation). We only need to know which depth we are coming from and which depth we are going to and we can get the same level every time. The obvious problem with this is that our map will generate new instances of the same monsters, items, etc.
To resolve duplications, I would split it up into three phases. The first phase is to tell our level generator which items/monsters not to generate. When we leave a map, we can store which objects were removed from the map via a bitstring or some other minimal data-type. We only need to store the object relative to the order in which objects were generated. That is, if you kill the monster that was generated first (by the algorithm), we only need to know not to generate the first monster. We can give each object a unique binary enumeration and run a binary tree compression algorithm to ensure that the amount of information we're storing is minimal. If we're going upstairs, we can have something like -> GenerateLevel(RNG(seed,currentDepth-1),currentDepth, phase1[currentDepth-1], ... ). Where phase1 is the pointer to an array for each depth that stores a binary compressed value of the monsters and items that should not be generated.
We could potentially stop here and be satisfied with pseudo-persistence. However, when a level is regenerated the monsters/items will spawn with full health at their original spawn points. Phase2 will simply be storing xcoord, ycoord, and HP of monsters and items remaining on the map. Undisturbed monsters/items can be generated as per the normal generation algorithm, but objects that have moved or been hurt need to have those values stored in some way. Then when we regenerate the level we can move/adjust the HP of the objects accordingly. To reduce the memory footprint, we can store +/- 2-3 depths worth of monsters/items. Then, after you distance yourself enough from a level, we can say those objects have healed and returned to their original locations as specified by the level generator. At which point we would only need to use the values of phase1 to determine what is supposed to be in the level and what is not.
Phase 3 is for items NOT native to a level. We need to be able to fetch the seed used to generate that object and it's current depth and coordinates. This is the most memory taxing, as it isn't very coherent for an item dropped on a different level to suddenly disappear when previous items of that level are still there. There are mechanics that you could use to make this more interesting, but it boils down to just about the same memory footprint. Ideally, you would enumerate every item generated and have a separate RNG seed for items. RNG(itemSeed,itemID) will give you the seed necessary to generate that item. So if phase3 is a pointer to an array of tuples that stores the itemID (the order in which the item was generated), the xcoord and ycoord, we can ensure that items that have migrated to different levels stay there after we've left. These items will be culled from their original levels via the phase1 for that depth, so we don't need to know where it came from originally.
Phase2 can be culled over time and phase1 will only store very small values. If your mechanics are designed well, players won't be dropping a superfluously large number of items on levels where they weren't generated, ensuring that phase3 doesn't get out of hand.
IDK if this is the best approach, but some ideas here might get you started. Persistence isn't at all necessary though.