Kraken nightly scan (issue #804) flagged LambdaProcessor as a potential propagation-context gap. Investigation confirmed: a lambda node invoked via an observer chain (or via RESET) allocated a fresh epoch instead of inheriting the parent’s, and suppressPublish=true was silently lost, allowing the lambda result to fan out to observers when it should not.
LambdaProcessor.process() delegated async work with a bare scope.launch {} rather than scope.launchProcessing(nodeManager, node). The PropagationContextElement installed by ServerNodeManager.invoke() (which pins propagationContextTL on the coroutine’s thread for the duration of the processor run) is not inherited by sibling coroutines launched from the injected application scope. The inner LambdaPythonExecutor.start() eventually calls nodeManager.succeeded() → publish(), which reads propagationContextTL.get() directly. With context lost, publish() called nextEpoch() for a fresh epoch (breaking D7 cycle-dedupe) and used DEFAULT_HOP_TTL (breaking D11 hop-count), and the suppressPublish=true guard in update() was never seen (breaking D6 terminal-RESET semantics).
This is the same class of bug as #699 (fixed in PR #700), which introduced launchProcessing for ServerComputeProcessor, ServerCalculationProcessor, ServerLLMProcessor, ServerSMTPProcessor, and ServerTaskListProcessor. LambdaProcessor was not in that sweep because it delegates to a LambdaExecutor abstraction and the context loss was one call level deeper than a direct scope.launch in process().
LambdaProcessor.kt: replaced scope.launch { try { lambdaExecutor.start(node) } catch ... } with scope.launchProcessing(nodeManager, node) { lambdaExecutor.start(node) }. launchProcessing captures propagationContextTL.get() synchronously at call time (while the caller’s PropagationContextElement is still pinned on the thread) and wraps it in a new PropagationContextElement passed into the launched coroutine. Error handling is subsumed by launchProcessing’s own reportFailure catch.PropagationContextPropagationTest.kt: added a regression test for the succeeded() → publish() path inside launchProcessing for D7 (epoch inheritance). Note: succeeded() → publish() intentionally does NOT check suppressPublish — that guard lives only in update(). If a lambda processor is invoked via RESET and should not fan out, the processor’s process() must check the verb and not call succeeded(); launchProcessing cannot suppress publish() on behalf of the processor for a RESET.process() body needs async work, always use scope.launchProcessing(nodeManager, node) {} — never bare scope.launch {}. The key test: will any code path inside the launch eventually call nodeManager.update(), nodeManager.succeeded(), or nodeManager.publish()? If yes, launchProcessing is required.LambdaExecutor, future similar abstractions) rather than directly calling nodeManager.*. The depth of the call stack doesn’t matter — the thread-local context is checked at the point of the update/succeeded/publish call.CronTask, TimerBoss, SerialDirectoryMonitor, MQTT SUB subscriber callbacks) correctly use bare scope.launch {} since they have no ambient context to inherit and root new propagations with fresh epochs.