I number my prototypes and start each one fresh, its own repository with its own initial commit and nothing carried over on purpose. Across a run of Unreal iterations, that convention lost the same kind of thing three times in a row, and a fourth loss, different in kind, is what finally showed me that the boundary between one iteration and the next isn’t just where history stops — it’s where a written record can quietly stop being true and nobody is looking at it anymore.
Mostly the convention is good — an iteration is allowed to reason from scratch, and history from the one before stays where it was made. It’s also, provably, a way of losing things.
Loss one — a setting that had already been decided
The first prototype turned Lumen and virtual shadow maps off and wrote down why, right next to the CVars in Config/DefaultEngine.ini. The one right after it regenerated the project from Unreal’s third-person template instead of carrying the config forward, and got them silently back on — a fresh DefaultEngine.ini, written from the template’s defaults, with no comment and no mention in the commit that created it. The next Unreal iteration in the line inherited that regenerated file without anyone touching it.
It sat there for two iterations because nothing was heavy enough to notice. It surfaced once there were two maps and an AI in one of them, and the project “felt heavier than it should,” with a suspicion that ray tracing was involved. It was: the project was running the template’s defaults again. The fix turned four CVars back down — r.DynamicGlobalIlluminationMethod, r.ReflectionMethod, r.Shadow.Virtual.Enable, r.GenerateMeshDistanceFields — the same four the first prototype had already turned down once, verified from a packaged build’s log rather than trusted from the diff. A line went into the project’s own instructions afterward: regenerating from the template loses whatever the last iteration lowered, so diff Config/ against the previous project after regenerating.
That line is correct and it is exactly the kind of thing this post is about: a decision, written down once, made again two iterations later because nothing forced anyone to read the note before running the regeneration script.
Loss two — a deleted asset that came back
A later iteration hit a different shape of the same problem. An empty navigation-mesh actor — the kind the editor auto-creates next to a nav bounds volume — got deleted by hand once the problem behind it was understood: a commandlet had saved the level before tile generation produced anything, and Unreal’s navigation system only spawns new navigation data for an agent slot that has no matching actor already in the level. An empty one still counts as present, so it silently blocks the exact rebuild that would otherwise fix it.
The next iteration inherited that level, and two other tools that also save it — one that repairs stale World Settings, one that places interactables — re-created the empty actor as a side effect of doing their own job. The AI stood still again, for the same reason, on the same map.
The fix wasn’t a note. It pulled the deletion into one shared function and had every tool that saves a level call it before it saves — not “remember to delete the navmesh,” but “no tool can save a level without also stripping it.” That held: a few iterations later, when a new tool for assembling levels out of placed rooms was added, it forgot to call that function on its first pass and reintroduced the same symptom on a different map, on the same iteration it shipped in — caught and fixed within that iteration, not the next one, because the shared function already existed to point at.
Loss three — a gap nobody had written down either
A build script sends a working tree to a Windows build machine over tar and SSH, because Windows has shipped tar since Windows 10 and nothing else needs installing on the far side. tar adds and updates files. It does not delete them. Nothing in the commit that wrote the script said so — I went back and checked, and the script’s own comments explain the transfer cost (every run sends the whole tree, not a delta) without ever mentioning what happens to a file removed locally. That gap wasn’t a documented risk. It just wasn’t anyone’s problem yet.
Then it was. The empty navigation-mesh actor from loss two got deleted locally as part of the shared-function fix — and stayed behind on the Windows build machine, cooked into every package after it. The AI stood still on Windows and moved fine on Mac. It read as a platform difference. It was a sync one, found the only way that actually settles a question like that: by counting rather than reasoning. Sixty-seven external actor files on the Mac against sixty-eight on the build machine, with the extra one traceable to the exact commit that had deleted it locally.
The fix wipes the payload directories on the build machine before each extraction, so a local deletion becomes a remote one. The directories that make the build incremental — intermediate build output, the derived data cache, staged output — stay untouched.
The first attempt at that fix didn’t work, and the commit that made it said it did. The very next commit explains why: the edit’s replacement anchor didn’t match the file — there were two comment lines sitting between the parts it expected to be adjacent — so the change silently did nothing, and bash -n and shellcheck both passed because the script itself hadn’t changed. Neither tool can tell the difference between “no bug” and “no edit.” The real fix was verified the same way loss three itself was found: not by reading the diff, but by counting again — sixty-seven files on the Mac, sixty-seven on the build machine after the run, sixty-eight before it, and a direct existence check on the deleted navmesh actor coming back False.
Neither syntax checker could have caught the failed attempt on its own — they only read whether the script parses and lints cleanly, not whether the edit inside it actually happened.
Three losses, one shape: something true when it was written stopped being watched, and nothing forced anyone back to it before it mattered again.
A fourth failure, different in kind
The three losses above are all the same mechanism pointed at different assets: a setting, an actor, a line of shell script. The fourth one didn’t lose an asset at all. It lost the truth of a sentence that used to be accurate.
flowchart LR
A["instructions say:<br/>no gate placed"] -->|gate placed<br/>and played| B["README<br/>corrected"]
A -->|left<br/>untouched| C["instructions still<br/>say no gate"]
C -->|copied into<br/>next repo| D["agent reads<br/>the stale line"]
D -->|reasons<br/>soundly| E["wrong conclusion:<br/>class, not instance"]
The arrow that matters is the one from the uncorrected line straight into the next repository: nothing about copying a project forward re-checks whether what it says is still true.
One iteration’s own project instructions said, correctly at the time, that no exit gate had been placed in any level yet. Then one was — placed by hand, played through by two people, on Mac and on a packaged Windows build. The top-level README got corrected the same day. The equivalent line in the project’s own instructions file did not; it kept saying no gate existed.
That file gets copied wholesale into the next iteration before anything else happens to it. An agent setting up that next iteration found the gate’s class baked into the inherited level, read the still-uncorrected line, and reasoned from it: since the instructions said no instance was placed, this had to be a class reference rather than a placed one, so it reported the gate as inferred rather than confirmed. The map in fact held a named instance. The reasoning was sound. The premise it started from had quietly stopped being true less than a day earlier, in a different repository, and nothing about the boundary between the two forced anyone to notice.
Nobody was hurt by it this time — the agent said outright that it hadn’t verified the claim directly, so the report stayed honestly hedged. But the shape of the failure is the one that should worry me most out of all four: reading a stale fact and reasoning correctly from it produces a confident, well-argued, wrong answer, and nothing about that answer looks different from a confident, well-argued, right one. A missing CVar or a resurrected actor breaks visibly. A stale sentence that nobody re-reads against reality breaks silently, and it breaks whoever reads it next, not whoever wrote it.
A later prototype in this series hit the same shape again, on a fact that had nothing to do with this repository at all: an identification of an engine bug carried forward for years because it kept coming from the same cited source, and nobody went back to check that source until something forced the question open.
PostEvery click on macOS was landing outside the window, and I put it thereOpening a menu once, on macOS only, killed every click for the rest of the session — the window kept reporting a screen position it never occupied. The engine bug is real and has its own tracker number. The thing that triggered it was a spawn I placed twice.Wrap-up
Writing a lesson down is not the same as being unable to lose it. A comment in a config file, a line in a project’s own instructions, a note in a commit message — all of them are passive. They only pay off if someone reads them at the exact moment they matter, and across this run of iterations nobody reliably did, including, in loss four, the person who was supposed to keep two copies of the same fact in sync.
What actually held was the one fix with a mechanism behind it: a function every level-saving tool is forced to call. That one didn’t need to be remembered, because there was no step where remembering was ever the thing doing the work. The build-machine wipe is the same kind of fix, once it actually landed. The CVar diff-after-regenerate note is not — it’s still just a sentence someone has to read at the right moment, which is exactly the failure mode loss four repeated on a different fact.
I don’t think the answer is to stop writing things down. But I’m trying to notice, now, when a note is standing in for a mechanism I could build instead, and when the same fact lives in two places that a single correction won’t reach both of. Reach for the mechanism where you can. Know that the note is a note, and know where its other copies are.
References
- Lumen Global Illumination and Reflections in Unreal Engine — Epic’s documentation on enabling and disabling Lumen
- The navigation-rebuild claim in loss two is from reading
UNavigationSystemV1::SpawnMissingNavigationDataandFillInstantiatedDataMaskin the engine’s ownNavigationSystem.cpp(Unreal Engine 5.8) — Epic’s public navigation-mesh documentation page returned no readable content when checked, so it isn’t linked here