fix(save-pipeline): count user-facing save failures + stop the generate-summary DLQ loop#937
fix(save-pipeline): count user-facing save failures + stop the generate-summary DLQ loop#937FagnerMartinsBrack wants to merge 1 commit into
Conversation
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 11m 38s —— View job High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1. The "out of scope" note's
|
|
|
8cf9e38 to
68bd979
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 6m 25s —— View job PR Review: fix(save-pipeline): count user-facing save failures + stop the generate-summary DLQ loop
High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1. The two outcome-agnostic save-intent widgets now fold validation errors into
|
|
|
|
@claude Resolve conflicts. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 27m 36s —— View job Conflicts resolved ✅
Rebased
The PR's substantive changes ( Verification —
|
…ummary DLQ loop Emit view_save_intent outcome="error" on every synchronous save-validation 422 (queue save bar, extension save-article/save-html/save-content) so the save funnel finally has an error count; buildSaveIntentEvent now derives article_host via a null-safe parser, making article_host/content_class nullable (dashboards already coalesce these). Adds a "Save errors by surface" dashboard widget. In generate-summary, when canonical content is absent AND the aggregate row's crawl.kind is failed/unsupported, mark the summary skipped (crawl-failed/ crawl-unsupported) instead of asserting — this terminal collapse stops the auto-heal from repriming the row forever and ends the ~170/mo DLQ loop. The crawl=pending race and crawl=ready inconsistency keep the assert -> DLQ path. Claude-Session: https://claude.ai/code/session_01En2uUiiwQoRQKWenCnkPi9
68bd979 to
c29b75e
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 4m 34s —— View job PR Review: fix(save-pipeline): count user-facing save failures + stop the generate-summary DLQ loop
Re-review of the rebased/conflict-resolved commit High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1. The two outcome-agnostic save-intent widgets fold validation errors into
|
|
|
fix(save-pipeline): count user-facing save failures + stop the generate-summary DLQ loop
Hypothesis
Two visibility holes hide save-pipeline pain: (1)
view_save_intenthas anoutcome="error"enum value that is never emitted for the failures users actually hit (URL validation 422s), so the save funnel reads as if saves never fail; (2) thegenerate-summaryworker treats "canonical content absent because the crawl terminally failed" identically to "content not yet written", producing a permanent retry/DLQ loop via the summary auto-heal that fires almost daily and drowns the errors dashboard.Evidence (30d, Jun 4 – Jul 4 2026)
view_save_intent outcome="error": 0 events in 30d — yet the enum exists and the catch-path emits are wired. EveryvalidateSaveableUrl422 branch (queue save bar, extension save-article/save-html/save-content) renders a user-facing save error without emitting.AssertionError "Article content not found: <url>", ~520 retry ERROR lines, near-daily. Mechanism (verified in code):markCrawlExhaustedleavessummary=failed(crawl-failed)→ stale-check auto-heal reprimes anysummary=failedrow (3 attempts, then one round per 24h forever) → GenerateSummary finds no canonical content (crawl is terminal) → assert → 4 receives → DLQ →markSummaryExhausted→failed(exhausted-retries)→ eligible again next tick. The 73 crawl-failed dead-lettered anonymous first-saves are the seed population."exhausted-retries (receiveCount=4) (receiveCount=4)"(167/170 events): already fixed on main by fix(summary): stop doubling receiveCount in exhausted-retries parse-error #886 (commit3dd818a5, Jul 3) with a regression test — intentionally not touched here.What this changes
view_save_intent outcome="error"on every synchronous save failure: queue save bar validation 422, extension save-article validation 422, save-html invalid-request/invalid-URL 422s, save-content missing-field/invalid-URL/unsupported-media/not-a-pdf 422s.buildSaveIntentEventnow derivesarticle_hostvia a null-safe parser (article_host/content_classbecome nullable — dashboards alreadycoalesce()these). Deliberately not instrumented: bulk save-all-tabs per-entry skips (expected filtering of chrome:// tabs; per-job failures already emit), multipart-parse 422s/406s (no URL available), and async crawl failures (out of scope by design).crawl.kindis"failed"/"unsupported", mark the summary skipped ("crawl-failed"/"crawl-unsupported"— the same terminal collapsemarkCrawlBlocked/markCrawlNotFoundalready write) instead of throwing. Skipped is terminal: the auto-heal gate stops repriming, the loop ends, and existing stuck rows self-drain on their next (final) auto-heal round — no migration. The genuine race (crawl=pending) and genuine inconsistency (crawl=ready, content missing) keep the assert → retry → DLQ path.Estimated result
How to measure
Save errors (hutch handler log group):
DLQ decay (generate-summary log group):
Expect the first query to go nonzero within days and the second to decay to zero within ~48h.
Conflict note
analytics-dashboard.test.tsasserts an exact widget count (27 on main at time of writing; this PR bumps it to 28) plus non-overlapping grid positions. Sibling PRs #916–#918 touching the dashboard will trivially conflict on that literal and possibly the grid row at y:122. Resolution: after rebase, re-count thewidgets.pushentries, set the literal to the real total, and shift this PR's widget to the next free 12-wide slot if taken.Out of scope (deliberate)
Crawler-canary follow-up for the top DLQ'd hosts (fagnerbrack.com ×9, trends.google.com ×6, unsplash.com ×5, reddit/HN/npmjs ×2 each) is not attempted here: per CLAUDE.md's crawler-health rule, each fix starts by reproducing the failing fetch locally against the real URL and iterating until the canary passes — a separate, per-host effort. This PR only stops those already-failed rows from generating infinite summary retries. Also untouched:
markCrawlExhausted'ssummary=failed(crawl-failed)write (converting it to skipped would disablerecrawlTieKeptCanonical's recovery predicate — candidate follow-up).https://claude.ai/code/session_01En2uUiiwQoRQKWenCnkPi9