84bd95275d
The finalizing subtask counter (pending_subtasks_count) could get stuck at a non-zero value, leaving knowledge permanently in "finalizing" and never promoted to "completed". Root cause and related hardening: - UpdateKnowledge does a full-row Save; pending_subtasks_count was NOT in the omit list, so any concurrent enrichment subtask that loaded the row, did slow work (LLM call), then saved an unrelated field wrote back the STALE counter, clobbering decrements other subtasks performed in between. Add PendingSubtasksCount to omitFieldsOnUpdate so only the atomic helpers (SetFinalizing / FinalizeSubtask / explicit column writes) ever touch it. - FinalizeSubtask no longer gates the finalizing->completed promote on a separate re-read of the counter. Every caller unconditionally runs the guarded promote UPDATE (WHERE pending_subtasks_count = 0), the single authoritative atomic check, so a racy/stale read can't strand the row. - Decrements run on a context detached from the caller's cancellation (context.WithoutCancel + timeout) so graceful shutdown / preemption can't silently skip a decrement. - Reconcile seeded vs actually-enqueued subtask slots (summary/question/graph) and release any shortfall so un-enqueued planned slots (e.g. graph with NEO4J off) don't strand the row. - Reparse paths reset pending_subtasks_count via an explicit column write since full-row Save now omits it. Question generation is also moved off the synchronous single-task path onto a batched async fan-out (windows of text chunks), each batch independently queued / retried / traced, grouped under a postprocess.question span.