Per-chunk reveal: refetch the graph when a JOB row leaves the feed, not when the source drains

The reveal rule (c902bc6: a run leaving the feed means its merge is on
disk, so refetch immediately) was tracking chatId sets — fine when one
row meant one source, but per-chunk jobs made it re-batch the reveal:
chunks finish one by one, each row leaves, but the source's chatId stays
present until its LAST chunk drains, so a big import showed nothing
until it fully completed. The exact batching bug the rule exists to
prevent.

Track by row id (the registry job key) in both the snapshot-reveal poll
and the per-run costs refresh — a chunk row disappearing IS that
chunk's merge landing.
This commit is contained in:
amosblomqvist
2026-09-29 01:32:53 +03:00
parent 4001c3465b
commit 92146248d2
+9 -8
View File
@@ -1067,9 +1067,10 @@ export function GraphView() {
// merge is its own graph change and must surface immediately, not wait
// for the whole feed to drain. A redundant fetch on aborted runs is
// free — the same doctrine as the deletion-refresh path below.
// Row ids are the registry JOB keys (source id, or "src#i" for chunk
// jobs) — unique per pill; chatId stays the SOURCE id (reveal/cost
// tracking is source-scoped).
// Tracked by ROW id (the registry JOB key — source id, or "src#i" for
// chunk jobs), NOT chatId: chatId-scoping re-batched per-chunk jobs
// into one end-of-import reveal (the exact bug this rule exists to
// prevent — a chunk's row leaving IS its merge landing).
const [pipelineRuns, setPipelineRuns] = useState<{ id: string; chatId: string; title: string; phase: string; startedAt: number }[]>([]);
useEffect(() => {
let cancelled = false;
@@ -1080,7 +1081,7 @@ export function GraphView() {
.then(({ runs }) => {
if (cancelled) return;
setPipelineRuns(runs);
const ids = new Set(runs.map((r) => r.chatId));
const ids = new Set(runs.map((r) => r.id));
if ([...prevIds].some((id) => !ids.has(id))) setRefreshTick((t) => t + 1);
prevIds = ids;
})
@@ -1111,13 +1112,13 @@ export function GraphView() {
refreshCosts();
}, [refreshCosts]);
// The same disappearance rule as the snapshot refresh (owner
// 2026-09-28): usage persists per run, so costs refresh per run —
// not only when the whole feed drains.
// 2026-09-28): usage persists per JOB (per-chunk jobs), so costs refresh
// per job row — not only when the whole feed drains.
const prevRunIds = useRef(new Set<string>());
useEffect(() => {
const gone = [...prevRunIds.current].some((id) => !pipelineRuns.some((r) => r.chatId === id));
const gone = [...prevRunIds.current].some((id) => !pipelineRuns.some((r) => r.id === id));
if (gone && prevRunIds.current.size > 0) refreshCosts();
prevRunIds.current = new Set(pipelineRuns.map((r) => r.chatId));
prevRunIds.current = new Set(pipelineRuns.map((r) => r.id));
}, [pipelineRuns, refreshCosts]);
/* Deletion refresh (wave 6 chunk A): the DELETE /api/chats/:id response