Nightly bug-hunt flagged ServerBoss as a structured-concurrency lead: start() captures val taskSnapshot = tasks.toList() and only ever launches that snapshot. addTask() calls after start() has run only appended to tasks, which was never re-read — the task was silently registered but never launched. The bug was concrete enough that an existing unit test (ServerBossTest.kt) asserted it as intended behavior: “task snapshot is taken at start-time - task added after start is not launched.”
The taskSnapshot fix in krill#346 (2026-06-04) solved a real ConcurrentModificationException race by snapshotting tasks under the mutex before launching — but that snapshot became the only source of truth for what gets launched. Nothing observed later additions to tasks, so any caller relying on addTask() being usable post-start() (hot-reload, dynamic plugin registration) would silently lose work with no error.
ServerBoss.kt: start() now launches its coroutine body with CoroutineStart.UNDISPATCHED and assigns runningScope = this as the coroutine’s first statement, so the running CoroutineScope is captured before start() returns — no race between the assignment and the coroutine actually beginning to run.addTask() now checks runningScope: if the boss job is already running, it launches the new task immediately as a structured child of that scope (same launch { } idiom the initial snapshot uses), in addition to appending to tasks for bookkeeping. If the boss hasn’t started yet, or has already exited, the task is only queued (and picked up by a subsequent start(), or dropped if the boss has fully shut down — matching the original “not started yet” semantics).scope.launch(job) { task.start() } (passing the raw Job as launch context): it works, but kotlinx.coroutines deprecates that overload precisely because parenting a coroutine to a bare Job bypasses the scope’s own context composition. Capturing the real CoroutineScope (runningScope = this) and calling unqualified launch { } on it is the supported structured-concurrency idiom, and it’s exactly the same pattern krill#553 already established for tasks launched at start() time.start()-time snapshot exists purely to protect a shared mutable collection from concurrent iteration (the krill#346 race), don’t let it become the sole schedule for that collection’s contents. If registration and consumption can happen at different times, provide the “already running” path explicit follow-up handling instead of only fixing the collection-mutation race.CoroutineScope.launch(job: Job) to parent a coroutine to an externally-held Job reference — it’s a deprecated overload. Capture and store the actual CoroutineScope (e.g., via CoroutineStart.UNDISPATCHED + runningScope = this as the coroutine’s first line) and use unqualified launch { } on it instead.ServerBossTest did here) is itself a signal worth re-reading whenever the surrounding contract changes — “task added after start is not launched” read as a spec, not a known gap, until this issue’s nightly scan flagged it.