Skip to content

Add nyan_filler: on-device Nyan Cat dark-filler - #17

Merged
sumitake merged 14 commits into
mainfrom
dev/claude/nyan-filler
Aug 7, 2026
Merged

Add nyan_filler: on-device Nyan Cat dark-filler#17
sumitake merged 14 commits into
mainfrom
dev/claude/nyan-filler

Conversation

@sumitake

@sumitake sumitake commented Aug 7, 2026

Copy link
Copy Markdown
Owner

What & why

A third integration, nyan_filler, that fills the BUSY Bar's panel with an on-device Nyan Cat animation whenever it would otherwise be black — the CI overlay's dwell gaps and idle time — except during quiet hours. This closes the "blank screen during the CI cycle" gap.

How it behaves (verified on-device, fw 1.1.1)

  • Draws at a new PRIORITY_FILLER = 5 — above the empty/stub panel (priority 0), below built-in apps (10) and every other tier. Confirmed on hardware: fills a black gap (DRAWN), and is rejected (409) by a built-in app@10 and by the CI/calendar tier@21 — so it never overrides a built-in app and yields to everything real.
  • Animation is a native .anim (bicycle0) the device self-loops; host cost is ~1 tiny re-assert draw per poll (no per-frame pushing). The asset is generated by an in-repo encoder ported from the firmware's own seq2anim.ts (tools/anim_encoder.py) and committed at assets/nyan/nyan_72x16.anim.
  • Quiet hours default 00:00–07:00 (configurable; "" disables). Per-poll INFO logging is change-gated + heartbeat (mirrors calendar_countdown) so a 1s poller doesn't flood the log.
  • Startup uploads the asset to the device; --dry-run makes zero device writes.

Design & rationale

Four on-device spikes (native .anim renders; redraw continues the loop; priority floor is 0; the ported encoder produces a device-renderable file) are recorded in the spec: docs/superpowers/specs/2026-08-06-nyan-filler-design.md. Plan: docs/superpowers/plans/2026-08-06-nyan-filler.md.

Testing

  • 372 unit tests pass (encoder round-trip, committed-asset parse, quiet-hours boundaries incl. midnight wrap, poll-loop draw/clear/backoff, log-gating, upload_asset).
  • On-device: asset renders as Nyan (rainbow + cat, animating); fills black gaps; never overrides a built-in; yields to CI tier; real agent --once uploads + draws.

Notes

  • New dev-only dependency: Pillow (used by the build tool under tools/; the shipped integration is stdlib + requests).
  • No runtime deps added; no secrets; config.toml untracked.

🤖 Generated with Claude Code

sumitake and others added 11 commits August 6, 2026 21:06
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review found the unconditional client.clear(APP) at main() startup fired
a real device write even under --dry-run, contradicting the plan's Step 6
"no device writes" acceptance criterion for dry-run mode. Move it inside
the same guard that already gated the self-healing asset upload so both
startup device writes are skipped together in dry-run.
Fix three factual inaccuracies identified in post-review:
1. --once --dry-run makes zero device calls; fix test section to recommend --once
2. Asset uploads on EVERY process start, not conditionally; fix startup section
3. poll_seconds controls host re-assertion, not animation frame rate; fix config table

All fixes verified against main.py code. Tests still green (369 passed).
Fix four more accuracy issues identified in second review round:
1. Design section: clarify upload happens on EVERY non-dry-run process start, not just first run
2. Design section: reword rebuild guidance to clarify re-uploads are unconditional
3. Test output example: use real DrawResult value 'drawn' instead of 'ACCEPTED'
4. Add quiet_hours caveat to test section (default window suppresses draw)

All fixes verified against main.py and src/busybar/client.py. Tests still green (369 passed).
…t, quiet_hours validation)

Applies all four must-fix items from the final whole-branch review:
gate per-poll INFO logging behind a change/heartbeat helper (mirrors
calendar_countdown's should_log_info), fix the broken
`python tools/build_nyan_anim.py` invocation to `-m tools.build_nyan_anim`
in both the tool's docstring and main.py's runtime warning, make
test_nyan_filler_defaults hermetic against a real repo-root config.toml,
and fail fast with a clean error (not a launchd crash-loop) on a
malformed quiet_hours at startup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d10a9845c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread integrations/nyan_filler/main.py Outdated
The .anim upload was a one-shot at startup guarded by --dry-run; if the
device was transiently unreachable at that instant, upload_asset returned
False, nothing retried it, and every subsequent draw for the life of the
process referenced an asset that was never uploaded (it worked only when
the asset happened to already be on the device from a prior run).

Move the upload into run_once behind an `asset_uploaded` latch: attempt it
on each active poll until it succeeds once, then never again — no per-poll
uploads in steady state. In the in-scope transient-unreachable case the
retries are naturally spaced by main()'s exponential UNREACHABLE backoff.
A locally missing build artifact is warned once and skipped (polling can't
fix it). Extend FakeClient with upload_asset coverage and refresh the
README section that previously said the upload never happens during polling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39a01bc657

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread integrations/nyan_filler/main.py
@sumitake

sumitake commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Robustness fix folded in: self-healing asset upload (39a01bc)

The problem. The .anim upload was a one-shot in main() at startup, guarded by if not args.dry_run, with upload_asset(...)'s return value discarded. If the local device was transiently unreachable at that exact moment, upload_asset logged asset upload unreachable, returned False, and nothing ever retried it — for the life of the process. Every subsequent draw then referenced an asset that was never uploaded. Under launchd KeepAlive a crash would re-upload on relaunch, but a long-lived process that merely blipped at startup would silently never recover. (It's been working only because the asset happened to already be on the device from prior verification.)

Approach chosen: (b) latch on upload success, retry until it lands once.

I considered the three options:

  • (a) re-upload when a draw signals the asset is missing — depends on the firmware returning a distinguishable error for a missing asset. draw collapses everything non-200/409 into DrawResult.ERROR, so we can't tell "asset missing" from any other error. Fragile.
  • (c) rely on what draw returns for a missing asset — from a live device a missing asset would be a non-200 → ERROR (it does not 200-silently the way /api/audio/play does). But confirming that requires the hardware, and building the fix on an unverified device-error contract is exactly the fragility we're removing.
  • (b) needs none of that — it's driven purely by upload_asset's own success/failure, which we already know. Picked (b).

What changed (integrations/nyan_filler/main.py):

  • New ensure_asset_uploaded(client, state); run_once calls it right before the draw on active polls. It attempts the upload only until it succeeds once (state["asset_uploaded"] latch), then never uploads again — no per-poll uploads in steady state.
  • Removed the one-shot startup upload from main() (the startup clear stays). A device transiently unreachable at process start now self-heals within the same process.
  • In the in-scope transient-unreachable case, the retries are naturally spaced out by main()'s existing exponential UNREACHABLE backoff (5 s → ×2 → 300 s cap) — so this doesn't hammer.
  • A locally-missing build artifact is a different failure (polling can't fix it): warned once, naming the rebuild command, and skipped without retry bookkeeping. If the file later appears, the next poll uploads it.

Tests (tests/test_nyan_main.py): extended FakeClient with upload_asset (records calls; a list arg scripts "fail, fail, succeed"). Added 6 cases: uploads once before the first active draw; no re-upload across 5 steady-state polls; retried each poll until success then stops; dry-run never uploads; quiet hours never uploads; missing local asset warns once and keeps looping. Stdlib + BusyBarClient only.

Docs: corrected the README paragraph that claimed the upload "never happens during polling" / "before the main polling loop starts" — it's now a self-healing upload that happens on the first active poll and retries until it lands.

Verification: full suite 378 passed locally; CI test and secret-scan green on this push.

Heads-up for anyone with dev/claude/nyan-filler checked out locally: this was a fast-forward from 1d10a9839a01bc; pull to pick it up.

@sumitake

sumitake commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

FYI for anyone with dev/claude/nyan-filler checked out locally: the branch was fast-forwarded 1d10a9839a01bc (self-healing asset upload; CI green). If your local copy is still at 1d10a98, it's one commit behind — pull before you push, and don't force-push or you'll clobber the fix. With uncommitted local work there, stash/commit first, then git pull --ff-only (fast-forwards cleanly from 1d10a98).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3867618581

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/anim_encoder.py
…pload retries

- anim_encoder: cap the per-frame duration byte at 255, splitting runs of
  256+ identical frames into chunks (was a ValueError on serialize).
- ensure_asset_uploaded: exponential backoff (5s->x2->300s cap) between
  failed upload attempts, so a persistent non-200 (device reachable for
  draws but rejecting the upload) no longer re-POSTs ~76KB every poll;
  transient failures still retry until they latch. Updated the retry test
  to advance the clock past each backoff window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sumitake
sumitake merged commit b716aef into main Aug 7, 2026
4 checks passed
@sumitake
sumitake deleted the dev/claude/nyan-filler branch August 7, 2026 19:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd05ba45a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +98 to +99
ensure_asset_uploaded(client, state) # retries until it lands once; then a no-op
result = client.draw(APP, elements, priority=PRIORITY_FILLER)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid drawing until the asset is available

On a fresh device where the asset is absent and the upload is currently throttled, rejected, or skipped because the local file is missing, ensure_asset_uploaded() returns without setting asset_uploaded, but the draw is still submitted every poll. The device then rejects the missing-asset reference and BusyBarClient.draw() logs a warning each time—up to roughly 86,400 warnings per day at the default interval—so the new upload backoff does not prevent the persistent log/request flood. Skip the draw until the upload succeeds (and likewise while the local artifact is missing).

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant