Replay auto-upload (Dev builds) design
Replay auto-upload (Dev builds), design
Section titled “Replay auto-upload (Dev builds), design”Approved by Chris 2026-10-14 (auth option (a): no key, hard caps).
Dev builds record every human run as an input replay (DevFlags.record_replay,
main._save_replay_if_recording). On tvOS the only writable place is Library/Caches, which the
OS may purge under storage pressure, and a copy only reaches the Mac when someone remembers to pull
it with devicectl. Toby’s first recorded runs were lost (a save bug, fixed 065d4f78), and relying on
“tell Claude someone played” is the discipline-based fix this repo avoids. Replays must leave the
device on their own.
What exists (2026-10-14)
Section titled “What exists (2026-10-14)”- Telemetry Worker
telemetry/src/worker.js: open ingest routes (CORS*, no client key), D1DB, KVCONFIG, daily cron withpurgeOldoverRETENTION_TABLES(D1 only)./eventscaps at 512 KB and 20 events. No R2 binding anywhere. - Client:
net/event_queue.gdposts text withHTTPRequest;mutedcovers playback, capture and showcase runs; the player’s Share diagnostics toggle gates sending. Nothing compresses a body or usesrequest_raw. - Channel:
BuildChannel.current() == "dev"on the tvOS-Dev / iOS-Dev / Android-Dev presets, BUT the root Web preset also carriescustom_features="dev", so channel alone is not a safe gate. - Replay files:
replay-latest.json(overwritten each run) +replay-<unix>-<seed>.json(one per run), saved touser://or, on tvOS,OS.get_cache_dir(). ~1.5 MB of JSON per 5 min of play.
Design
Section titled “Design”Client: net/replay_uploader.gd (new, render-side Node)
Section titled “Client: net/replay_uploader.gd (new, render-side Node)”- Gate:
BuildChannel.current() == "dev"AND notOS.has_feature("web")AND not muted (playback, capture, showcase) AND Share diagnostics on ANDEventQueue.live_platform()(never headless/editor, so tests inject the transport). - The files on disk are the queue. No new queue: the uploader scans the replay dirs for
replay-<unix>-<seed>.json(neverreplay-latest.json), uploads one at a time, oldest first, and deletes a file only after the Worker answers 200 with{"ok":true,...}. A failure leaves the file for the next pass. - When: a pass runs at boot (after the prewarm) and right after each replay save. Backoff on failure mirrors the event queue (10 s doubling to 300 s); a pass never runs while a human run is in progress, so uploads never compete with gameplay frames.
- Body:
file bytes.compress(FileAccess.COMPRESSION_GZIP)sent withHTTPRequest.request_raw,Content-Type: application/gzip, querybuild,install(the telemetry install id),unix,seed. replay-latest.jsonstays on device untouched (local convenience).
Worker: POST /replay on the telemetry Worker
Section titled “Worker: POST /replay on the telemetry Worker”- New R2 binding
REPLAYS(bucketbh-replays). - Validates before storing: gzip body ≤ 2 MB (else 413); decompresses with
DecompressionStream("gzip"), decompressed ≤ 16 MB; parses JSON and requires the replay shape (seedinteger,ticksarray,fresh_savebool) andseed== the query seed. Anything else → 400{"error":...}(the client then deletes it, matching the event queue’s “the Worker’s own error drops it” rule, so one bad file cannot block the queue forever). - Daily cap: at most 200 stored objects per UTC day (counter in KV
CONFIG), 429 over it (client retries later). This plus the shape check is the abuse bound in place of a key. - Stores the ORIGINAL gzip bytes at
replays/<build>/<install>/<unix>-<seed>.json.gz, with custom metadata (build, install, unix, seed, ticks count). Idempotent: an existing key returns 200 (a retried upload after a lost response must not duplicate or fail). - Retention: an R2 lifecycle rule deletes objects after 365 days (the privacy page’s 12-month
promise). Set once by hand with wrangler, documented in
docs/architecture/dev-tools.md§ Telemetry v2.
Mac: scripts/replays-pull.sh
Section titled “Mac: scripts/replays-pull.sh”Lists bh-replays via wrangler and downloads every object not already present into gitignored
replays/ (same <build>/<install>/ layout, gunzipped). Prints the new files with tick count and
duration. Existing hand-pulled copies in .superpowers/replays/ move there.
Privacy
Section titled “Privacy”Add a “development builds only” line for gameplay input replays to
~/Claude/bullet-heaven-site/privacy/ (deploy via that repo’s scripts/deploy-site.sh) and
marketing/appstore/compliance.md, following the LAN co-op diagnostics precedent. Store/TestFlight
builds never upload replays (they never record them: DevFlags are all-false there).
Testing
Section titled “Testing”- GUT: uploader gate truth table (dev/web/muted/diagnostics); queue selection (oldest first, skips
replay-latest.json); delete-only-on-200; keep on network error and 429; drop on 400; gzip round trip of a real recorder file. Injectable transport, scratch dirs (_replay_dirsstyle), and a test of the DEFAULT dir list (the 2026-10-14 bug was a test seam hiding the default branch). - Worker (node tests,
telemetry/test/): valid upload stored with metadata; idempotent re-upload; 413/400 on oversize, non-gzip, bad shape, seed mismatch; 429 at the daily cap. - Device: a Dev ATV install, an idle autostart death, the object appears in R2, the on-device archive
file is gone,
replays-pull.shfetches it and it matches.
Out-of-band steps (Chris’s OK at the time)
Section titled “Out-of-band steps (Chris’s OK at the time)”wrangler r2 bucket create bh-replays + lifecycle rule, telemetry/deploy.sh (also clears the
pending console/telemetry redeploys), privacy site deploy, Dev install.
Not doing
Section titled “Not doing”No D1 table or dashboard for replays (R2 listing is enough), no upload of tutorial-replay.json
yet, no store/TestFlight path.