Three bugs surfaced by the Dove2 demo run on prod and addressed together
because they all conspire to make the run page look dead:
1. Apify cost events were never persisted. Stage 2 imported onApifyCost
and registered an empty no-op callback inside its run loop, silently
overwriting the CLI's DB-writing handler set in cli.ts:logCost(). The
APIFY total stuck at $0.00 even though Stage 2 had spent $5+ in real
billing. Removed the override; the CLI's callback now wins.
2. Stage 4 inherited Stage 2's Pass-1 soft cap and skipped every actor.
resetBudget() sets a hard ceiling (95% of brief.budget_usd) and
setSoftCap() sets the Pass-1 cap (50%). Stage 2 fills the soft cap,
but Stage 4 never released it — every TIKTOK_TRANSCRIPTS / COMMENTS /
PROFILE call returned "budget reached — skipping" and the manifest
gate failed at 0% coverage. Stage 4 now calls setSoftCap(null) at
entry so it stays bounded only by the hard ceiling.
3. Even between cost events the run page had no liveness signal. Apify
actors run 1-3 minutes per scrape with no DB writes in flight, so the
UI looked frozen. Added a best-effort heartbeat: apify_client writes
.state/live_activity.json on every Apify status poll (every 5s),
GET /reports/:id includes it on the response, and the run page shows
a live banner with the current activity, elapsed time, last
heartbeat age (flags as suspicious past 90s), and running Apify
spend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>