After a lot of debugging (and swearing) and very hairy math, I have my version of the NTP libraries mostly keeping time consistently. It does the full offset and delay calculations. There are still a few quirks that I need to track down, but the skew is far more predictable now–though still larger than I would like. In 10 seconds, the clock drifts between -4000 and -14000 microseconds (about 1000 ppm, which is pretty high). With a little adaptive frequency shifting I think I can get that drift down quite a bit.
I have starting thinking about submitting a pull request to merge my NTPClient changes back into the main library, but I committed a cardinal sin. I modified an older version of the code rather than starting with the latest. My attempts to reconcile their changes (mostly making all the getters idempotent) and mine (changing the fundamental time unit from seconds to microseconds while not breaking the API) have proven very clunky, and I’ve given up for the moment. I’m thinking of heading in the opposite direction and just forking my effort into a whole new refactored library with a more elegant way to handle complex time calculations, similar to the DateTime class in the RTClib, but more flexible.