feat(pipeline): keep every rendered surface fresh on a 4x-daily schedule - #62
Merged
Merged
Conversation
Auditing what the dashboard actually renders turned up four surfaces that had
stopped tracking reality, three of them silently. Running more often alone would
not have fixed any of them, because each was write-once by construction rather
than merely infrequent.
Dead video player. data-builder.ts never loaded .env, so a standalone
`npm run dashboard:data` computed CONFIG.VIDEOS_DIR from an unset
INSTAGRAM_USERNAME, looked for mp4s in videos/user_saved, found none, and
emitted hasVideo:false / videoPath:null for all 478 videos — the detail page
renders its <video> behind `{#if detail.videoPath}`, so nothing was playable
anywhere. In-pipeline runs were fine because runner.ts loads dotenv, which is
why this survived: the scheduled job produced a working dashboard and any
manual rebuild quietly replaced it with a broken one. Fixed by the same
side-effect import runner.ts already documents.
Write-once research and verification. Both agents skipped any entry lacking an
error, so 84% of URL liveness checks and 75% of verification verdicts were
still the ones computed in 2026-05: /tools showed three-month-old live/dead
status, and a not_verified verdict was terminal no matter how the underlying
links changed. A bare TTL would hand the next run the entire backlog at once,
so refresh.ts pairs it with a per-run cap — new and errored items never consume
that budget, so a large intake cannot starve the backlog and a run full of
failures still makes progress. ~20/run x 4 runs drains the May backlog in about
five days, then holds flat.
/tools hid 30% of the corpus. Tools were only collected from verified videos,
so 145 of 478 — and 59% of anything saved recently — contributed nothing, and a
new save was invisible until it happened to verify well on the first attempt.
They are included now and carry their status in a new Verified column, so the
table distinguishes them instead of the builder discarding them. 670 -> 957 tools.
Scheduling. Harvest at 00/06/12/18, pipeline 30 minutes later. One deep 40-scroll
sweep a day walks the whole saved feed; the other slots scroll 6, since they are
only chasing the last few hours and a deep pass every slot would multiply our
Instagram footprint for content the daily sweep already has.
Both runners now take a single-instance lock. Six-hour slots against a ~20min
run leave plenty of room, but a stalled model call or a large intake can overrun,
and two concurrent runners read-modify-write the same JSON: the loser overwrites
with a copy read before the winner's changes existed, so work disappears with no
error anywhere. macOS has no flock(1), so the guard is an atomic mkdir with
stale-PID reclaim — a killed run (rc=137 has happened) must not wedge the
schedule permanently.
Verified: 501 tests across 58 files; 13 new cover the refresh gate's TTL
boundary, cap exhaustion, error-retry, and unparseable timestamps. Lock
acquisition, refusal, release and stale reclaim exercised directly. A standalone
rebuild now yields videoPath 478/478. Both plists lint clean and load with 4
slots each.
Single Commit Policy — COMPLIANTStatus: 1 commit | Valid format | No merge commits | Ready for merge Commit details
Automated validation — dopamine single-commit enforcement |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audited what the dashboard actually renders against what the pipeline regenerates. Four surfaces had stopped tracking reality — three of them silently. Running more often would not have fixed any of them, because each was write-once by construction rather than merely infrequent.
1. The video player was dead on every page
data-builder.tsnever loaded.env. A standalonenpm run dashboard:datatherefore computedCONFIG.VIDEOS_DIRfrom an unsetINSTAGRAM_USERNAME, looked for mp4s invideos/user_saved, found none, and emittedhasVideo:false/videoPath:nullfor all 478 videos. The detail page renders its<video>behind{#if detail.videoPath}, so nothing was playable anywhere.In-pipeline runs were unaffected because
runner.tsloads dotenv — which is exactly why this survived so long: the scheduled job produced a working dashboard, and any manual rebuild quietly replaced it with a broken one. Fixed with the same side-effect importrunner.ts:1-5already documents.Verified: standalone rebuild now yields
videoPath478/478.2. Research and verification were write-once
Both agents skipped any entry without an
error, so their results never changed once written:/toolswas rendering three-month-old live/dead status, and anot_verifiedverdict was terminal however much the underlying links had changed.A bare TTL would hand the next run the whole backlog at once, so
src/pipeline/refresh.tspairs it with a per-run cap. New and errored items never consume that budget, so a large intake can't starve the backlog and a run full of failures still makes progress. At ~20/run × 4 runs the May backlog drains in about five days, then holds flat.Knobs:
REFRESH_TTL_DAYS=30,REFRESH_MAX_PER_RUN=20(0 disables).3.
/toolshid 30% of the corpusTools were collected only from verified videos, so 145 of 478 — and 59% of anything saved recently — contributed nothing. A new save stayed invisible until it happened to verify well on the first attempt. They're included now and carry their status in a new Verified column, so the table distinguishes them instead of the builder discarding them.
670 → 957 tools.
4. Schedule: 4×/day, with a deep/shallow harvest split
Harvest at 00/06/12/18, pipeline 30 minutes later. One deep 40-scroll sweep a day walks the whole saved feed; the other slots scroll 6, since they're only chasing the last few hours — a deep pass every slot would multiply our Instagram footprint for content the daily sweep already has.
5. Single-instance lock (required by the above)
Six-hour slots against a ~20min run leave plenty of room, but a stalled model call or a large intake can overrun. Two concurrent runners read-modify-write the same JSON, and the failure is silent: the loser overwrites with a copy it read before the winner's changes existed, so work vanishes with no error anywhere.
macOS ships no
flock(1), so the guard is an atomicmkdirwith stale-PID reclaim — a killed run (rc=137has happened here) must not wedge the schedule permanently. A blocked run exits 0 so launchd records a skip, not a failure.Testing
npm test— 501 tests across 58 files; 13 new cover the refresh gate's TTL boundary, cap exhaustion, error-retry, budget starvation, and unparseable/missing timestampsnpx tsc --noEmitclean;svelte-check490 files, 0 errorsplutil -lintclean and load with 4 slots each;IG_PIGGYBACK_SCROLLSunpinned so the script picks depth10#hour prefix is load-bearing — bare$((08))errors with "value too great for base", which would have broken the 08:00-era schedule