Kraken’s nightly architectural scan (routed via Sautner-Studio-LLC/krill-oss#233) flagged that krill-sdk’s NodeObserver interface could not type-enforce its coroutine-cancellation contract for implementations that bypass AbstractNodeObserver. krill-sdk 0.0.66 gates direct NodeObserver implementation behind @SubclassOptInRequired(UnsafeNodeObserverImplementation::class). krill’s own DefaultNodeObserver implemented NodeObserver directly, so once the krill-sdk pin in gradle/libs.versions.toml moved past 0.0.66 this class would have failed to compile.
DefaultNodeObserver predated AbstractNodeObserver (added to krill-sdk at 0.0.58) and hand-rolled the exact scope-lifecycle pattern the SDK now standardizes: a child CoroutineScope built from SupervisorJob(parentJob) so individual node failures stay isolated while the scope still dies with its parent, plus a synchronous close() that cancels that scope. Nothing was functionally wrong — close() already cancelled synchronously, so there was no live leak — but the class was never migrated to extend the SDK’s abstract base once it existed, so it kept using the unsafe direct-implementation path the new opt-in gate is designed to catch.
DefaultNodeObserver now extends AbstractNodeObserver(scope) instead of implementing NodeObserver directly (shared/src/commonMain/kotlin/krill/zone/shared/node/NodeObserver.kt).observerScope field and close() override; both now come from the SDK base class. Verified byte-for-byte equivalence by decompiling the currently-pinned krill-sdk-jvm-0.0.64.jar: AbstractNodeObserver’s constructor builds CoroutineScope(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) and its close() cancels that scope synchronously — identical to what was removed.AbstractNodeObserver was already available at the pinned 0.0.64, so this fix lands ahead of any future pin bump rather than being blocked on one.When an SDK adds an abstract base class that formalizes a pattern already hand-rolled downstream, migrate the downstream implementation at the same time rather than leaving it on the direct-interface path “since it still compiles.” A @SubclassOptInRequired gate added later turns that deferred migration into a surprise compile failure on an unrelated version-bump PR. Grep consuming repos for direct implementations of an SDK interface whenever that interface gains an Abstract* base class, not just when a breaking annotation actually lands.