Skip to content
Consultation

Deleting the RAMP Table That Haunted Our Scheduler

Adityo Guni Waluyo

We deleted the daily 6+6 article cap and went event-driven: per-entry cursor, cycle budgets, and a local bge-m3 embedding dedup.

I was staring at the RAMP table in our internal agent repo, right before commit 7394e01. It was a block of staged targets from v1: day 1 = 2 articles, day 7 = 12, ending at a hard 6 Indonesian + 6 English per day. The scheduler had just woken the agent at a fixed hour to check that cap, and the commit timeline was empty. Again.

That was the moment I knew the quota was a **vanity metric**.

The RAMP table

V1 froze on 2026-08-27 with that ramp table baked in code. Every cycle the scheduler woke the agent on a fixed schedule, even when there was nothing new to write. Empty wake-ups burned tokens for zero output. People love fixed schedulers like Vercel Cron Jobs (they fire by HTTP GET to a path in vercel.json and only on production deployments Vercel Cron Jobs docs, launched Feb 2023 the launch changelog), but any clock-driven trigger means dead runs.

V3 threw the table out. No daily cap at all. The trigger is a new entry on our internal commit timeline, checked every 15 minutes by a no-LLM monitor. That monitor just polls the GitHub REST list commits endpoint (GET /repos/{owner}/{repo}/commits with since/per_page params) GitHub REST commits endpoint. At 4 requests an hour we're nowhere near the authenticated GitHub REST rate limit of 5,000 requests/hour per user 5,000 requests/hour per user. Zero tokens spent while idle.

The honest cursor

The subtle shift is what counts as progress. Under the cap system, progress was output volume: articles per day, no matter why. Under the cursor, progress is state that can't be faked: a slug live on two locales, an entry permanently settled. When everything can be reconstructed from the timeline plus the covered index, there's nothing left to babysit.

The old cap --strict command is gone. In its place: a per-entry cursor that only moves on proof. Pending entries get processed in batches of max 3 per cycle, oldest first. You advance an entry with advance <id> **ONLY** after a successful publish in both languages.

Mid-flight crash? The same entry retries next cycle. Failed publish after 2 retries hits fail --reason validation, permanently indexed so it can't wake-loop. An anti-overlap lock keeps a new cycle from starting while the previous one runs. Budgets are tight: max 3 entries/cycle, 15 minutes research/entry, 2 publish retries.

I did consider a webhook from our internal forge instead of polling. The stack we run doesn't expose one today, though, and a 15-minute poll is already responsive for a pipeline whose own cycle takes ten-plus minutes per entry. Not worth adding a new component just to shave a few minutes of latency.

I personally prefer this over any quota. A pipeline that advances only on proof of publish is honest. Daily quotas make sense for a human editorial calendar, not for a bot.

The cheap dedup

We didn't want the agent rewriting the same commit twice. So we built 3-layer dedup: exact meta.source_commit match, then covered index, then embedding similarity. The embedding step runs locally through Ollama /api/embed, which returns L2-normalized vectors and supports batch input Ollama's /api/embed. We use bge-m3 (BAAI) at 1024 dims, 8K context, 100+ languages bge-m3 (BAAI). Because vectors are L2-normalized, cosine similarity is just a dot product.

A new commit message gets scored against topics of published articles. 0.75-0.90 = ambiguous, agent decides manually. Once calibration ends, >=0.90 auto-skips. No API bill, no external calls.

Until then every score just gets logged, so we can watch the real distribution for two or three weeks before pinning the final thresholds.

We shipped v3 with +350/-57 lines, RAMP table deleted. The pipeline now sleeps until a commit actually appears, and I'd take a quiet day over filler any time.

Related articles