P20/P29: cursor-on-black detection, repro tooling, auto-recovery - #29
Conversation
Repro-generated run sheets for cohort 2, collected 2026-07-19 on the v0.4.0 build. Group A baseline runs 11-24 and 36 (blacks 14, 22, 24 on-condition; 11 black on AC; 17 settled E2 variant; 19 disregarded as possibly misreported) and group B displaysleep-recovery runs 25-35 and 37-40 (one black, run 37, cleared by the recovery).
Add group A cohort 2 and group B tables indexing run sheets 11-40, with the off-condition notes (run 11 on AC, run 17 settled E2, run 19 disregarded) and a pointer to the P29 detection result. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cohort 2 met the capture gate (3 on-condition black, 9 clean settled group A captures). Field-by-field diff of the post-wake bundles: - dcp.txt / connection-mode.txt / config.txt / ioreg.txt: negative. Black and clean bundles are byte-identical (ioreg after masking volatile counters), closing the NormalModeActive and DCPPowerAssertionCount questions from the 2026-06-10 update. - windowserver.txt: positive. The SkyLight hotplug-coalescing line (out replaced with in for display 2) appears in the wake window of every black-panel run and no clean run: 10/10 vs 0/15 across both cohorts (cohort 2: blacks 14, 22, 24 + corroborating 11, 37, 17; cleans 12, 13, 15, 16, 18, 20, 21, 23, 36; cohort 1 corroborates 4/4 vs 0/6). Supports hypothesis A: the external link drops during sleep and the wake coalesces the out/in pair, so WindowServer skips the full re-attach and the DCP scanout stays un-rearmed. Group B recovery held 1/1 (run 37). Next: second group B black, then a read-only log-query detector prototype at wake-settle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Group B extension runs 41-57 (blacks 49, 51-54, 56-57 — all but 49 provoked by the new C1 cable trigger; run 55 the C1 miss) and the first group C runs 58-60 (58 effectively unlocked due to lock ordering; 60 the first locked black, cleared while locked).
Add the R0/M1/C1 short-code legend (C1 = pre-run USB-C unplug/replug cable trigger), the built-in immediate-state caveat (possible full inversions with the cursor unseen; observed directly on runs 54, 56-57), the group B extension rows 41-57 with the group's verdict (displaysleep cleared 8/8 blacks), and a group C section recording the working lock-before-repro osascript invocation and runs 58-60. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Marker validated on the extension: 9/9 blacks vs 0/11 cleans, running total 19/19 vs 0/26; FuseBoard secondary demoted (split breaks on runs 49 and 60). Record the C1 cable trigger as the first on-demand repro lever, the displaysleep recovery verdict (10/10 programmatic clears including one while locked), and the surveyed engineering paths: IOMobileFramebuffer HPD notifications as the deterministic upstream detection candidate (OSLogStore as the implementation of record meanwhile), and RequestPowerChange / CGS external cycle / DDC 0xD6 as targeted recovery candidates to test against C1 blacks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build the P29 candidate (2) primitive — a CG transaction that disables then re-enables the external display — and expose it two ways: as a second recovery method (recover --method extcycle, repro --recover extcycle) and as a pre-sleep repro trigger (--trigger extcycle), the software analog of the C1 cable trigger (disable, 5 s for the built-in restore/redraw, re-enable, 5 s re-attach settle). The run sheet's conditions block gains a machine-filled trigger line so soft-trigger runs are distinguished from physical C1 runs, and --help now documents the recover and repro methods, which were previously undocumented. The daemon needs no changes: it reacts to the cycle's reconfiguration callbacks as it would to a physical unplug/replug (safety-invariant restore, then auto-blackout on re-attach). Verified: make build clean; clang-format and clang-tidy clean; --dry-run walkthrough exercises trigger placement, recovery dispatch, and the unknown-method/trigger errors. Runtime behavior against a real black is pending maintainer C1 runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document --trigger extcycle in the matrix how-to and add the trigger line to the per-run template; extend P29 with the tooling note and its validation order (soft-trigger A/B, then extcycle as recovery against C1 blacks). Correct run 58 in both docs: the maintainer later recalled the cable trigger may have been applied, so it must not be cited as a trigger-free natural repro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Soft-trigger series 61-80 (--trigger extcycle; 18 blacks, misses 71 and 72; all W1 — scheduled wake failed, manual wake) split between displaysleep recovery (61-68) and extcycle recovery (69-80, cleared 10/10 unlocked), and group C runs 81-85 (--recover extcycle while locked: failed on both blacks and induced black externals on clean wakes, run 84 leaving the external CG-offline).
Every --trigger run (61-80) hit W1 — scheduled wake failed — because the wake was scheduled before the trigger, whose ~15 s of cueing and settling left the wake time in the past by the time the machine slept. Reorder repro: prime sudo (sudo --validate is now built in; no manual prefix), run the trigger, then schedule the wake with the lead computed after the trigger completes. Add an "awake" cue spoken within ~1 s of the wake: instead of one fixed post-sleepnow interval, 1 s ticks detect the wake (a tick whose wall duration jumps past 5 s spanned the sleep), which also anchors the settle countdown at the wake itself and makes manual and overnight wakes capture correctly. Add --lock: issue the ctrl-cmd-q session-lock keystroke (System Events, as validated manually in runs 59-60) just before sleep, after every possible sudo prompt, replacing the external osascript chain for group C runs. Abort if the lock fails so a run recorded as locked was actually locked. Verified: build, clang-format, dry-run walkthroughs of the trigger+lock and wake=0 paths. Live behavior pending maintainer runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matrix: add W1 and M0 short codes, the awake cue and built-in sudo priming and --lock to the how-to, the soft-trigger series table (runs 61-80) with its verdicts (trigger 18/20; extcycle recovery 10/10 unlocked), and the group C extension table (runs 81-85: extcycle while locked fails and induces blacks, run 84 leaving the external CG-offline). P29: evening update — marker running total 39/39 vs 0/31 with proof the trigger does not plant it (clean trigger runs 71-72; marker timestamps at the wake); W1 root cause and same-evening fixes; extcycle recovery lock-state split; attribution assessment (host-side macOS display stack; cable and monitor exonerated as necessary conditions; MonitorControlLite control series proposed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tick-loop wake detector only noticed a wake when one 1 s tick spanned more than 5 s of wall clock, so a short sleep (lead mostly consumed by falling asleep) woke undetected and repro spun silently with no ceiling — the post-run-85 shakedown failure. Register NSWorkspaceWillSleepNotification and NSWorkspaceDidWakeNotification observers before sleepnow and wait on the did-wake notification in 1 s run-loop slices: deterministic for arbitrarily short sleeps. Keep the tick-jump check only as a fallback for missed delivery, and abort if no will-sleep arrives within 60 s of sleepnow so a sleep that never happens errors instead of hanging; the wake side has no ceiling on purpose (overnight manual wakes are a legitimate run shape). Also schedule the wake immediately before sleepnow — after the lock, condition reads, and cue speech — so the lead is consumed only by falling asleep, and skip the interactive sudo fallback when --lock is active (nobody can answer a prompt on a locked screen). Verified: build, clang-format, clang-tidy clean; dry-run walkthrough of the trigger+lock path. Live sleep/wake pending maintainer shakedown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P29: the post-run-85 shakedown reproduced the black on the reordered build but exposed the tick-loop wake detector (sub-5 s sleeps woke undetected; loop had no ceiling); record the notification-based rework, the 60 s did-not-sleep guard, the schedule-last timing change, and the note that userspace ticks can neither cause nor prevent a wake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shakedowns 86-89 on the notification-based wake detection (no W1), MonitorControlLite-quit control series 90-97 (blacks unabated — confound retired), locked displaysleep runs 98-102 (recovery cleared 4/4 locked blacks), and group E lid runs 103-104 (103: AC, clamshell closed, woke clean to the external; 104: battery, no closed-lid wake, lid-open wake went black and displaysleep cleared it).
Runs 87, 89, 90, 91, 96, 97 ran on AC power (machine-recorded), which is coverage row D, not B — rename the sheets' grp tags and group fields per the maintainer. Locked AC runs 98-99 stay group C (D is defined unlocked; the power field carries the truth). Bundle directory names carry no group tag, so no bundle paths change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire the validated detection and recovery into the daemon. At wakeSettleTimerFired, when an external display is present, query the unified log (log show over a window opening 30 s before the recorded wake — the same unprivileged path diagnose uses) for the WindowServer hotplug-coalescing marker that separates black wakes from clean ones (56/56 vs 0/33 across matrix runs 1-104). Marker present: run the displaysleep cycle (pmset displaysleepnow, 2 s, caffeinate -u) off the main queue — the recovery every programmatic clear in the matrix used, validated locked and unlocked. Marker absent: log and do nothing. Gated by a new recoveryStrategy pref (String, default displaysleep; none disables; unknown values fall back to none with a warning), reloaded via SIGHUP, settable with the new blackoutd recovery <none|displaysleep> subcommand (P23 verbosity pattern) and surfaced in status and diagnose config.txt. Guards: a once-per-wake latch (the settle timer can re-fire on err=1014 retries), a _systemSleeping re-check after the query, and the fact that the display-sleep cycle emits screen (not system) sleep notifications, so it cannot re-arm the wake path and loop. The marker wording is private SkyLight internals pinned to macOS 26; its absence is always treated as clean-or-unknown, never an error. Verified: build, clang-format, clang-tidy clean; recovery subcommand usage/validation exercised; status shows the new line against the live daemon. Live wake behavior pending the maintainer's next runs (note: matrix data collection now requires blackoutd recovery none first). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matrix: index runs 86-104 (shakedowns, MCL controls, groups C/D/E) with the block's verdicts — MonitorControlLite retired as a confound, locked displaysleep at n=5, group D answered at n=6, group E's lived scenario reproduced and recovered, awake-cue audibility quirk — plus the data-collection caveat that the daemon's new auto-recovery must be disabled (blackoutd recovery none) before matrix runs. All-time marker tally 56/56 black vs 0/33 clean. P20/P29: record the confound retirement and the implemented detector-gated displaysleep auto-recovery (recoveryStrategy pref, blackoutd recovery subcommand); flicker-free candidates and BD RE remain the follow-up. AGENTS.md: document the recoveryStrategy NSUserDefaults key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Experimental CLI-only recovery candidate for the flicker-free follow-up (P29): open the external framebuffer by name via private IOMobileFramebuffer.framework (dlsym-resolved, no link dependency) and request a power 0 -> 1 cycle. Signatures are reconstructed from public reverse-engineering headers and unverified on macOS 26; the probe prints every return code, and even a refusal at open is an answer — the HPD-notification detection idea depends on the same user-client access. The daemon cannot invoke it (recoveryStrategy accepts only none|displaysleep). Verified: build, clang-format clean; dry-run and unknown-method paths exercised. Live invocation is maintainer-run against a soft-trigger black. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Record the detector-gated displaysleep recovery decision as ADR 0010 (context, evidence tallies, guard rails, and the considered alternatives: reconfig-flag keying, below-CG polling, unconditional recovery, extcycle, IOMFB HPD notifications, OSLogStore). Add the matching Key Design Decisions entry to docs/architecture.md, fix the stale ADR count in AGENTS.md (9 ADRs, numbered through 0010; 0009 is cited in docs but not yet written), and note the fbpower probe in P29's flicker-free follow-up bullet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Important Review skippedToo many files! This PR contains 107 files, which is 7 over the limit of 100. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (107)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@CodeRabbit review --dir src |
|
✅ Action performedReview finished.
|
Per external review of PR #29 (assessed and adopted): emit the log show --start timestamp with an explicit local zone and offset (the "YYYY-MM-DD HH:MM:SSZZZZZ" form log(1) documents) instead of relying on the formatter's local-zone default matching log's local-zone interpretation; check pmset displaysleepnow's exit status before the 2 s pause and caffeinate nudge, logging and returning early on failure; and document why the marker window opens 30 s before the recorded wake (notification-vs-hardware wake skew plus log-flush latency, still far inside consecutive-run spacing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstruct the long-cited-but-unwritten ADR 0009 from technical-debt P26, the 2026-05-25 session handoff, and inject_edid ADRs 0001-0002: the SP2309W EDID correction, if hosted in blackoutd, lives as an opt-in isolated quirk module (own translation unit under src/quirks/, compiled only under make QUIRKS=sp2309w, runtime-keyed to vendor 0x10AC / product 0xD01D, one narrow core hook, no quirk framework, self-originated reconfiguration marking that never weakens the safety invariant). Status proposed, per P26's first acceptance criterion; recorded retroactively with the reconstruction sources cited. Correct AGENTS.md's ADR note: no 0009 collision existed — repo-foundation's 0009 (logging) is a different repo's namespace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live validation of the daemon's marker-gated displaysleep recovery (runs 106-107; 105 was a false start): with recoveryStrategy displaysleep active and no repro-side --recover, a soft-trigger black wake (106) cleared to E0/B0 before the capture cue, and a clean-side run (107) passed through untouched — both with no perceived flicker, the cycle landing inside the wake transition.
Runs 106-107: marker-gated displaysleep cleared a black wake before the capture cue and left a clean wake untouched, with no perceived flicker (the cycle lands inside the wake transition) — lowering the urgency of the flicker-free follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
adrs doctor flagged both for a missing Decision Outcome section. Reshape to the org MADR 4.0 layout (RF ADR 0004 / 0018): Decision Drivers, Considered Options, Decision Outcome with "Chosen:", Good/Bad/Neutral-because Consequences, Confirmation (0010 cites the live validation runs 106-107; 0009 records the standalone-confirmed mechanism with the hosted form pending), and Pros and Cons of the Options. Content unchanged; adrs doctor now reports no issues. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Closes out the empirical phase of the cursor-on-black investigation (technical-debt P20/P29) and lands the first automatic recovery.
reprogained a software cable-trigger (--trigger extcycle, 26/29 black), built-in sudo priming,--lock, notification-based wake detection with an "awake" cue and a 60 s did-not-sleep guard, and a machine-filledtriggerfield on run sheets.recoveryStrategypref (blackoutd recovery <none|displaysleep>, defaultdisplaysleep). Decision recorded as ADR 0010; architecture.md updated.recover --method extcycle(unlocked-only; hazardous while locked — documented) and an experimentalfbpowerIOMobileFramebuffer probe for the flicker-free follow-up.Verification
Manual, on the target hardware (M2 MacBook Air + Dell SP2309W): 94 recorded eyewitness-paired repro runs; every code change exercised live by the maintainer during collection (wake detection, trigger, lock, recovery methods). Build, clang-format, and clang-tidy clean; dry-run walkthroughs for every new CLI path. The daemon's live auto-recovery has been validated.
Written with AI assistance (Claude Fable 5, high); all display-state observations are the maintainer's, recorded in the run sheets.