Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions packages/backend/convex/crons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ crons.interval(
// automated refresh is ever wanted again.

// ---------------------------------------------------------------------------
// Article Enrichment (Embeddings) — Every 40 minutes
// Article Enrichment (Embeddings) — Every 30 minutes
// ---------------------------------------------------------------------------
// Generates embeddings for unprocessed articles.
// Runs more frequently than ingestion to keep the pipeline flowing.
// Runs more frequently than ingestion to keep the pipeline flowing and to work
// down the unprocessed-article backlog faster.
crons.interval(
"enrich-articles",
{ minutes: 40 },
{ minutes: 30 },
internal.enrichmentNode.enrichUnprocessedArticles,
);

Expand Down Expand Up @@ -68,12 +69,20 @@ crons.interval(
);

// ---------------------------------------------------------------------------
// Stale Singleton Archive — Hourly
// Stale Singleton Archive — Every 53 minutes (drifting)
// ---------------------------------------------------------------------------
// Archives stale processing singletons so they stop inflating the vector index.
// The job yields (skips) whenever a clustering job holds a pipeline lock to
// avoid concurrent mutation of hot event/embedding rows. Convex interval crons
// are epoch-phase-aligned, so an *hourly* cadence is an exact multiple of the
// 20-min merge and 30-min recluster cadences and fired in lockstep with them
// every single time — guaranteeing a blocking lock and a 100% skip rate. A
// 53-minute cadence is coprime with 20/30/40/60, so archive drifts across
// phases and regularly lands in quiet windows without ever starving the core
// clustering pipeline (which keeps priority).
crons.interval(
"archive-stale-singleton-events",
{ hours: 1 },
{ minutes: 53 },
internal.singletonCleanup.archiveStaleSingletonEvents,
{},
);
Expand Down
12 changes: 8 additions & 4 deletions packages/backend/convex/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,14 @@ const TIER_1: FeedDefinition[] = [
mbfc: { category: "left-center", factual: "very-high", credibility: "high" },
},
{
// NOTE: endpoint returned 520/522 at verification time while the homepage
// was healthy — kept because Agerpres is the national wire service; the
// ingestion quarantine covers it if the RSS endpoint stays broken.
url: "https://www.agerpres.ro/rss/stiri",
// Agerpres has no native RSS: www.agerpres.ro/rss/stiri 301-redirects to an
// allorigins.win proxy wrapper that returns 520/522 (dead), which kept the
// feed permanently quarantined. The underlying bazqux "createfeed" extractor
// over the Agerpres widget returns valid RSS directly, so we point at it
// without the broken proxy layer. If this third-party extractor ever fails,
// the ingestion quarantine covers it. Kept because Agerpres is the national
// wire service.
url: "https://createfeed.bazqux.com/extract.php?url=https%3A%2F%2Fagerpres.ro%2Fwidget&max=50&order=document&guid=0",
name: "Agerpres",
domain: "agerpres.ro",
tier: 1,
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/convex/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,13 @@ export const checkPipelineAlerts = internalAction({

const byJob = new Map<string, { total: number; ok: number }>();
for (const log of logs as Array<Doc<"pipelineRunLogs">>) {
// A "skipped" run is a deliberate no-op (e.g. a job yielding to a
// pipeline lock, or short-circuiting because it was already running).
// It did no work, so it must not count as a failure toward the
// error-rate SLO — otherwise jobs that intentionally yield (like the
// stale-singleton archive) flap below 80%. The "job stuck skipping"
// failure mode is still caught by the per-job absent-ok-run checks.
if (log.status === "skipped") continue;
const row = byJob.get(log.jobName) ?? { total: 0, ok: 0 };
row.total++;
if (log.status === "ok") row.ok++;
Expand Down