Backend: summary, question, and graph-extract async tasks now record
real processing time as subspans hanging off the (closed) postprocess
stage, so the trace viewer no longer caps the postprocess row at the
~10 ms enqueue duration. Carries Attempt through SummaryGeneration /
QuestionGeneration / ExtractChunk payloads so cross-process workers
can resolve the right parent attempt.
Frontend: drawer now uses attach="body" so the secondary 820px detail
drawer escapes the 654px container of the main drawer; timeline
timestamps include date prefix (MM-DD, or YYYY-MM-DD across years);
"updated X ago" caption only shows during live polling.
Tests: 4 new cases covering postprocess subspan attaching under a
closed parent, missing-parent fallthrough, and Attempt JSON round-trip
on the three task payloads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Collapse migrations 000052 (flat stages) + 000053 (span tree) into a
single 000052_knowledge_processing_spans migration; the flat stages
table never escaped this branch and the create-then-drop sequence had
no value.
- BeginStage: detect an existing (kid, attempt, stage) row before
inserting and reuse its span_id with reset state, so re-entry from
asynq retries or adjacent code paths no longer produces duplicate
timeline segments.
- FailSpan: when sibling-stage cascade flips a dependent stage to
cancelled, also CancelDescendants on its subtree so already-running
subspans (embedding.batch[i] etc.) cannot remain as orphan running
rows under a cancelled parent.
- Dead-letter callback: replace two sequential UpdateKnowledgeColumn
writes with a single UpdateKnowledgeColumns map update so we cannot
end up with parse_status=failed and stale error_message (or vice
versa) when one of the two writes fails.
- touchKnowledgeHeartbeat: skip subspan/generation transitions; only
root and stage transitions poke knowledge.updated_at. Spans-table
MAX(updated_at) already covers subspan progress for housekeeping, so
this avoids 2*N+ UPDATE bursts on the same hot row when a multimodal
stage fans out to many images.
- Add regression tests for the BeginStage idempotency contract and the
cascade-into-subspans behaviour.
Addresses three review concerns from the prior PR:
1. No tests existed for any of the stuck-parsing fixes (PR① / PR②.5).
This commit adds coverage for the four most regression-prone
surfaces: span repo upsert/cascade/attempt isolation, span
tracker cascade-cancel and cross-process LookupStage,
housekeeping false-kill protection, and handler tree assembly.
2. Housekeeping was using only knowledge.updated_at as its staleness
signal, but knowledge.updated_at advances only at parse_status
transitions — a long DocReader call (or large embedding batch)
can run for an hour with no updated_at change, so a tight
DocumentProcessTimeout setting would falsely flip an actively
running parse to "failed".
The sweep now does a two-stage check: candidates by knowledge
updated_at, then filtered by MAX(spans.updated_at). Every
SpanTracker Begin/End/Fail/Skip now also pokes
knowledge.updated_at as a side-channel heartbeat, so the
filter sees recent activity even when no parse_status
transition has fired.
3. parseHeartbeatTime accepts the timestamp formats both Postgres
and SQLite emit for an aggregated MAX() column (the SQLite
driver doesn't auto-cast aggregates to time.Time the way
Postgres does), so the same code path works in Lite mode.
The new TestHousekeeping_NoFalseKill_ActiveSpan is the regression
test for the user-flagged scenario: a 3-hour-stale knowledge.updated_at
combined with a 2-minute-fresh span row must NOT be killed.