Skip to content

refactor(display): run loop stage 3 -- ScreenRunner, PREEMPTED, OnDemand/Live/Rotation Sources - #762

Merged
ChuckBuilds merged 6 commits into
mainfrom
claude/run-loop-stage3
Oct 5, 2026
Merged

ChuckBuilds merged 6 commits into
mainfrom
claude/run-loop-stage3

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Stage 3 of docs/RUN_LOOP_REDESIGN.md: a ScreenRunner runs each screen, and the Arbiter now decides every screen except Vegas. No behaviour change is intended. Do not merge yet: this moves the frame loops, so it needs the ledpi frame soak below first.

Design

src/screen_runner.py (new, on the mypy ratchet). ScreenRunner(clock, host).run(plan, plugin) -> Outcome.

  • It draws the first frame, runs the 125 Hz or 1 Hz frame loop that plan.frame_policy picks, makes up the minimum duration, and handles the dynamic-duration exit. All of this moved out of run() unchanged, including the 8 ms deadline pacing, the 1 ms minimum yield, and the 1 Hz wait with socket wake.
  • The clock is injected (FrameClock: time, perf_counter, sleep). In production it is _ModuleClock, which looks up src.display_controller.time on each call, so the harness's fake clock still drives it.
  • ExitReason is one of DURATION, CYCLE_COMPLETE, EMPTY, ERROR, DISPLAY_FALSE, RELOAD or PREEMPTED.
    • PREEMPTED replaces the five "did the mode change under this screen" re-checks. On PREEMPTED, run() goes to the next pass without advancing.
    • RELOAD is the one early end that still advances, as a pending plugin reload always did.
  • The controller implements ScreenHost through _ScreenHost, which is one-line forwards to its own methods.

Sources. src/display_arbiter.py gains ON_DEMAND, LIVE and ROTATION. LEGACY now means only Vegas.

  • ArbiterState gains:
    • the current mode
    • the rotation and its index
    • the on-demand modes, index, expiry and pin
    • the live resume point
    • the "takeover not shown yet" flag
  • ArbiterInputs gains:
    • live_modes (None where no scan was made)
    • vegas_enabled, vegas_live_in_ticker and vegas_yielded
    • reload_pending (mid-screen only)
  • ScreenPlan gains:
    • mode, plugin, min_duration, max_duration, dynamic, frame_policy and preemptible_by
    • deadline (the on-demand expiry) and ends_live

The plugin's answers (durations, dynamic, frame policy) are filled in after the first frame, where they were always read. decide() is pure, so it can't ask a plugin.

What each Source decides:

  • OnDemand answers with the session's current mode. Its max_duration is what is left of the session at now. on_demand_bound() is _clamp_to_on_demand made pure, and is still applied after the first frame.
  • Live answers with the next live mode, round-robin (live_pick, now also behind _check_live_priority). It outranks Vegas unless the ticker keeps live content. With nothing live, the plan below carries ends_live and the rotation resumes.
  • Rotation answers with state.current_mode. That is available_modes[current_mode_index] except where something moved the panel off the list, and run() always showed current_display_mode.

The state transitions are pure methods: next_on_demand, showing, claim_live, release_live and after(outcome). after replaces _advance_after_screen's step. The controller's attributes stay the record, because the web UI, the socket and the cache read them. _arbiter_state() snapshots them, and _adopt_state() writes a transition back.

A pass asks decide() twice:

  1. With the inputs every pass reads.
  2. Only when nothing above the notice took the panel: with the Vegas check and the live scan, read where run() always read them. The scan asks every live-priority plugin, and _is_vegas_mode_active applies queued Vegas config, so reading them on a follower or notice pass would change behaviour.

A Vegas iteration that yields asks once more, with vegas_yielded.

One decide() call at the runner's service points. decide(state, inputs, now, running=plan) returns running itself while the screen holds, else the plan that ends it. The rules come in the order the loops checked them:

  1. a live takeover, claimed even if a notice is also pending (the next pass shows the notice, then the game, as before)
  2. the mode moved under the screen
  3. the schedule is off
  4. a WiFi notice, compared with its expiry
  5. a plugin reload

preemptible_by gates each rule. Every screen is preemptible by everything except a follower and Vegas, and a live screen leaves Live out.

The service points are FRAME, AFTER_LOOP/AFTER_COMPLETED_LOOP, after_dwell and FINAL. Each checkpoint says whether a reload counts there and when the WiFi file is read (NoticeRead). The read is throttled and deletes an expired file, so it happens exactly where it did. _screen_preempted, _resolve_active_mode and _clamp_to_on_demand are gone. _check_live_takeover and _wifi_notice_pending remain for the dwell sleep and the Vegas yield path, built on the same rules.

Not converted, on purpose. _sleep_with_plugin_updates keeps its own break rules:

  • It also serves the blank, the notice and the idle wait, which are not screens.
  • Its rules are edge-triggered. An on-demand session starting on the mode already showing ends a dwell but not a frame loop, so folding them into decide() would change behaviour.

Commits

Rebased onto #758 ("report a scrolling screen held by its plugin's update()"). Its report_hold=True on the 125 Hz loop's frames is carried by _ScreenHost.draw, which passes it when plan.frame_policy is HIGH_FPS; a test pins this.

  1. The runner, still with the old checks behind the host.
  2. The OnDemand Source.
  3. The Live Source, with the Vegas iteration moved to _run_vegas_iteration.
  4. The Rotation Source (LEGACY = Vegas only) and ArbiterState.after.
  5. One decide() call at the service points.
  6. Docs and CHANGELOG.

Golden traces

Byte-identical. Nothing was regenerated, and LEDMATRIX_REGEN_GOLDEN=1 produces no diff. That covers all 17 scenarios, including #748's on_demand_named_live and on_demand_restore_failed and #713's live_priority and vegas, plus test_run_loop_wifi_and_live.py.

A stricter check ran after every commit. Every RunLoopHarness run in the suite was captured (67 runs: the goldens plus the live-takeover, WiFi+live, socket-wake, plugin-reload, schedule, tick and duration tests). The capture logs every fake-clock sleep, display() call, WiFi file read, live scan, has_live_content() call, publish, dwell and scroll-state call. It was diffed event by event against origin/main, and it is identical except:

  • A Vegas pass scans the live plugins once, not twice at the same instant. Before, step 7 scanned and then step 8 asked "is anything live?" again.
  • _apply_live_priority(None) calls that changed nothing are not made.
  • Throttled WiFi reads that returned the cached answer are not made after a notice has already ended the screen. Such a read has no side effect.
  • One event moves 8 ms (one scenario). In the 125 Hz loop the live scan still runs before the frame's sleep, at the same instants, but the claim is now made by the service point after the sleep. So test_live_game_cuts_a_scrolling_screen_short logs live at 8.072 instead of 8.064. The screen ends at the same frame, 8.072, and every frame, sleep and pass is unchanged. Keeping the claim before the sleep would have needed a second decide() call per frame.

Tests

  • test/test_display_arbiter.py gains:

    • the OnDemand table and the bound table
    • live_pick, the Live table and the claim/release transitions
    • the Vegas/Rotation table and after
    • the 24-row mid-screen table, with purity checks
    • the live_takeover table

    The stage-2 rows that said LEGACY for "on-demand" or "live/Vegas/rotation" now name ON_DEMAND and ROTATION.

  • test/test_screen_runner.py (new) tests the runner on a scripted host and fake clock:

    • each ExitReason
    • the call order per frame
    • 8 ms pacing and the 1 ms yield
    • service before the sleep, decision after it
    • the make-up dwell and the notice-after-dwell rule
    • dynamic grace and cap

    It also tests the controller's service point: which checkpoint reads the notice, whether a live takeover is claimed before the notice, and that a reload counts only between frames.

  • Mutation check: 46 mutations, each breaking one moved or new piece once (OnDemand, Live, Vegas/Rotation, after, each mid-screen rule, the runner's pacing, exits and service points, and the controller's gathering and claims). All 46 fail at least one test. The first run left three survivors, and each now has a test:

    • the after-loop service point not reading the notice
    • the Vegas yield not marking vegas_yielded
    • _take_plan not writing back a reset on-demand index

    The second survivor turned up a pre-existing oddity, recorded under "may be wrong" in the doc: Vegas stops for a sync follower, but a full rotation screen runs before the follower gets the panel. It is pinned by a test that passes on main too, and is left for its own PR (stage 4 is the natural place).

  • Full suite against an origin/main baseline worktree (caec9f5), both run at the same time on this Windows host. The FAILED/ERROR ids are identical: main 35 failed + 6 errors (8550 passed), branch 35 failed + 6 errors (8686 passed; the extra passes are the new tests). These are the known Windows-only failures. test/test_install_lowmem.py was excluded from both runs because its shell children hung under load from other sessions' suites; it touches nothing here. CI's Linux core suites (3.11, 3.13) and every other check pass.

  • scripts/check_types.py with mypy 1.20.2: clean (95 files, src/screen_runner.py added to mypy-clean.txt).

Soak plan (ledpi, please run before merging)

ledpi is the Pi 4 with the Triple Bonnet and a 2x96x48 panel. Plain main is about 0.11-0.16 % late there, and every arm fails the default 0.1 % gate. Judge the branch against alternated main arms, not against 0.1 %.

  1. Before deploying, run git status on ledpi. Other sessions leave src/ edits there; save them with git diff > ~/other-session-edits-<ts>.patch before checking out. Re-check git log -1 before each arm.
  2. Run the arms A B B A (A = main at this PR's base, B = this branch), 20 min each, with the service running. Use python3 scripts/frame_soak.py --preview via ~/ab/run_arm.sh <label> 1200, on the usual rotation: news ticker, then clock-simple, then football, then baseball, Vegas off.
    • Compare: late-frame %, freezes, stall-after-static counts and CPU.
    • Pass: B within the spread of the two A arms.
  3. Vegas arms: repeat A B B A with Vegas on for 10 min each. The iteration is still run_iteration(), but its entry, yield handling and the screen after a yield moved.
  4. Watch the 125 Hz loop's per-frame cost. Each frame now builds a small ArbiterInputs and calls decide(), about 1 µs on the desktop, so maybe 10-20 µs on a Pi 4 against an 8 ms frame. If B's late % is above A's, profile _screen_check first.
  5. By hand on B:
    • on-demand start, stop and expiry (including one whose plugin was loaded only for on-demand)
    • a live game taking over mid-screen on a static screen and on a scroller, then handing back to the screen it cut short
    • the schedule turning the panel off mid-screen and back on
    • a WiFi notice mid-screen
    • a plugin reload from the store mid-screen

Follow-ups and overlaps

🤖 Generated with Claude Code

ChuckBuilds and others added 6 commits October 4, 2026 17:52
…tage 3)

The two frame loops, the make-up dwell and the dynamic-duration exit move
out of DisplayController.run() into src/screen_runner.py. ScreenRunner
paces with an injected FrameClock (production: this module's time, looked
up per call so the golden harness's fake clock still drives it) and
returns one Outcome whose ExitReason is DURATION, CYCLE_COMPLETE, EMPTY,
ERROR, DISPLAY_FALSE, RELOAD or PREEMPTED.

PREEMPTED replaces the re-checks that used to follow each frame loop and
the make-up dwell (current_display_mode != active_mode, the schedule, a
pending WiFi notice): the runner asks its host at named service points
(FRAME, AFTER_LOOP, after_dwell, FINAL), and on PREEMPTED run() goes to
the next pass without advancing the rotation, as each `continue` did.
RELOAD is the one early end that still advances, as a reload always did.
Each service point reads the WiFi notice file exactly when the loop did
(NoticeRead), because the read is throttled and deletes expired files.

The host answers still use the old checks; the following commits move
them to the Arbiter. _screen_preempted is gone (folded into the FRAME
check); _wait_frame_interval returns the preempting plan instead of a
bool. The frame pacing (8 ms deadline, 1 ms minimum yield, 1 Hz wait with
socket wake) is the same code, moved.

Golden traces unchanged. A capture of every harness run (all 67, with
every sleep, display() call, wifi read, live scan, publish and dwell
logged) is identical to origin/main except for throttled WiFi reads that
returned the cached answer (no side effect) after a notice preempted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arbiter.decide() now answers for an active on-demand session itself
(Source.ON_DEMAND) instead of returning LEGACY:

- ArbiterState gains the session: its mode list, index, expiry and pin,
  plus current_mode, snapshotted from the controller's fields by
  _arbiter_state(). The controller's attributes stay the record that the
  web UI, the control socket and the cache read.
- The OnDemand plan is the session's current mode (an index past the end
  of a shortened list starts again at 0), with what is left of a timed
  session at `now` as max_duration and the expiry as deadline. A session
  with no modes left is a plan with no mode; the controller ends it and
  shows the rotation's mode, as _resolve_active_mode did.
- on_demand_bound() is _clamp_to_on_demand made pure. It is still applied
  after the first frame, with the clock read there.
- ArbiterState.next_on_demand() is the step _advance_on_demand takes.

run() asks decide() for the screen at the point it used to call
_resolve_active_mode (after any Vegas iteration, so a session that
started mid-iteration still shows next), and _take_plan() applies it.

Golden traces and the 67-run capture identical to origin/main. Adds
TestOnDemand and the bound table to test_display_arbiter.py; the stage-2
table's on-demand rows now name ON_DEMAND instead of LEGACY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live-priority step of run() (step 7) and the live checks around the
Vegas iteration become the Arbiter's Live Source:

- ArbiterInputs gains live_modes (the scan, None where run() made none),
  vegas_enabled, vegas_live_in_ticker and vegas_yielded. ArbiterState
  gains the rotation and its index, the live resume point and the
  "takeover not shown yet" flag.
- Live picks the next live mode round-robin (live_pick, now also what
  _check_live_priority returns), not advancing past a mid-screen takeover
  that has not shown. It outranks Vegas unless the ticker keeps live
  content, in which case it has no say at all, as before. With nothing
  live, a plan below it carries ends_live and the interrupted rotation
  resumes.
- ArbiterState.claim_live/release_live are _apply_live_priority's
  bookkeeping made pure; _apply_live_priority applies them.

run() reads the inputs below the WiFi notice where it always did
(_arbiter_inputs_below_wifi: the Vegas check, then the scan), asks
decide() once more, and _take_plan applies the claim or the resume. The
Vegas iteration moves to _run_vegas_iteration, which re-decides with
vegas_yielded after a yield, so a game that stopped the ticker or an
on-demand session that started mid-iteration still shows next. LEGACY
now means Vegas or the rotation.

One redundant call is gone: a Vegas pass scanned the live plugins twice
at the same instant (step 7, then step 8's "is anything live?"); it scans
once. Golden traces unchanged. The 67-run capture is identical to
origin/main once that duplicate scan and _apply_live_priority(None) calls
that changed nothing are left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gas (run loop stage 3)

decide() now names the screen for every pass: the Rotation Source
(Source.ROTATION) answers with the rotation's current mode, after the
resume when live priority just ended. LEGACY is left meaning only Vegas,
whose iteration is still run()'s own code until stage 4. Once this pass's
iteration has yielded (vegas_yielded), Vegas passes and the screen it fell
through to is decided like any other.

The rotation's mode is state.current_mode rather than
rotation[rotation_index]: they agree except where something moved the
panel off the list and the rotation carries on from there (a live mode no
entry names, or None after a session ended with nothing to resume to),
and run() always showed current_display_mode.

ArbiterState.after(outcome) is _advance_after_screen's step: an on-demand
session moves to its next mode; otherwise the rotation advances unless the
mode just shown is still live. The Outcome carries the two facts only the
controller can see at the end of the screen (on_demand_active, the live
hold from _still_live). Ending a session with no modes left stays in the
controller, because it is not pure.

Golden traces unchanged; the 67-run capture is identical to origin/main
with the same two exclusions as the previous commit. Adds the Vegas /
Rotation table and TestAfter; the stage-2 rows that said LEGACY for "live,
Vegas or rotation" now say ROTATION.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…points (run loop stage 3)

The three mid-screen checks the frame loops made one after another --
_check_live_takeover, then _screen_preempted with _wifi_notice_pending in
it -- become one call: Arbiter.decide(state, inputs, now, running=plan).
It returns `running` itself while the screen holds, else the plan that
ends it, from these rules in the order the loops checked them:

1. Live: a game went live while a non-live screen runs. First because it
   is the one preemption that changes the state (the rotation moves to
   the live mode and remembers where it was), and it is still claimed
   when a WiFi notice is pending too; the next pass shows the notice,
   then the game, as before.
2. The panel's mode moved under the screen (on-demand started, ended or
   changed; the rotation was rebuilt).
3. The schedule turned the panel off.
4. A WiFi notice arrived (on-demand outranks it; compared with expiry).
5. A plugin reload is waiting (between frames only).

Each rule is gated by plan.preemptible_by: every screen may be preempted
by the gate, OnDemand, Wifi, Live, Rotation and a reload, except that a
live screen leaves Live out. A follower and Vegas never preempt
mid-screen. The pure helper live_takeover() is the Live rule, shared with
the dwell sleep's _check_live_takeover.

The controller only gathers and applies. _screen_service applies pending
changes and makes the live scan when one is due (_scan_for_takeover: the
same throttle and gates as before); _screen_check reads the WiFi notice
exactly where the loop did (the read is throttled and deletes an expired
file, so an extra read would move both), calls decide() once, and claims
a live takeover. _screen_preempted is gone; _check_live_takeover and
_wifi_notice_pending remain for the dwell sleep and the Vegas yield path,
built on the same rules.

Golden traces unchanged. The 67-run capture is identical to origin/main
(with the earlier two exclusions) except for one event: in the 125 Hz loop
the live scan still runs before the frame's sleep, but the claim is now
made by the service point after it, so the "live" state change is logged
8 ms later (test_live_game_cuts_a_scrolling_screen_short: 8.064 -> 8.072).
The screen still ends at the same frame (8.072) and every frame, sleep
and pass is unchanged.

Adds the mid-screen table (24 rows), live_takeover's table, and
test/test_screen_runner.py (the runner on a scripted host, plus the
controller's service point: which reads it makes at which checkpoint).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/RUN_LOOP_REDESIGN.md describes run() as it now is (two decide()
calls per pass, the runner and its service points, the state snapshot and
its transitions), records what stage 3 shipped and how it was checked,
and adds one open "may be wrong" behaviour the mutation run surfaced: a
Vegas iteration stopped for a sync follower falls through to a full
rotation screen before the follower gets the panel (pinned by
test_vegas_yielding_to_a_follower_shows_a_rotation_screen_first; passes on
origin/main too). docs/IPC_CONTROL_SOCKET.md no longer names
_screen_preempted. CHANGELOG entry under Unreleased.

A mutation run broke 46 moved or new pieces once each (OnDemand, Live,
Vegas/Rotation, after(), each mid-screen rule, the runner's pacing, exits
and service points, the controller's gathering and claims). Three
survived and get a test here:

- the after-loop service point not reading the WiFi notice: the
  completed-loop checkpoint gets its own name, and a run-loop test has a
  notice pending when a later frame comes back empty;
- the Vegas yield path not marking vegas_yielded: the follower test above;
- _take_plan not writing back a reset on-demand index: a controller test
  with an index past a shortened list.

All 46 now fail at least one test.

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

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b745684b-06c1-4795-ada6-4a917980c832
📥 Commits

Reviewing files that changed from the base of the PR and between caec9f5 and fa69b3b.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • docs/IPC_CONTROL_SOCKET.md
  • docs/RUN_LOOP_REDESIGN.md
  • mypy-clean.txt
  • src/display_arbiter.py
  • src/display_controller.py
  • src/screen_runner.py
  • test/test_display_arbiter.py
  • test/test_handover_scroll_state.py
  • test/test_ipc_display_stage2.py
  • test/test_screen_runner.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The display loop now selects explicit Arbiter plans and uses ScreenRunner to pace screens and report outcomes. DisplayController connects plan selection, screen execution, preemption checks, and state advancement.

Changes

Display loop

Layer / File(s) Summary
Arbiter plans and state
src/display_arbiter.py, test/test_display_arbiter.py, docs/RUN_LOOP_REDESIGN.md
Arbiter state and plans now cover on-demand, live, Vegas, rotation, and reload decisions. Tests cover plan selection, state transitions, and mid-screen preemption.
ScreenRunner lifecycle and pacing
src/screen_runner.py, test/test_screen_runner.py, mypy-clean.txt, docs/RUN_LOOP_REDESIGN.md
ScreenRunner runs high-FPS or static screen loops and returns outcomes for completion, errors, empty frames, and preemption. Tests cover frame pacing, checkpoints, and exit conditions.
Controller integration and run-loop flow
src/display_controller.py, test/test_ipc_display_stage2.py, test/test_handover_scroll_state.py, docs/*, CHANGELOG.md
DisplayController adapts controller operations to ScreenRunner, supplies Arbiter inputs, and advances state from screen outcomes. Documentation describes the updated flow and records observed timing and trace differences.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant DisplayController
  participant Arbiter
  participant ScreenRunner
  participant ScreenHost
  DisplayController->>Arbiter: decide from controller state and inputs
  Arbiter-->>DisplayController: return screen plan
  DisplayController->>ScreenRunner: run plan and plugin
  ScreenRunner->>ScreenHost: complete plan and run screen callbacks
  ScreenHost->>Arbiter: check running plan for preemption
  Arbiter-->>ScreenHost: return preempting plan or retain running plan
  ScreenRunner-->>DisplayController: return outcome
  DisplayController->>DisplayController: advance state from plan and outcome
Loading

Merge Risk: 🔵 Low · up to fa69b

The display loop now runs screens through a new runner and chooses screens through the Arbiter, and no other behavior change is intended. No concrete defect was found. The author has asked that the PR wait for an on-hardware frame soak and for the full test-suite comparison to finish, so merge after those checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to fa69b

The examined access controls and state-ownership safeguards remain in place. A suspected reload regression was also present before this refactor. Risk remains low rather than minimal because deployment exposure and device-level validation are not fully established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The supported control path affects the local display instance and its installed, registered plugins under the display process's authority. Effective access depends on local socket permissions and configured peer authorization; production group membership and broader deployment exposure were not established.

Trust Boundaries and Controls

  • observed — When peer checks are enabled and credentials are available, the control server checks root, display-user, or configured-group authorization before request processing. Parsed command types cross into the controller through a queue rather than directly invoking rendering from socket threads.

Resilience and Maintainability Implications

  • observed — The inspected IPC path limits concurrent connections, applies connection timeouts, and rejects commands when its queue is full. These controls contain socket-client resource pressure without executing rendering on connection threads.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.82% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 193 functions across 7 files. (4 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the display run-loop refactor and names its main changes: ScreenRunner and the OnDemand, Live, and Rotation sources.
Full details: Docstring Coverage

Explanation

Docstring coverage is 37.82% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 193 functions across 7 files. (4 skipped: 4 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 126 complexity

Metric Results
Complexity 126

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@ChuckBuilds
ChuckBuilds merged commit 26cae3e into main Oct 5, 2026
15 checks passed
@ChuckBuilds
ChuckBuilds deleted the claude/run-loop-stage3 branch October 5, 2026 02:15
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