From 596363eb9d9f6502a22aa602b8895e10e53e79b8 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 14:47:25 -0400 Subject: [PATCH 01/24] docs: record cohort 2 run sheets (runs 11-40) 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). --- .../run011-grp-a-20260719-000924.md | 46 +++++++++++++++++ .../run012-grp-a-20260719-082315.md | 46 +++++++++++++++++ .../run013-grp-a-20260719-110044.md | 46 +++++++++++++++++ .../run014-grp-a-20260719-110205.md | 46 +++++++++++++++++ .../run015-grp-a-20260719-110325.md | 46 +++++++++++++++++ .../run016-grp-a-20260719-110439.md | 46 +++++++++++++++++ .../run017-grp-a-20260719-120552.md | 46 +++++++++++++++++ .../run018-grp-a-20260719-120742.md | 46 +++++++++++++++++ .../run019-grp-a-20260719-120843.md | 46 +++++++++++++++++ .../run020-grp-a-20260719-120955.md | 46 +++++++++++++++++ .../run021-grp-a-20260719-121755.md | 46 +++++++++++++++++ .../run022-grp-a-20260719-121916.md | 46 +++++++++++++++++ .../run023-grp-a-20260719-122131.md | 46 +++++++++++++++++ .../run024-grp-a-20260719-122252.md | 46 +++++++++++++++++ .../run025-grp-b-20260719-122426.md | 51 +++++++++++++++++++ .../run026-grp-b-20260719-122726.md | 51 +++++++++++++++++++ .../run027-grp-b-20260719-122934.md | 51 +++++++++++++++++++ .../run028-grp-b-20260719-131053.md | 51 +++++++++++++++++++ .../run029-grp-b-20260719-131252.md | 51 +++++++++++++++++++ .../run030-grp-b-20260719-131452.md | 51 +++++++++++++++++++ .../run031-grp-b-20260719-131825.md | 51 +++++++++++++++++++ .../run032-grp-b-20260719-132525.md | 51 +++++++++++++++++++ .../run033-grp-b-20260719-132720.md | 51 +++++++++++++++++++ .../run034-grp-b-20260719-132947.md | 51 +++++++++++++++++++ .../run035-grp-b-20260719-140116.md | 51 +++++++++++++++++++ .../run036-grp-a-20260719-141127.md | 46 +++++++++++++++++ .../run037-grp-b-20260719-141825.md | 51 +++++++++++++++++++ .../run038-grp-b-20260719-142134.md | 51 +++++++++++++++++++ .../run039-grp-b-20260719-142353.md | 51 +++++++++++++++++++ .../run040-grp-b-20260719-142621.md | 51 +++++++++++++++++++ 30 files changed, 1455 insertions(+) create mode 100644 docs/debug/repro-matrix/run011-grp-a-20260719-000924.md create mode 100644 docs/debug/repro-matrix/run012-grp-a-20260719-082315.md create mode 100644 docs/debug/repro-matrix/run013-grp-a-20260719-110044.md create mode 100644 docs/debug/repro-matrix/run014-grp-a-20260719-110205.md create mode 100644 docs/debug/repro-matrix/run015-grp-a-20260719-110325.md create mode 100644 docs/debug/repro-matrix/run016-grp-a-20260719-110439.md create mode 100644 docs/debug/repro-matrix/run017-grp-a-20260719-120552.md create mode 100644 docs/debug/repro-matrix/run018-grp-a-20260719-120742.md create mode 100644 docs/debug/repro-matrix/run019-grp-a-20260719-120843.md create mode 100644 docs/debug/repro-matrix/run020-grp-a-20260719-120955.md create mode 100644 docs/debug/repro-matrix/run021-grp-a-20260719-121755.md create mode 100644 docs/debug/repro-matrix/run022-grp-a-20260719-121916.md create mode 100644 docs/debug/repro-matrix/run023-grp-a-20260719-122131.md create mode 100644 docs/debug/repro-matrix/run024-grp-a-20260719-122252.md create mode 100644 docs/debug/repro-matrix/run025-grp-b-20260719-122426.md create mode 100644 docs/debug/repro-matrix/run026-grp-b-20260719-122726.md create mode 100644 docs/debug/repro-matrix/run027-grp-b-20260719-122934.md create mode 100644 docs/debug/repro-matrix/run028-grp-b-20260719-131053.md create mode 100644 docs/debug/repro-matrix/run029-grp-b-20260719-131252.md create mode 100644 docs/debug/repro-matrix/run030-grp-b-20260719-131452.md create mode 100644 docs/debug/repro-matrix/run031-grp-b-20260719-131825.md create mode 100644 docs/debug/repro-matrix/run032-grp-b-20260719-132525.md create mode 100644 docs/debug/repro-matrix/run033-grp-b-20260719-132720.md create mode 100644 docs/debug/repro-matrix/run034-grp-b-20260719-132947.md create mode 100644 docs/debug/repro-matrix/run035-grp-b-20260719-140116.md create mode 100644 docs/debug/repro-matrix/run036-grp-a-20260719-141127.md create mode 100644 docs/debug/repro-matrix/run037-grp-b-20260719-141825.md create mode 100644 docs/debug/repro-matrix/run038-grp-b-20260719-142134.md create mode 100644 docs/debug/repro-matrix/run039-grp-b-20260719-142353.md create mode 100644 docs/debug/repro-matrix/run040-grp-b-20260719-142621.md diff --git a/docs/debug/repro-matrix/run011-grp-a-20260719-000924.md b/docs/debug/repro-matrix/run011-grp-a-20260719-000924.md new file mode 100644 index 0000000..6a2bf01 --- /dev/null +++ b/docs/debug/repro-matrix/run011-grp-a-20260719-000924.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-000924 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 11 started: 2026-07-19 00:09:24 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 00:09:42.726 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-000953 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run012-grp-a-20260719-082315.md b/docs/debug/repro-matrix/run012-grp-a-20260719-082315.md new file mode 100644 index 0000000..af5d6ed --- /dev/null +++ b/docs/debug/repro-matrix/run012-grp-a-20260719-082315.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-082315 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 12 started: 2026-07-19 08:23:15 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 08:23:33.415 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-082343 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run013-grp-a-20260719-110044.md b/docs/debug/repro-matrix/run013-grp-a-20260719-110044.md new file mode 100644 index 0000000..6271b8d --- /dev/null +++ b/docs/debug/repro-matrix/run013-grp-a-20260719-110044.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-110044 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 13 started: 2026-07-19 11:00:44 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 11:01:03.321 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-110117 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run014-grp-a-20260719-110205.md b/docs/debug/repro-matrix/run014-grp-a-20260719-110205.md new file mode 100644 index 0000000..79ba8db --- /dev/null +++ b/docs/debug/repro-matrix/run014-grp-a-20260719-110205.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-110205 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 14 started: 2026-07-19 11:02:05 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 11:02:23.897 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-110237 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run015-grp-a-20260719-110325.md b/docs/debug/repro-matrix/run015-grp-a-20260719-110325.md new file mode 100644 index 0000000..3bf8a53 --- /dev/null +++ b/docs/debug/repro-matrix/run015-grp-a-20260719-110325.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-110325 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 15 started: 2026-07-19 11:03:25 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 11:03:43.719 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-110357 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run016-grp-a-20260719-110439.md b/docs/debug/repro-matrix/run016-grp-a-20260719-110439.md new file mode 100644 index 0000000..6d039b8 --- /dev/null +++ b/docs/debug/repro-matrix/run016-grp-a-20260719-110439.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-110439 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 16 started: 2026-07-19 11:04:39 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 11:04:57.798 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-110511 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run017-grp-a-20260719-120552.md b/docs/debug/repro-matrix/run017-grp-a-20260719-120552.md new file mode 100644 index 0000000..da88292 --- /dev/null +++ b/docs/debug/repro-matrix/run017-grp-a-20260719-120552.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-120552 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 17 started: 2026-07-19 12:05:52 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 12:06:10.853 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-120624 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run018-grp-a-20260719-120742.md b/docs/debug/repro-matrix/run018-grp-a-20260719-120742.md new file mode 100644 index 0000000..83551dd --- /dev/null +++ b/docs/debug/repro-matrix/run018-grp-a-20260719-120742.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-120742 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 18 started: 2026-07-19 12:07:42 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 12:07:54.184 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-120810 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run019-grp-a-20260719-120843.md b/docs/debug/repro-matrix/run019-grp-a-20260719-120843.md new file mode 100644 index 0000000..b3fd74e --- /dev/null +++ b/docs/debug/repro-matrix/run019-grp-a-20260719-120843.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-120843 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 19 started: 2026-07-19 12:08:43 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 12:09:01.770 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-120915 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run020-grp-a-20260719-120955.md b/docs/debug/repro-matrix/run020-grp-a-20260719-120955.md new file mode 100644 index 0000000..6d87177 --- /dev/null +++ b/docs/debug/repro-matrix/run020-grp-a-20260719-120955.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-120955 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 20 started: 2026-07-19 12:09:55 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 12:10:14.335 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-121027 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run021-grp-a-20260719-121755.md b/docs/debug/repro-matrix/run021-grp-a-20260719-121755.md new file mode 100644 index 0000000..2acf7e7 --- /dev/null +++ b/docs/debug/repro-matrix/run021-grp-a-20260719-121755.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-121755 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 21 started: 2026-07-19 12:17:55 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 12:18:14.155 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-121827 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run022-grp-a-20260719-121916.md b/docs/debug/repro-matrix/run022-grp-a-20260719-121916.md new file mode 100644 index 0000000..e5b0a11 --- /dev/null +++ b/docs/debug/repro-matrix/run022-grp-a-20260719-121916.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-121916 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 22 started: 2026-07-19 12:19:16 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 12:19:35.597 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-121948 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: There were a few brief moments during the blackout and switchover where both the built-in and external were cursor-on-black. +``` diff --git a/docs/debug/repro-matrix/run023-grp-a-20260719-122131.md b/docs/debug/repro-matrix/run023-grp-a-20260719-122131.md new file mode 100644 index 0000000..edb964e --- /dev/null +++ b/docs/debug/repro-matrix/run023-grp-a-20260719-122131.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-122131 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 23 started: 2026-07-19 12:21:31 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 12:21:50.180 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-122203 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run024-grp-a-20260719-122252.md b/docs/debug/repro-matrix/run024-grp-a-20260719-122252.md new file mode 100644 index 0000000..d788fe2 --- /dev/null +++ b/docs/debug/repro-matrix/run024-grp-a-20260719-122252.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-122252 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 24 started: 2026-07-19 12:22:52 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 12:23:12.237 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-122324 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run025-grp-b-20260719-122426.md b/docs/debug/repro-matrix/run025-grp-b-20260719-122426.md new file mode 100644 index 0000000..7a60889 --- /dev/null +++ b/docs/debug/repro-matrix/run025-grp-b-20260719-122426.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-122426 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 25 started: 2026-07-19 12:24:26 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 12:24:45.144 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-122458 + post-recover = docs/debug/blackoutd-diag-20260719-122541 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Displays active; recovery was not necessary but occurred anyway after some delay. +``` diff --git a/docs/debug/repro-matrix/run026-grp-b-20260719-122726.md b/docs/debug/repro-matrix/run026-grp-b-20260719-122726.md new file mode 100644 index 0000000..62fbe23 --- /dev/null +++ b/docs/debug/repro-matrix/run026-grp-b-20260719-122726.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-122726 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 26 started: 2026-07-19 12:27:26 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 12:27:45.247 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-122759 + post-recover = docs/debug/blackoutd-diag-20260719-122843 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Displays active, recovery unnecessary +``` diff --git a/docs/debug/repro-matrix/run027-grp-b-20260719-122934.md b/docs/debug/repro-matrix/run027-grp-b-20260719-122934.md new file mode 100644 index 0000000..a4df590 --- /dev/null +++ b/docs/debug/repro-matrix/run027-grp-b-20260719-122934.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-122934 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 27 started: 2026-07-19 12:29:34 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 12:29:53.101 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-123006 + post-recover = docs/debug/blackoutd-diag-20260719-123051 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Displays active, recovery not needed. It took a few moments for the external display to be lit, showing desktop after the built-in blacked out. +``` diff --git a/docs/debug/repro-matrix/run028-grp-b-20260719-131053.md b/docs/debug/repro-matrix/run028-grp-b-20260719-131053.md new file mode 100644 index 0000000..07c1573 --- /dev/null +++ b/docs/debug/repro-matrix/run028-grp-b-20260719-131053.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-131053 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 28 started: 2026-07-19 13:10:53 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 13:11:12.273 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-131126 + post-recover = docs/debug/blackoutd-diag-20260719-131205 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Displays active, no recovery needed +``` diff --git a/docs/debug/repro-matrix/run029-grp-b-20260719-131252.md b/docs/debug/repro-matrix/run029-grp-b-20260719-131252.md new file mode 100644 index 0000000..91b12d9 --- /dev/null +++ b/docs/debug/repro-matrix/run029-grp-b-20260719-131252.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-131252 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 29 started: 2026-07-19 13:12:52 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 13:13:10.635 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-131324 + post-recover = docs/debug/blackoutd-diag-20260719-131404 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Displays active, no recovery needed +``` diff --git a/docs/debug/repro-matrix/run030-grp-b-20260719-131452.md b/docs/debug/repro-matrix/run030-grp-b-20260719-131452.md new file mode 100644 index 0000000..3345342 --- /dev/null +++ b/docs/debug/repro-matrix/run030-grp-b-20260719-131452.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-131452 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 30 started: 2026-07-19 13:14:52 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 13:15:11.265 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-131524 + post-recover = docs/debug/blackoutd-diag-20260719-131605 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Recovery not needed +``` diff --git a/docs/debug/repro-matrix/run031-grp-b-20260719-131825.md b/docs/debug/repro-matrix/run031-grp-b-20260719-131825.md new file mode 100644 index 0000000..62cf75e --- /dev/null +++ b/docs/debug/repro-matrix/run031-grp-b-20260719-131825.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-131825 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 31 started: 2026-07-19 13:18:25 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 13:18:44.123 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-131857 + post-recover = docs/debug/blackoutd-diag-20260719-131940 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Recovery not needed +``` diff --git a/docs/debug/repro-matrix/run032-grp-b-20260719-132525.md b/docs/debug/repro-matrix/run032-grp-b-20260719-132525.md new file mode 100644 index 0000000..e07d854 --- /dev/null +++ b/docs/debug/repro-matrix/run032-grp-b-20260719-132525.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-132525 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 32 started: 2026-07-19 13:25:25 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 13:25:44.481 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-132559 + post-recover = docs/debug/blackoutd-diag-20260719-132641 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed +``` diff --git a/docs/debug/repro-matrix/run033-grp-b-20260719-132720.md b/docs/debug/repro-matrix/run033-grp-b-20260719-132720.md new file mode 100644 index 0000000..9d3fc3a --- /dev/null +++ b/docs/debug/repro-matrix/run033-grp-b-20260719-132720.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-132720 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 33 started: 2026-07-19 13:27:20 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 13:27:39.193 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-132753 + post-recover = docs/debug/blackoutd-diag-20260719-132834 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed +``` diff --git a/docs/debug/repro-matrix/run034-grp-b-20260719-132947.md b/docs/debug/repro-matrix/run034-grp-b-20260719-132947.md new file mode 100644 index 0000000..4b674bf --- /dev/null +++ b/docs/debug/repro-matrix/run034-grp-b-20260719-132947.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-132947 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 34 started: 2026-07-19 13:29:47 +Build: v0.4.0 built: 2026-07-19T04:08:56+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 13:30:06.059 blackoutd[80240:1872712] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-133019 + post-recover = docs/debug/blackoutd-diag-20260719-133100 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed +``` diff --git a/docs/debug/repro-matrix/run035-grp-b-20260719-140116.md b/docs/debug/repro-matrix/run035-grp-b-20260719-140116.md new file mode 100644 index 0000000..932582e --- /dev/null +++ b/docs/debug/repro-matrix/run035-grp-b-20260719-140116.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-140116 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 35 started: 2026-07-19 14:01:16 +Build: v0.4.0 built: 2026-07-19T17:36:33+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 14:01:36.596 blackoutd[87639:2064839] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-140149 + post-recover = docs/debug/blackoutd-diag-20260719-140226 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed +``` diff --git a/docs/debug/repro-matrix/run036-grp-a-20260719-141127.md b/docs/debug/repro-matrix/run036-grp-a-20260719-141127.md new file mode 100644 index 0000000..c86117c --- /dev/null +++ b/docs/debug/repro-matrix/run036-grp-a-20260719-141127.md @@ -0,0 +1,46 @@ + + +# repro run sheet — 20260719-141127 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 36 started: 2026-07-19 14:11:27 +Build: v0.4.0 built: 2026-07-19T17:36:33+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. A + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-19 14:11:46.975 blackoutd[87639:2064839] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-141200 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run037-grp-b-20260719-141825.md b/docs/debug/repro-matrix/run037-grp-b-20260719-141825.md new file mode 100644 index 0000000..c0706b8 --- /dev/null +++ b/docs/debug/repro-matrix/run037-grp-b-20260719-141825.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-141825 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 37 started: 2026-07-19 14:18:25 +Build: v0.4.0 built: 2026-07-19T17:36:33+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 14:18:45.191 blackoutd[87639:2064839] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-141857 + post-recover = docs/debug/blackoutd-diag-20260719-141935 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Music.app playing pre-sleep; went silent at sleep and resumed playing music prior to display wake. +``` diff --git a/docs/debug/repro-matrix/run038-grp-b-20260719-142134.md b/docs/debug/repro-matrix/run038-grp-b-20260719-142134.md new file mode 100644 index 0000000..7c55e4b --- /dev/null +++ b/docs/debug/repro-matrix/run038-grp-b-20260719-142134.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-142134 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 38 started: 2026-07-19 14:21:34 +Build: v0.4.0 built: 2026-07-19T17:36:33+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 14:21:53.662 blackoutd[87639:2064839] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-142207 + post-recover = docs/debug/blackoutd-diag-20260719-142245 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Music playing +``` diff --git a/docs/debug/repro-matrix/run039-grp-b-20260719-142353.md b/docs/debug/repro-matrix/run039-grp-b-20260719-142353.md new file mode 100644 index 0000000..8b0831f --- /dev/null +++ b/docs/debug/repro-matrix/run039-grp-b-20260719-142353.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-142353 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 39 started: 2026-07-19 14:23:53 +Build: v0.4.0 built: 2026-07-19T17:36:33+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 14:24:11.746 blackoutd[87639:2064839] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-142425 + post-recover = docs/debug/blackoutd-diag-20260719-142503 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed, music playing +``` diff --git a/docs/debug/repro-matrix/run040-grp-b-20260719-142621.md b/docs/debug/repro-matrix/run040-grp-b-20260719-142621.md new file mode 100644 index 0000000..5ead5a6 --- /dev/null +++ b/docs/debug/repro-matrix/run040-grp-b-20260719-142621.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-142621 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 40 started: 2026-07-19 14:26:21 +Build: v0.4.0 built: 2026-07-19T17:36:33+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 14:26:40.915 blackoutd[87639:2064839] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-142653 + post-recover = docs/debug/blackoutd-diag-20260719-142733 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed, music on +``` From ad9f651bd3878bae98c7baa7e3d7e6adfd842881 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 14:47:57 -0400 Subject: [PATCH 02/24] docs: index cohort 2 in cursor-on-black matrix 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 --- docs/debug/cursor-on-black-matrix.md | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/debug/cursor-on-black-matrix.md b/docs/debug/cursor-on-black-matrix.md index 456b31b..db68ed0 100644 --- a/docs/debug/cursor-on-black-matrix.md +++ b/docs/debug/cursor-on-black-matrix.md @@ -239,6 +239,71 @@ current build (spoken cues, at-the-moment prompts, boundary-timed condition reads, single-sample settle marker). **Cohort 2 starts at run 011 and owes its own ≥3 black / ≥3 clean settled captures before analysis begins.** + +### Group A — cohort 2 (runs 011+, v0.4.0 build) + +Collected 2026-07-19 on the merged v0.4.0 build with the full +workflow. The cohort satisfies the capture gate: 3 on-condition black +(runs 14, 22, 24) and 9 clean settled captures. + +| run | sheet | immediate E/B | settled E/B | black? | +|-----|--------------------------------------------------------|---------------|-------------|--------| +| 11 | [run011](repro-matrix/run011-grp-a-20260719-000924.md) | E1/B0 | E1/B0 | yes¹ | +| 12 | [run012](repro-matrix/run012-grp-a-20260719-082315.md) | E2/B2 | E0/B0 | no | +| 13 | [run013](repro-matrix/run013-grp-a-20260719-110044.md) | E2/B2 | E0/B0 | no | +| 14 | [run014](repro-matrix/run014-grp-a-20260719-110205.md) | E2/B1 | E1/B0 | yes | +| 15 | [run015](repro-matrix/run015-grp-a-20260719-110325.md) | E2/B2 | E0/B0 | no | +| 16 | [run016](repro-matrix/run016-grp-a-20260719-110439.md) | E2/B2 | E0/B0 | no | +| 17 | [run017](repro-matrix/run017-grp-a-20260719-120552.md) | E2/B1 | E2/B0 | no² | +| 18 | [run018](repro-matrix/run018-grp-a-20260719-120742.md) | E2/B2 | E0/B0 | no | +| 19 | [run019](repro-matrix/run019-grp-a-20260719-120843.md) | *disregarded* | *disregarded* | — | +| 20 | [run020](repro-matrix/run020-grp-a-20260719-120955.md) | E2/B2 | E0/B0 | no | +| 21 | [run021](repro-matrix/run021-grp-a-20260719-121755.md) | E2/B2 | E0/B0 | no | +| 22 | [run022](repro-matrix/run022-grp-a-20260719-121916.md) | E2/B1 | E1/B0 | yes | +| 23 | [run023](repro-matrix/run023-grp-a-20260719-122131.md) | E2/B2 | E0/B0 | no | +| 24 | [run024](repro-matrix/run024-grp-a-20260719-122252.md) | E2/B1 | E1/B0 | yes | +| 36 | [run036](repro-matrix/run036-grp-a-20260719-141127.md) | E2/B2 | E0/B0 | no | + +¹ Run 11 ran on AC (group A specifies battery): a black capture kept +as corroboration, off-condition for the matched diff. +² Run 17 settled **E2** (black, no cursor) — a black panel that the +E1-keyed Outcome line scores "no". Out of both pools for the matched +diff; corroboration for the detector (its bundle sides with the +blacks). +Run 19 is disregarded (maintainer: possibly misreported); its window +is excluded from every tally. + +### Group B (runs 025+) + +| run | sheet | settled E/B | black? | recovery cleared? | +|-----|--------------------------------------------------------|-------------|--------|-------------------| +| 25 | [run025](repro-matrix/run025-grp-b-20260719-122426.md) | E0/B0 | no | n/a (was clean) | +| 26 | [run026](repro-matrix/run026-grp-b-20260719-122726.md) | E0/B0 | no | n/a (was clean) | +| 27 | [run027](repro-matrix/run027-grp-b-20260719-122934.md) | E0/B0 | no | n/a (was clean) | +| 28 | [run028](repro-matrix/run028-grp-b-20260719-131053.md) | E0/B0 | no | n/a (was clean) | +| 29 | [run029](repro-matrix/run029-grp-b-20260719-131252.md) | E0/B0 | no | n/a (was clean) | +| 30 | [run030](repro-matrix/run030-grp-b-20260719-131452.md) | E0/B0 | no | n/a (was clean) | +| 31 | [run031](repro-matrix/run031-grp-b-20260719-131825.md) | E0/B0 | no | n/a (was clean) | +| 32 | [run032](repro-matrix/run032-grp-b-20260719-132525.md) | E0/B0 | no | n/a (was clean) | +| 33 | [run033](repro-matrix/run033-grp-b-20260719-132720.md) | E0/B0 | no | n/a (was clean) | +| 34 | [run034](repro-matrix/run034-grp-b-20260719-132947.md) | E0/B0 | no | n/a (was clean) | +| 35 | [run035](repro-matrix/run035-grp-b-20260719-140116.md) | E0/B0 | no | n/a (was clean) | +| 37 | [run037](repro-matrix/run037-grp-b-20260719-141825.md) | E1/B0 | yes | **yes** | +| 38 | [run038](repro-matrix/run038-grp-b-20260719-142134.md) | E0/B0 | no | n/a (was clean) | +| 39 | [run039](repro-matrix/run039-grp-b-20260719-142353.md) | E0/B0 | no | n/a (was clean) | +| 40 | [run040](repro-matrix/run040-grp-b-20260719-142621.md) | E0/B0 | no | n/a (was clean) | + +14 valid runs, one black (run 37): the `displaysleep` recovery cleared +it end-to-end (post-recover E0/B0). Positive but n=1; cohort 1's four +manual displaysleep clears corroborate. Group B stays open until a +second black lands (n ≥ 2). + +**Detection result (2026-07-19)**: the field-by-field diff over this +cohort found a WindowServer log marker that splits black from clean — +10/10 black wakes vs 0/15 clean across both cohorts. See +`docs/technical-debt.md` P29 (2026-07-19 update) for the diff record, +mechanism reading, and caveats. + ## What we do with the data - **Detection** — diff `dcp.txt` / `connection-mode.txt` (and the raw From 6638933d23797da798ed27fd626fc8424abccc1a Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 14:48:16 -0400 Subject: [PATCH 03/24] docs: record cohort 2 detection diff in P20/P29 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 --- docs/technical-debt.md | 100 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/docs/technical-debt.md b/docs/technical-debt.md index f0dee62..6f46c37 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -787,6 +787,17 @@ that list — capture the connection mode (BetterDisplay `get -connectionMode`, a native reader once it exists) *during* the broken state, to distinguish "stuck in a bad mode" from "no active mode". +**2026-07-19 update — capture gate met; first surviving detector +candidate.** Cohort 2 (matrix runs 011+) delivered the required ≥3 +black / ≥3 clean settled `verbosityLevel=2` captures during genuine +occurrences, satisfying the "Diagnostic data needed" list below via the +`repro` workflow. The field diff found the first black/clean splitter +to survive n ≥ 3: a WindowServer hotplug-coalescing log marker, 10/10 +black wakes vs 0/15 clean across both cohorts, supporting hypothesis A +(link drop during sleep, coalesced on wake). Full diff record, +mechanism reading, caveats, and next steps under P29 (2026-07-19 +update). + **Architecture analysis confirming the gap**: `recommitDisplayConfiguration` fires from exactly two paths today @@ -1689,6 +1700,95 @@ black/clean split. `DCPPowerState` carries no cursor-on-black signal on either controller. `NormalModeActive` and `DCPPowerAssertionCount` remain untested as detectors; capture them at the black instant to test. +**Update (2026-07-19) — cohort 2 diff: a WindowServer log marker splits +black from clean, 10/10 vs 0/15.** Cohort 2 (matrix runs 011+, sheets +under `docs/debug/repro-matrix/`, v0.4.0 build) satisfied the capture +gate: settled group A blacks runs 14, 22, 24 (battery, on-condition; +run 11 also black but on AC) vs cleans runs 12, 13, 15, 16, 18, 20, +21, 23, 36. Run 17 settled E2 (black, no cursor — a black panel the +E1-keyed Outcome line scores "no"); run 19 disregarded (maintainer: +possibly misreported). Field-by-field diff of the post-wake bundles +under matching conditions: + +- `dcp.txt` / `connection-mode.txt`: **byte-identical** across all + three on-condition blacks and all nine cleans. This extends the dead + `DCPPowerState` verdict to the previously untested fields: + `NormalModeActive`, `DCPPowerAssertionCount`, resolution / + `DPTimingModeId` / `Transport`, and the advertised ColorElements + catalog — none carries any black/clean signal (negative). +- `config.txt`: differs only in timestamps (negative). +- `ioreg.txt`: with volatile fields masked (object ids, busy times, + retain counts, `IOPowerManagement`, `DebugState`, statistics, + `TransferCount`), the external `dcpext@71C00000` subtree of black + run 14 is byte-identical to clean runs 13 and 36, and black run 22 + to clean run 18; the built-in `dcp@31C00000` subtree is identical + across all on-condition blacks and cleans. The stalled scanout is + not surfaced to ioreg at all — confirms the 2026-06-10 reading + (negative). +- `windowserver.txt`: **positive.** Clipped to each run's own wake + window (per the classify-per-wake rule above), the SkyLight line + `[ Display:Hotplug ] Replacing existing hotplug event for state + "out" with "in", display id: 2` appears exactly once at the wake of + every black-panel run — run 14 (11:02:20), run 22 (12:19:31), run 24 + (12:23:07), plus corroborating run 11 (00:09:39, AC), run 37 + (14:18:40, group B post-wake), and the E2-variant run 17 (12:06:07) + — and zero times in all nine cleans, each of whose logs covers the + full wake window. Cohort 1 corroborates: in-window marker on all + four blacks (runs 3, 5, 7, 8), on none of the five cleans nor + stricken run 9 (run 10's file carries the line 7 minutes *before* + its run — outside the window, the same per-wake trap as the 0x133e + grep). Combined: **10/10 black-panel wakes vs 0/15 clean**; the + n ≥ 3 bar is met on cohort 2 alone. + +**Mechanism reading.** The marker means WindowServer still holds an +unprocessed hotplug "out" for display 2 when the wake "in" arrives, +and SkyLight coalesces the pair. So on black wakes the external +dropped its link during sleep (hypothesis A, Alt Mode dropout) and +WindowServer — seeing continuity — skips the full teardown/re-attach, +leaving the DCP scanout un-rearmed while every CG/WS-visible field +looks healthy (the below-WS mechanism above). Clean wakes show no +coalescing. A secondary, effect-side split points the same way: the +FuseBoard `updated window N from "occluded" to "visible"` line appears +≥ 5 times in every clean wake window and never in a black one (windows +only become visible when the external actually renders). + +**Side observations (not detectors).** + +- Runs 11 and 37 — the only two blacks whose sheets record an + *immediate* E1/B0 wake (no role reversal) — are also the only two + captures whose built-in DCP reads `DCPPowerState=4` / assertion 1, + with the built-in advertising ColorElements; run 37's post-recover + bundle still reads 4, and the clean music-playing runs 38–40 read 0. + The on-condition blacks (14, 22, 24; immediate E2/B1) read 0 like + every clean. This sub-classes the wake shape, not black vs clean. +- Disregarded run 19: later runs' log windows show the marker fired at + 12:08:58, inside run 19's wake window — consistent with its original + "black" record, but it stays out of every tally per the maintainer. + +**Next steps.** Detection is now plausible with no new private API: at +`wakeSettleTimerFired`, query the unified log for the marker over the +last ~60 s (`OSLogStore`, or `log show --last 60s --predicate 'process +== "WindowServer" AND eventMessage CONTAINS "Replacing existing +hotplug event"'`; the line logs at Default level, so it is in the +persistent store without any debug-logging setup; in cohort 2 it fires +~3.5 s *before* the daemon's settle marker, so it is already on disk +when the settle timer fires). Marker present ⇒ arm the +display-power-cycle-class recovery; absent ⇒ do nothing. Caveats: the +wording is private SkyLight internals and can change across macOS +releases — treat a missing marker as "unknown", pin the predicate per +macOS version, and keep the eyewitness matrix as the validation loop. +Sequence: + +1. More group B runs until a second black lands (recovery is 1/1 in + cohort 2 — run 37 black ⇒ `--recover displaysleep` ⇒ cleared, + post-recover E0/B0 — corroborated by cohort 1's four manual clears, + but below the matrix's n ≥ 2 bar); then group C (locked). +2. Prototype the detector read-only (log the query verdict at settle + time, no recovery wiring) and validate it live against ≥ 3 further + eyewitness-confirmed blacks and cleans. +3. Only then wire detector ⇒ recovery, behind a `recoveryStrategy` + pref (separate PR, per the matrix's "What we do with the data"). + **Files**: `src/main.m` (the `diagnose` `dcp.txt` / `connection-mode.txt` readers); investigation otherwise. Relates to P2, P20, P28, ADR 0003, ADR 0009 (SP2309W YCbCr), and the inject_edid `--mode` reader (of which From 80ed1a0b51de9f80a99f339bb9f6e996bd6def5b Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 16:04:24 -0400 Subject: [PATCH 04/24] docs: record run sheets 41-60 (C1 lever, group C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../run041-grp-b-20260719-145713.md | 51 +++++++++++++++++++ .../run042-grp-b-20260719-150121.md | 51 +++++++++++++++++++ .../run043-grp-b-20260719-150356.md | 51 +++++++++++++++++++ .../run044-grp-b-20260719-150606.md | 51 +++++++++++++++++++ .../run045-grp-b-20260719-150805.md | 51 +++++++++++++++++++ .../run046-grp-b-20260719-151024.md | 51 +++++++++++++++++++ .../run047-grp-b-20260719-151223.md | 51 +++++++++++++++++++ .../run048-grp-b-20260719-151427.md | 51 +++++++++++++++++++ .../run049-grp-b-20260719-151715.md | 51 +++++++++++++++++++ .../run050-grp-b-20260719-152024.md | 51 +++++++++++++++++++ .../run051-grp-b-20260719-152300.md | 51 +++++++++++++++++++ .../run052-grp-b-20260719-152718.md | 51 +++++++++++++++++++ .../run053-grp-b-20260719-153031.md | 51 +++++++++++++++++++ .../run054-grp-b-20260719-153227.md | 51 +++++++++++++++++++ .../run055-grp-b-20260719-153508.md | 51 +++++++++++++++++++ .../run056-grp-b-20260719-153711.md | 51 +++++++++++++++++++ .../run057-grp-b-20260719-153916.md | 51 +++++++++++++++++++ .../run058-grp-c-20260719-154916.md | 51 +++++++++++++++++++ .../run059-grp-c-20260719-155209.md | 51 +++++++++++++++++++ .../run060-grp-c-20260719-155510.md | 51 +++++++++++++++++++ 20 files changed, 1020 insertions(+) create mode 100644 docs/debug/repro-matrix/run041-grp-b-20260719-145713.md create mode 100644 docs/debug/repro-matrix/run042-grp-b-20260719-150121.md create mode 100644 docs/debug/repro-matrix/run043-grp-b-20260719-150356.md create mode 100644 docs/debug/repro-matrix/run044-grp-b-20260719-150606.md create mode 100644 docs/debug/repro-matrix/run045-grp-b-20260719-150805.md create mode 100644 docs/debug/repro-matrix/run046-grp-b-20260719-151024.md create mode 100644 docs/debug/repro-matrix/run047-grp-b-20260719-151223.md create mode 100644 docs/debug/repro-matrix/run048-grp-b-20260719-151427.md create mode 100644 docs/debug/repro-matrix/run049-grp-b-20260719-151715.md create mode 100644 docs/debug/repro-matrix/run050-grp-b-20260719-152024.md create mode 100644 docs/debug/repro-matrix/run051-grp-b-20260719-152300.md create mode 100644 docs/debug/repro-matrix/run052-grp-b-20260719-152718.md create mode 100644 docs/debug/repro-matrix/run053-grp-b-20260719-153031.md create mode 100644 docs/debug/repro-matrix/run054-grp-b-20260719-153227.md create mode 100644 docs/debug/repro-matrix/run055-grp-b-20260719-153508.md create mode 100644 docs/debug/repro-matrix/run056-grp-b-20260719-153711.md create mode 100644 docs/debug/repro-matrix/run057-grp-b-20260719-153916.md create mode 100644 docs/debug/repro-matrix/run058-grp-c-20260719-154916.md create mode 100644 docs/debug/repro-matrix/run059-grp-c-20260719-155209.md create mode 100644 docs/debug/repro-matrix/run060-grp-c-20260719-155510.md diff --git a/docs/debug/repro-matrix/run041-grp-b-20260719-145713.md b/docs/debug/repro-matrix/run041-grp-b-20260719-145713.md new file mode 100644 index 0000000..973578a --- /dev/null +++ b/docs/debug/repro-matrix/run041-grp-b-20260719-145713.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-145713 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 41 started: 2026-07-19 14:57:13 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 14:57:32.389 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-145745 + post-recover = docs/debug/blackoutd-diag-20260719-145823 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed, music on +``` diff --git a/docs/debug/repro-matrix/run042-grp-b-20260719-150121.md b/docs/debug/repro-matrix/run042-grp-b-20260719-150121.md new file mode 100644 index 0000000..3943c58 --- /dev/null +++ b/docs/debug/repro-matrix/run042-grp-b-20260719-150121.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-150121 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 42 started: 2026-07-19 15:01:21 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:01:40.057 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-150153 + post-recover = docs/debug/blackoutd-diag-20260719-150230 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed, music on +``` diff --git a/docs/debug/repro-matrix/run043-grp-b-20260719-150356.md b/docs/debug/repro-matrix/run043-grp-b-20260719-150356.md new file mode 100644 index 0000000..4ee9e34 --- /dev/null +++ b/docs/debug/repro-matrix/run043-grp-b-20260719-150356.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-150356 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 43 started: 2026-07-19 15:03:56 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:04:15.022 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-150428 + post-recover = docs/debug/blackoutd-diag-20260719-150506 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed, music on +``` diff --git a/docs/debug/repro-matrix/run044-grp-b-20260719-150606.md b/docs/debug/repro-matrix/run044-grp-b-20260719-150606.md new file mode 100644 index 0000000..e38883d --- /dev/null +++ b/docs/debug/repro-matrix/run044-grp-b-20260719-150606.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-150606 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 44 started: 2026-07-19 15:06:06 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:06:25.215 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-150638 + post-recover = docs/debug/blackoutd-diag-20260719-150716 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed, music on +``` diff --git a/docs/debug/repro-matrix/run045-grp-b-20260719-150805.md b/docs/debug/repro-matrix/run045-grp-b-20260719-150805.md new file mode 100644 index 0000000..027cb08 --- /dev/null +++ b/docs/debug/repro-matrix/run045-grp-b-20260719-150805.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-150805 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 45 started: 2026-07-19 15:08:05 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:08:24.438 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-150837 + post-recover = docs/debug/blackoutd-diag-20260719-150915 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed, music on +``` diff --git a/docs/debug/repro-matrix/run046-grp-b-20260719-151024.md b/docs/debug/repro-matrix/run046-grp-b-20260719-151024.md new file mode 100644 index 0000000..cbf6e48 --- /dev/null +++ b/docs/debug/repro-matrix/run046-grp-b-20260719-151024.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-151024 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 46 started: 2026-07-19 15:10:24 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:10:43.280 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-151056 + post-recover = docs/debug/blackoutd-diag-20260719-151134 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: No recovery needed (R0), music on (M1) +``` diff --git a/docs/debug/repro-matrix/run047-grp-b-20260719-151223.md b/docs/debug/repro-matrix/run047-grp-b-20260719-151223.md new file mode 100644 index 0000000..77a53bf --- /dev/null +++ b/docs/debug/repro-matrix/run047-grp-b-20260719-151223.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-151223 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 47 started: 2026-07-19 15:12:23 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:12:42.962 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-151255 + post-recover = docs/debug/blackoutd-diag-20260719-151335 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: R0 M1, on AC not battery +``` diff --git a/docs/debug/repro-matrix/run048-grp-b-20260719-151427.md b/docs/debug/repro-matrix/run048-grp-b-20260719-151427.md new file mode 100644 index 0000000..7423826 --- /dev/null +++ b/docs/debug/repro-matrix/run048-grp-b-20260719-151427.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-151427 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 48 started: 2026-07-19 15:14:27 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [ ] no [x] yes: closed + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:15:02.022 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-151501 + post-recover = docs/debug/blackoutd-diag-20260719-151540 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: R0 M1, wno visible or audible wake until clamshell reopened +``` diff --git a/docs/debug/repro-matrix/run049-grp-b-20260719-151715.md b/docs/debug/repro-matrix/run049-grp-b-20260719-151715.md new file mode 100644 index 0000000..1e93070 --- /dev/null +++ b/docs/debug/repro-matrix/run049-grp-b-20260719-151715.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-151715 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 49 started: 2026-07-19 15:17:15 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:17:34.145 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-151747 + post-recover = docs/debug/blackoutd-diag-20260719-151825 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: M1. Growing frustrated without a cursor-on-black repro, I physically unplugged/replugged the external display's USB-C (-> HDMI) cable from the Macbook prior to this run. +``` diff --git a/docs/debug/repro-matrix/run050-grp-b-20260719-152024.md b/docs/debug/repro-matrix/run050-grp-b-20260719-152024.md new file mode 100644 index 0000000..09e5a51 --- /dev/null +++ b/docs/debug/repro-matrix/run050-grp-b-20260719-152024.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-152024 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 50 started: 2026-07-19 15:20:24 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:20:43.503 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-152056 + post-recover = docs/debug/blackoutd-diag-20260719-152134 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: R0 M1 +``` diff --git a/docs/debug/repro-matrix/run051-grp-b-20260719-152300.md b/docs/debug/repro-matrix/run051-grp-b-20260719-152300.md new file mode 100644 index 0000000..82329f5 --- /dev/null +++ b/docs/debug/repro-matrix/run051-grp-b-20260719-152300.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-152300 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 51 started: 2026-07-19 15:23:00 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:23:19.909 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-152332 + post-recover = docs/debug/blackoutd-diag-20260719-152410 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: M1. Physically unplug/replug the external display's USB-C side of its USB-C -> HDMI cable from the Macbook prior to the run (C1) +``` diff --git a/docs/debug/repro-matrix/run052-grp-b-20260719-152718.md b/docs/debug/repro-matrix/run052-grp-b-20260719-152718.md new file mode 100644 index 0000000..d592f65 --- /dev/null +++ b/docs/debug/repro-matrix/run052-grp-b-20260719-152718.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-152718 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 52 started: 2026-07-19 15:27:18 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:27:37.737 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-152751 + post-recover = docs/debug/blackoutd-diag-20260719-152829 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: C1 M1 +``` diff --git a/docs/debug/repro-matrix/run053-grp-b-20260719-153031.md b/docs/debug/repro-matrix/run053-grp-b-20260719-153031.md new file mode 100644 index 0000000..102a315 --- /dev/null +++ b/docs/debug/repro-matrix/run053-grp-b-20260719-153031.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-153031 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 53 started: 2026-07-19 15:30:31 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:30:50.731 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-153103 + post-recover = docs/debug/blackoutd-diag-20260719-153141 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: C1 M1 +``` diff --git a/docs/debug/repro-matrix/run054-grp-b-20260719-153227.md b/docs/debug/repro-matrix/run054-grp-b-20260719-153227.md new file mode 100644 index 0000000..893f4a8 --- /dev/null +++ b/docs/debug/repro-matrix/run054-grp-b-20260719-153227.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-153227 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 54 started: 2026-07-19 15:32:27 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:32:46.032 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-153259 + post-recover = docs/debug/blackoutd-diag-20260719-153337 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: C1 M1 +``` diff --git a/docs/debug/repro-matrix/run055-grp-b-20260719-153508.md b/docs/debug/repro-matrix/run055-grp-b-20260719-153508.md new file mode 100644 index 0000000..694e52f --- /dev/null +++ b/docs/debug/repro-matrix/run055-grp-b-20260719-153508.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-153508 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 55 started: 2026-07-19 15:35:08 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:35:27.476 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-153541 + post-recover = docs/debug/blackoutd-diag-20260719-153620 + +@ wake (immediate — first look as the panels light): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: C1 M1 R0 +``` diff --git a/docs/debug/repro-matrix/run056-grp-b-20260719-153711.md b/docs/debug/repro-matrix/run056-grp-b-20260719-153711.md new file mode 100644 index 0000000..c9976b3 --- /dev/null +++ b/docs/debug/repro-matrix/run056-grp-b-20260719-153711.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-153711 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 56 started: 2026-07-19 15:37:11 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T18:56:53+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:37:30.616 blackoutd[92781:2163480] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-153743 + post-recover = docs/debug/blackoutd-diag-20260719-153821 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: C1 M1 +``` diff --git a/docs/debug/repro-matrix/run057-grp-b-20260719-153916.md b/docs/debug/repro-matrix/run057-grp-b-20260719-153916.md new file mode 100644 index 0000000..7110a6b --- /dev/null +++ b/docs/debug/repro-matrix/run057-grp-b-20260719-153916.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-153916 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 57 started: 2026-07-19 15:39:16 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T19:38:58+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:39:35.193 blackoutd[98648:2250223] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-153948 + post-recover = docs/debug/blackoutd-diag-20260719-154026 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: C1 M1 +``` diff --git a/docs/debug/repro-matrix/run058-grp-c-20260719-154916.md b/docs/debug/repro-matrix/run058-grp-c-20260719-154916.md new file mode 100644 index 0000000..137e056 --- /dev/null +++ b/docs/debug/repro-matrix/run058-grp-c-20260719-154916.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-154916 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 58 started: 2026-07-19 15:49:16 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T19:38:58+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:49:37.037 blackoutd[98648:2250223] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-154948 + post-recover = docs/debug/blackoutd-diag-20260719-155027 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: M1. Apple Watch was off, but the lock screen command seemed not to work as post-recovery was to an unlocked screen: sudo --validate && ./build/blackoutd repro --wake 15 --recover displaysleep --group C && osascript -l JavaScript -e 'Application("System Events").keystroke("q", {using:["control down","command down"]})' +``` diff --git a/docs/debug/repro-matrix/run059-grp-c-20260719-155209.md b/docs/debug/repro-matrix/run059-grp-c-20260719-155209.md new file mode 100644 index 0000000..6b4a9a0 --- /dev/null +++ b/docs/debug/repro-matrix/run059-grp-c-20260719-155209.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-155209 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 59 started: 2026-07-19 15:52:09 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T19:38:58+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:52:28.625 blackoutd[98648:2250223] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-155241 + post-recover = docs/debug/blackoutd-diag-20260719-155319 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: M1. sudo --validate && osascript -l JavaScript -e 'Application("System Events").keystroke("q", {using:["control down","command down"]})' && ./build/blackoutd repro --wake 15 --recover displaysleep --group C +``` diff --git a/docs/debug/repro-matrix/run060-grp-c-20260719-155510.md b/docs/debug/repro-matrix/run060-grp-c-20260719-155510.md new file mode 100644 index 0000000..6841cde --- /dev/null +++ b/docs/debug/repro-matrix/run060-grp-c-20260719-155510.md @@ -0,0 +1,51 @@ + + +# repro run sheet — 20260719-155510 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 60 started: 2026-07-19 15:55:10 +Build: v0.4.0-3-g460ad9d built: 2026-07-19T19:38:58+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 15:55:28.698 blackoutd[98648:2250223] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-155542 + post-recover = docs/debug/blackoutd-diag-20260719-155621 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: C1 M1. sudo --validate && osascript -l JavaScript -e 'Application("System Events").keystroke("q", {using:["control down","command down"]})' && ./build/blackoutd repro --wake 15 --recover displaysleep --group C +``` From 6e901dcb9905dbb4ea400af5f09750ede090653f Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 16:04:42 -0400 Subject: [PATCH 05/24] docs: index runs 41-60; add short codes, group C 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 --- docs/debug/cursor-on-black-matrix.md | 93 +++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/docs/debug/cursor-on-black-matrix.md b/docs/debug/cursor-on-black-matrix.md index db68ed0..3f46796 100644 --- a/docs/debug/cursor-on-black-matrix.md +++ b/docs/debug/cursor-on-black-matrix.md @@ -129,6 +129,28 @@ open B2/E2 and settle to B0/E1 within seconds): - `B1` — lit, cursor-on-black (role reversal — wrongly active) - `B2` — lit, showing desktop/content (blackout not holding) +**Notes short codes** (used in the sheets' Notes field from run 46 on): + +- `R0` — no recovery needed (the run settled clean; the group B/C + displaysleep cycle ran anyway but had nothing to clear) +- `M1` — Music.app playing during the run +- `C1` — pre-run cable trigger: the external's **USB-C end** of its + USB-C → HDMI cable was unplugged from the MacBook and replugged + *before* the run, leaving it unplugged until the built-in fully + redrew. Mostly (not completely) reliably provokes cursor-on-black + on the following wake — the first on-demand repro lever (found run + 49; missed on run 55). + +> **Built-in immediate-state caveat (2026-07-19).** The built-in +> panel is small and glare-prone, and a cursor is easy to miss — it +> is possible that all role reversals are *full* inversions +> (immediate `E2/B1`) and that some earlier records under-report the +> built-in as `B0`/`B2` because the cursor went unseen. From run 54 +> the maintainer pre-positions the cursor near the top of the screen +> and observed the full `E2/B1` inversion directly on runs 54, 56, +> and 57. Treat pre-54 *immediate built-in* states as approximate; +> settled states and Outcome lines are unaffected. + ## Per-run record `repro` generates this block prefilled (the `` fields) in the run @@ -292,17 +314,74 @@ is excluded from every tally. | 38 | [run038](repro-matrix/run038-grp-b-20260719-142134.md) | E0/B0 | no | n/a (was clean) | | 39 | [run039](repro-matrix/run039-grp-b-20260719-142353.md) | E0/B0 | no | n/a (was clean) | | 40 | [run040](repro-matrix/run040-grp-b-20260719-142621.md) | E0/B0 | no | n/a (was clean) | +| 41 | [run041](repro-matrix/run041-grp-b-20260719-145713.md) | E0/B0 | no | n/a (R0) | +| 42 | [run042](repro-matrix/run042-grp-b-20260719-150121.md) | E0/B0 | no | n/a (R0) | +| 43 | [run043](repro-matrix/run043-grp-b-20260719-150356.md) | E0/B0 | no | n/a (R0) | +| 44 | [run044](repro-matrix/run044-grp-b-20260719-150606.md) | E0/B0 | no | n/a (R0) | +| 45 | [run045](repro-matrix/run045-grp-b-20260719-150805.md) | E0/B0 | no | n/a (R0) | +| 46 | [run046](repro-matrix/run046-grp-b-20260719-151024.md) | E0/B0 | no | n/a (R0) | +| 47 | [run047](repro-matrix/run047-grp-b-20260719-151223.md) | E0/B0 | no | n/a (R0) | +| 48 | [run048](repro-matrix/run048-grp-b-20260719-151427.md) | E0/B0 | no | n/a (R0) | +| 49 | [run049](repro-matrix/run049-grp-b-20260719-151715.md) | E1/B0 | yes | **yes** | +| 50 | [run050](repro-matrix/run050-grp-b-20260719-152024.md) | E0/B0 | no | n/a (R0) | +| 51 | [run051](repro-matrix/run051-grp-b-20260719-152300.md) | E1/B0 | yes | **yes** | +| 52 | [run052](repro-matrix/run052-grp-b-20260719-152718.md) | E1/B0 | yes | **yes** | +| 53 | [run053](repro-matrix/run053-grp-b-20260719-153031.md) | E1/B0 | yes | **yes** | +| 54 | [run054](repro-matrix/run054-grp-b-20260719-153227.md) | E1/B0 | yes | **yes** | +| 55 | [run055](repro-matrix/run055-grp-b-20260719-153508.md) | E0/B0 | no | n/a (R0) | +| 56 | [run056](repro-matrix/run056-grp-b-20260719-153711.md) | E1/B0 | yes | **yes** | +| 57 | [run057](repro-matrix/run057-grp-b-20260719-153916.md) | E1/B0 | yes | **yes** | + +Codes on the extension runs (see legend): `M1` on runs 37–57; `C1` on +runs 49 and 51–57 (49 is where the cable trigger was discovered — its +sheet describes the unplug/replug in prose; run 55 is the C1 miss — +cable trigger applied, wake settled clean). Run 47 ran on AC; run 48's +lid was closed during sleep (no visible or audible wake until the +clamshell reopened). + +Group B verdict: 8 blacks (runs 37, 49, 51–54, 56–57), and the +`displaysleep` recovery cleared **every one** end-to-end (post-recover +E0/B0) — well past the n ≥ 2 consistency bar; cohort 1's four manual +clears corroborate. The group's question is answered: yes, the +display-sleep cycle clears it. + +### Group C (runs 058+) — recovery while locked + +Working invocation (run 59 first; lock the session **before** +starting `repro` — run 58 chained the `osascript` *after* `repro`, so +the lock never took effect and the run executed unlocked): + +```sh +sudo --validate && \ + osascript -l JavaScript -e 'Application("System Events").keystroke("q", {using:["control down","command down"]})' && \ + ./build/blackoutd repro --wake 15 --recover displaysleep --group C +``` + +(`ctrl-cmd-q` locks the screen; needs Accessibility permission for the +terminal app. Keep the Apple Watch off/out of range per the LOCKED +note above.) + +| run | sheet | session | settled E/B | black? | recovery cleared? | +|-----|--------------------------------------------------------|---------|-------------|--------|-------------------| +| 58 | [run058](repro-matrix/run058-grp-c-20260719-154916.md) | unlocked¹ | E1/B0 | yes | **yes** | +| 59 | [run059](repro-matrix/run059-grp-c-20260719-155209.md) | locked | E0/B0 | no | n/a (R0) | +| 60 | [run060](repro-matrix/run060-grp-c-20260719-155510.md) | locked | E1/B0 | yes | **yes** | -14 valid runs, one black (run 37): the `displaysleep` recovery cleared -it end-to-end (post-recover E0/B0). Positive but n=1; cohort 1's four -manual displaysleep clears corroborate. Group B stays open until a -second black lands (n ≥ 2). +¹ Run 58's lock keystroke ran after `repro` (see above); the sheet's +machine-read session field correctly records unlocked → unlocked, so +it is effectively a group B data point. Codes: `M1` on 58–60; `C1` on +60. Run 58 went black *without* the cable trigger (a natural repro +amid the C1 series). Run 60 is the first **locked** black: the +displaysleep recovery cleared it while the session stayed locked +(n=1; group C stays open until n ≥ 2). **Detection result (2026-07-19)**: the field-by-field diff over this cohort found a WindowServer log marker that splits black from clean — -10/10 black wakes vs 0/15 clean across both cohorts. See -`docs/technical-debt.md` P29 (2026-07-19 update) for the diff record, -mechanism reading, and caveats. +10/10 black wakes vs 0/15 clean across both cohorts, extended to +**19/19 vs 0/26** by the run 41–60 extension (including the C1-miss +run 55 and the locked runs). See `docs/technical-debt.md` P29 +(2026-07-19 updates) for the diff record, mechanism reading, and +caveats. ## What we do with the data From f2cb45ece193a290a3437ae41288e526181e0408 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 16:04:59 -0400 Subject: [PATCH 06/24] docs: extend P29 with runs 41-60 findings 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 --- docs/technical-debt.md | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/technical-debt.md b/docs/technical-debt.md index 6f46c37..55b0494 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -1789,6 +1789,78 @@ Sequence: 3. Only then wire detector ⇒ recovery, behind a `recoveryStrategy` pref (separate PR, per the matrix's "What we do with the data"). +**Update (2026-07-19, later) — extension runs 41–60: marker at 19/19 +vs 0/26; a manual repro lever (C1); recovery question answered.** + +- *Marker validated on 20 more runs.* Blacks 49, 51–54, 56–57 (group + B) and 58, 60 (group C series): the hotplug-coalescing marker fires + in-window on **9/9**; the 11 cleans (41–48, 50, 55, 59) show **0**. + Running total **19/19 black vs 0/26 clean** across all recorded + runs. The FuseBoard occluded→visible secondary is demoted: the + extension breaks its clean split (black run 49 shows 1, locked + black run 60 shows 5) — corroborator only, not a detector. +- *C1 — the cable trigger (first on-demand repro lever).* Unplugging + the external's **USB-C end** pre-run (leaving it out until the + built-in fully redraws, then replugging) provoked cursor-on-black + on the next wake in 8 of 9 tries (blacks 49, 51–54, 56–57, 60; + miss 55). Mechanistically consistent with the coalescing story: a + fresh physical re-attach immediately before sleep leaves link + state that drops again across the sleep boundary. Critically, the + C1-miss run 55 shows the pre-run replug alone does **not** plant + the marker — the marker still tracks the black, not the cable + handling. Run 58 went black with *no* C1 (natural repro amid the + series). C1 makes recovery-candidate testing cheap: black on + demand, ~3 min per data point. +- *Recovery: displaysleep is 10/10.* Every black in the extension was + cleared end-to-end by `--recover displaysleep` (`pmset + displaysleepnow` + `caffeinate -u`, no root), including run 60 + **while the session stayed locked**. With run 37 that is 10/10 + programmatic clears plus cohort 1's four manual ones. Group B's + question is answered; group C (locked) is 1/1 and stays open until + n ≥ 2. The 2026-05-21 "displaysleepnow = flicker dead end" note is + fully superseded for the *recovery* path (flicker accepted). +- *Deterministic detection beyond the log query (survey, untested).* + The marker is not a proxy — it is the trace of the exact SkyLight + branch that matters (the pending-"out" coalescing decision), and + no exported SkyLight symbol surfaces that queue state; a private + SkyLight *notification* would sit downstream of the same coalescing + and inherit the blind spot (the falsified `0x133e` flag is the + public shadow of exactly that). The genuinely deterministic path is + to observe the raw hotplug **upstream** of WindowServer: + `IOMobileFramebuffer.framework` exports + `IOMobileFramebufferEnableHotPlugDetectNotifications` / + `IOMobileFramebufferGetHotPlugRunLoopSource` (per-framebuffer HPD + events; BetterDisplay links this framework from an ordinary + notarized app, so the user client is reachable without special + entitlements). blackoutd already opens no IOMFB connection; a + read-only spike would open the external framebuffer + (`IOMobileFramebufferOpenByName`, name `external-0` per the WS + logs) at daemon start, log timestamped HPD events, and let a C1 + series show whether an HPD-event pattern around the sleep boundary + splits black/clean. If it does, detection needs no log query at + all. Until then, `OSLogStore` (in-process, structured predicate, + no subprocess) is the implementation of record for the marker + query — prefer it over spawning `log show`. +- *Targeted recovery beyond displaysleep (survey, untested).* + Candidates, in prototype order: (1) + `IOMobileFramebufferRequestPowerChange` on the external framebuffer + only — a DCP-level power cycle scoped to one display, no global + display sleep, no lock-screen interaction; (2) blackoutd's own + primitive, `CGSConfigureDisplayEnabled(external, false→true)` — no + new API but CG-level, may not reach the DCP (P29 lever b, still + untested); (3) DDC power cycle of the sink (VCP 0xD6 off→on via + `IOAVService` I2C, the Lunar/m1ddc path, open source — no RE + needed) — the software analog of the physical replug that C1 shows + re-arms the link, but sink-side and dependent on the SP2309W's DDC + support. Test each as a new `--recover` method against C1 blacks; + adopt over displaysleep only if ≥ its reliability. BetterDisplay + RE (its `_reinitializeOnWake` implementation) is the fallback if + all three miss. +- *Group C invocation* (osascript lock before `repro`) is recorded in + the matrix, with run 58's ordering lesson (lock keystroke chained + after `repro` never took effect; its machine-read session field + caught it). + **Files**: `src/main.m` (the `diagnose` `dcp.txt` / `connection-mode.txt` readers); investigation otherwise. Relates to P2, P20, P28, ADR 0003, ADR 0009 (SP2309W YCbCr), and the inject_edid `--mode` reader (of which From ee5ce9f5fa93d179d24288b36af62fb3f31f94de Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 17:38:41 -0400 Subject: [PATCH 07/24] feat: add extcycle recovery and repro trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/main.m | 186 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 157 insertions(+), 29 deletions(-) diff --git a/src/main.m b/src/main.m index a78cf0f..803e76f 100644 --- a/src/main.m +++ b/src/main.m @@ -968,12 +968,18 @@ static void printUsage(void) { " daemon stop Stop the daemon and restore built-in display\n" "\n" "Experimental (cursor-on-black investigation; maintainer-run):\n" - " recover Run one display-sleep recovery cycle\n" - " (--method displaysleep, --dry-run)\n" + " recover Run one recovery cycle (--method METHOD, --dry-run)\n" + " Methods: displaysleep — display-sleep cycle (pmset\n" + " displaysleepnow + caffeinate -u; the validated\n" + " recovery); extcycle — disable/re-enable the external\n" + " display via CG (experimental, narrower; no flicker\n" + " on the built-in beyond the daemon's own restore)\n" " repro Sleep/wake repro with labeled captures + recovery;\n" " emits a numbered, prefilled run sheet and copies\n" " bundles to docs/debug/ when run from the repo root\n" - " (--wake N, --settle S, --recover METHOD,\n" + " (--wake N, --settle S, --recover METHOD [as above],\n" + " --trigger extcycle [pre-sleep software analog of\n" + " the C1 cable trigger — see the matrix legend],\n" " --group a..e [matrix coverage row, any case],\n" " --no-copy, --no-prompt, --silent, --dry-run)\n" "\n" @@ -1072,14 +1078,102 @@ static void sayCue(NSString *text, BOOL silent, BOOL dryRun) { runStep(@"/usr/bin/say", @[ text ], dryRun); } -// Performs one recovery attempt. The only method today is "displaysleep": a -// programmatic display-sleep cycle (pmset displaysleepnow, then caffeinate -u -// to re-declare user activity and wake the panel). Flicker is accepted; this -// supersedes the older "displaysleepnow = flicker dead end" note now that the -// maintainer confirms it reliably recovers regardless of lock state. +// Private API (see DisplayController.m for the daemon-side declaration and +// rationale): enables/disables a display inside a CGDisplayConfigRef +// transaction. Declared here for the CLI-side extcycle primitive below. +extern CGError CGSConfigureDisplayEnabled(CGDisplayConfigRef, CGDirectDisplayID, + bool); + +// Returns the first online hardware-backed external display, or +// kCGNullDirectDisplay if none. Virtual/placeholder displays carry FourCC +// pseudo-vendor IDs > 0xFFFF (see displayIsHardwareBacked() in +// DisplayController.m; the vendor heuristic alone suffices here and avoids +// the deprecated CGDisplayIOServicePort in this file). +static CGDirectDisplayID firstExternalDisplayID(void) { + CGDirectDisplayID displays[8]; + uint32_t count = 0; + CGGetOnlineDisplayList(8, displays, &count); + for (uint32_t i = 0; i < count; i++) { + if (!CGDisplayIsBuiltin(displays[i]) && + CGDisplayVendorNumber(displays[i]) <= 0xFFFF) + return displays[i]; + } + return kCGNullDirectDisplay; +} + +// CLI-side mirror of DisplayController's setDisplay:enabled: transaction +// (without the daemon's recommit preamble). The resident daemon reacts to +// the resulting reconfiguration callbacks exactly as it would to a physical +// unplug/replug: safety-invariant restore on disable, auto-blackout on +// re-enable. +static CGError cliSetDisplayEnabled(CGDirectDisplayID display, bool enabled) { + CGDisplayConfigRef config; + CGError err = CGBeginDisplayConfiguration(&config); + if (err != kCGErrorSuccess) + return err; + err = CGSConfigureDisplayEnabled(config, display, enabled); + if (err != kCGErrorSuccess) { + CGCancelDisplayConfiguration(config); + return err; + } + return CGCompleteDisplayConfiguration(config, kCGConfigurePermanently); +} + +// The "extcycle" primitive — software analog of the C1 cable trigger and +// P29's recovery lever (b): disable the external display, wait for the +// built-in to come back and fully redraw (the daemon's safety invariant +// restores it; C1's lesson is that replugging before the redraw completes +// weakens the trigger), then re-enable the external and give it the +// few-seconds re-attach settle (P20). Serves as both a repro --trigger +// (provoke cursor-on-black on the next wake) and a --recover method +// (tear down and rebuild the external's configuration). CG-level: whether +// it reaches the DCP where the no-op recommit did not is exactly the open +// question it exists to answer. +static int performExternalCycle(BOOL dryRun) { + if (dryRun) { + printf(" [dry-run] extcycle: disable external, wait 5 s, re-enable, " + "wait 5 s\n"); + return 0; + } + CGDirectDisplayID external = firstExternalDisplayID(); + if (external == kCGNullDirectDisplay) { + fprintf(stderr, "blackoutd: extcycle: no external display online\n"); + return 1; + } + CGError err = cliSetDisplayEnabled(external, false); + if (err != kCGErrorSuccess) { + fprintf(stderr, "blackoutd: extcycle: disable failed (CGError %d)\n", + (int)err); + return 1; + } + [NSThread sleepForTimeInterval:5.0]; + err = cliSetDisplayEnabled(external, true); + if (err != kCGErrorSuccess) { + fprintf(stderr, "blackoutd: extcycle: re-enable failed (CGError %d)\n", + (int)err); + return 1; + } + [NSThread sleepForTimeInterval:5.0]; + return 0; +} + +// Performs one recovery attempt. Methods: +// displaysleep — programmatic display-sleep cycle (pmset displaysleepnow, +// then caffeinate -u to re-declare user activity and wake +// the panel). Flicker is accepted; this supersedes the +// older "displaysleepnow = flicker dead end" note now that +// the matrix confirms it reliably recovers regardless of +// lock state (10/10 through run 60). +// extcycle — external display disable/re-enable cycle (see +// performExternalCycle above). Experimental: candidate +// narrower recovery, untested against a black as of run 60. static int performRecovery(NSString *method, BOOL dryRun) { + if ([method isEqualToString:@"extcycle"]) + return performExternalCycle(dryRun); if (![method isEqualToString:@"displaysleep"]) { - fprintf(stderr, "blackoutd: unknown recovery method '%s'\n", + fprintf(stderr, + "blackoutd: unknown recovery method '%s' (known: displaysleep, " + "extcycle)\n", method.UTF8String); return 1; } @@ -1091,7 +1185,7 @@ static int performRecovery(NSString *method, BOOL dryRun) { return runStep(@"/usr/bin/caffeinate", @[ @"-u", @"-t", @"2" ], dryRun); } -// blackoutd recover [--method displaysleep] [--dry-run] +// blackoutd recover [--method displaysleep|extcycle] [--dry-run] static int recoverCommand(int argc, const char *argv[]) { NSString *method = @"displaysleep"; BOOL dryRun = NO; @@ -1266,13 +1360,12 @@ static NSInteger ansIndex(NSDictionary *ans, // `settleMarker` is sampled ONCE by the caller and passed to every render: // re-reading it on the post-prompt rewrite could replace the post-wake // marker with one from a later event (e.g. a manual hot-corner recovery). -static NSString * -renderRunSheet(NSDate *started, int runNumber, int wake, int settle, - NSString *recover, NSString *group, NSString *lidPre, - NSString *lidCap, NSString *lockPre, NSString *lockCap, - NSString *powerPre, NSString *powerCap, NSString *settleMarker, - NSString *postWakeBundle, NSString *postRecoverBundle, - NSDictionary *ans) { +static NSString *renderRunSheet( + NSDate *started, int runNumber, int wake, int settle, NSString *recover, + NSString *trigger, NSString *group, NSString *lidPre, NSString *lidCap, + NSString *lockPre, NSString *lockCap, NSString *powerPre, + NSString *powerCap, NSString *settleMarker, NSString *postWakeBundle, + NSString *postRecoverBundle, NSDictionary *ans) { NSDateFormatter *stamp = [[NSDateFormatter alloc] init]; stamp.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; stamp.dateFormat = @"yyyyMMdd-HHmmss"; @@ -1329,6 +1422,8 @@ static NSInteger ansIndex(NSDictionary *ans, ? [NSString stringWithFormat:@"scheduled (--wake %d)", wake] : @"manual (--wake 0)"]; + [s appendFormat:@" trigger ................ %@\n", + trigger.length ? trigger : @"none"]; [s appendFormat:@" recovery applied ....... %@\n", recover.length ? recover : @"none"]; [s appendFormat:@" settle ................. %d s\n", settle]; @@ -1593,13 +1688,16 @@ static NSInteger promptChoice(NSString *question, int maxIndex, } // blackoutd repro [--wake N] [--settle S] [--recover METHOD] [--group G] -// [--no-copy] [--no-prompt] [--silent] [--dry-run] -// Schedules an auto-wake (sudo — only the schedule needs it), sleeps, then on -// wake captures a labeled bundle, optionally runs a recovery, and captures -// again. Built to run blind: it narrates each step via `say`. +// [--trigger extcycle] [--no-copy] [--no-prompt] [--silent] +// [--dry-run] +// Schedules an auto-wake (sudo — only the schedule needs it), optionally +// fires a pre-sleep trigger (the software analog of the C1 cable trigger), +// sleeps, then on wake captures a labeled bundle, optionally runs a +// recovery, and captures again. Built to run blind: it narrates each step +// via `say`. static int reproCommand(int argc, const char *argv[]) { int wake = 15, settle = 20; - NSString *recover = nil, *group = nil; + NSString *recover = nil, *group = nil, *trigger = nil; BOOL silent = NO, dryRun = NO, noCopy = NO, noPrompt = NO; for (int i = 2; i < argc; i++) { const char *opt = argv[i]; @@ -1611,6 +1709,20 @@ static int reproCommand(int argc, const char *argv[]) { noCopy = YES; } else if (strcmp(opt, "--no-prompt") == 0) { noPrompt = YES; + } else if (strcmp(opt, "--trigger") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "blackoutd: --trigger requires a value\n"); + return 1; + } + const char *val = argv[++i]; + // Validate at parse time: an unknown trigger discovered pre-sleep + // would otherwise abort the run after the sudo prompt already fired. + if (strcmp(val, "extcycle") != 0) { + fprintf(stderr, "blackoutd: unknown trigger '%s' (known: extcycle)\n", + val); + return 1; + } + trigger = @(val); } else if (strcmp(opt, "--wake") == 0 || strcmp(opt, "--settle") == 0 || strcmp(opt, "--recover") == 0 || strcmp(opt, "--group") == 0) { if (i + 1 >= argc) { @@ -1702,6 +1814,22 @@ static int reproCommand(int argc, const char *argv[]) { } } + // Pre-sleep trigger, after the schedule step (so a sudo prompt cannot + // stretch the trigger-to-sleep gap) and before the condition read (the + // cycle flips lock-adjacent display state the read should see settled). + // A failed trigger aborts: nothing is captured yet, and a run whose + // trigger silently misfired would poison the matrix's C1-vs-soft data. + if (trigger.length) { + sayCue(@"triggering external cycle", silent, dryRun); + int trigRC = performExternalCycle(dryRun); + if (trigRC != 0) { + fprintf(stderr, + "repro: trigger '%s' failed (rc=%d); aborting pre-sleep\n", + trigger.UTF8String, trigRC); + return 1; + } + } + // Conditions are read twice — just before sleep and again just before // the capture — so the sheet shows both ends of the run; only "did the // lid move DURING sleep" stays a maintainer field (unobservable from @@ -1799,10 +1927,10 @@ static int reproCommand(int argc, const char *argv[]) { // Write the blank sheet BEFORE the prompt pass: an unanswered or // interrupted pass must still leave the run fully recorded on disk. sheetPath = runSheetPath(started, group, &runNumber); - NSString *sheet = - renderRunSheet(started, runNumber, wake, settle, recover, group, lidPre, - lidCap, lockPre, lockCap, powerPre, powerCap, - settleMarker, postWakeShown, postRecoverShown, nil); + NSString *sheet = renderRunSheet(started, runNumber, wake, settle, recover, + trigger, group, lidPre, lidCap, lockPre, + lockCap, powerPre, powerCap, settleMarker, + postWakeShown, postRecoverShown, nil); if (writeBundleText(sheet, sheetPath)) printf("repro: run sheet written to %s\n", sheetPath.UTF8String); else { @@ -1831,9 +1959,9 @@ static int reproCommand(int argc, const char *argv[]) { sayCue(@"prompts timed out, sheet left blank", silent, NO); } else if (answers.count) { NSString *filled = renderRunSheet( - started, runNumber, wake, settle, recover, group, lidPre, lidCap, - lockPre, lockCap, powerPre, powerCap, settleMarker, postWakeShown, - postRecoverShown, answers); + started, runNumber, wake, settle, recover, trigger, group, lidPre, + lidCap, lockPre, lockCap, powerPre, powerCap, settleMarker, + postWakeShown, postRecoverShown, answers); if (writeBundleText(filled, sheetPath)) printf("repro: observations recorded in %s\n", sheetPath.UTF8String); else { From 0b3e1f6e3fbaf632257837b7bfe4b4bad2fd9eea Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 17:39:00 -0400 Subject: [PATCH 08/24] docs: record extcycle tooling; run 58 caveat 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 --- docs/debug/cursor-on-black-matrix.md | 22 +++++++++++++++++----- docs/technical-debt.md | 24 +++++++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/docs/debug/cursor-on-black-matrix.md b/docs/debug/cursor-on-black-matrix.md index 3f46796..40db06e 100644 --- a/docs/debug/cursor-on-black-matrix.md +++ b/docs/debug/cursor-on-black-matrix.md @@ -44,6 +44,15 @@ sudo --validate # primes sudo so the schedule-wake step never prompts ./build/blackoutd repro --wake 15 --recover displaysleep --group B ``` +To emulate the C1 cable trigger in software (no cable handling), add +`--trigger extcycle`: after scheduling the wake and before sleeping, +`repro` disables the external display via CG, waits 5 s for the +built-in to restore and redraw (the daemon's safety invariant does the +restore), re-enables it, and waits 5 s for the re-attach — then sleeps +as usual. The sheet's `trigger` field records it, so soft-trigger runs +are machine-distinguished from physical `C1` runs (which stay a Notes +code). + `repro` schedules the wake (the only step that needs `sudo`), sleeps, and on wake **speaks each step** (`say`) so you can follow it with the screen black. Each step also posts a Notification Center banner stamped `HH:mm:ss` — @@ -168,7 +177,8 @@ Conditions (pre-sleep -> at capture): session ................ locked|unlocked> power .................. battery|AC> wake ................... - recovery applied ....... + trigger ................ + recovery applied ....... settle ................. daemon settled ......... @@ -370,10 +380,12 @@ note above.) ¹ Run 58's lock keystroke ran after `repro` (see above); the sheet's machine-read session field correctly records unlocked → unlocked, so it is effectively a group B data point. Codes: `M1` on 58–60; `C1` on -60. Run 58 went black *without* the cable trigger (a natural repro -amid the C1 series). Run 60 is the first **locked** black: the -displaysleep recovery cleared it while the session stayed locked -(n=1; group C stays open until n ≥ 2). +60 — and **possibly on 58**: its Notes do not record C1, but the +maintainer later recalled the cable trigger may have been applied +there too, so do not cite run 58 as a trigger-free natural repro. Run +60 is the first **locked** black: the displaysleep recovery cleared +it while the session stayed locked (n=1; group C stays open until +n ≥ 2). **Detection result (2026-07-19)**: the field-by-field diff over this cohort found a WindowServer log marker that splits black from clean — diff --git a/docs/technical-debt.md b/docs/technical-debt.md index 55b0494..6d0bf26 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -1808,9 +1808,11 @@ vs 0/26; a manual repro lever (C1); recovery question answered.** state that drops again across the sleep boundary. Critically, the C1-miss run 55 shows the pre-run replug alone does **not** plant the marker — the marker still tracks the black, not the cable - handling. Run 58 went black with *no* C1 (natural repro amid the - series). C1 makes recovery-candidate testing cheap: black on - demand, ~3 min per data point. + handling. Run 58's Notes record no C1, but the maintainer later + recalled the cable trigger may have been applied there too — do + not cite run 58 as a trigger-free natural repro. C1 makes + recovery-candidate testing cheap: black on demand, ~3 min per + data point. - *Recovery: displaysleep is 10/10.* Every black in the extension was cleared end-to-end by `--recover displaysleep` (`pmset displaysleepnow` + `caffeinate -u`, no root), including run 60 @@ -1860,6 +1862,22 @@ vs 0/26; a manual repro lever (C1); recovery question answered.** the matrix, with run 58's ordering lesson (lock keystroke chained after `repro` never took effect; its machine-read session field caught it). +- *Tooling (implemented same day, runtime-untested).* Candidate (2) + above — the external disable/re-enable cycle — is now built as + `extcycle`, dispatchable both as a recovery (`recover --method + extcycle`, `repro --recover extcycle`) and as a pre-sleep trigger + (`repro --trigger extcycle`, the software analog of C1: disable, + 5 s for the built-in restore/redraw, re-enable, 5 s re-attach + settle). The sheet's conditions block gains a machine-filled + `trigger` line so soft-trigger runs are distinguished from + physical C1 runs, and `blackoutd --help` now documents the + recover/repro methods. Validation order: (a) soft-trigger A/B — + does `--trigger extcycle` reproduce the black like C1? Either + answer is informative: yes ⇒ fully software repro, no ⇒ the + trigger's essence is physical HPD, itself a mechanism datum; + (b) if C1 (or the soft trigger) keeps producing blacks, race + `--recover extcycle` against them — does a CG-level cycle clear + what the no-op recommit could not? **Files**: `src/main.m` (the `diagnose` `dcp.txt` / `connection-mode.txt` readers); investigation otherwise. Relates to P2, P20, P28, ADR 0003, ADR From dcaa3dc4e45f301f826c39c469cbfa833e61d710 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 22:51:38 -0400 Subject: [PATCH 09/24] docs: record run sheets 61-85 (soft trigger) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../run061-grp-b-20260719-202401.md | 52 +++++++++++++++++++ .../run062-grp-b-20260719-202947.md | 52 +++++++++++++++++++ .../run063-grp-b-20260719-203423.md | 52 +++++++++++++++++++ .../run064-grp-b-20260719-203715.md | 52 +++++++++++++++++++ .../run065-grp-b-20260719-203953.md | 52 +++++++++++++++++++ .../run066-grp-b-20260719-204340.md | 52 +++++++++++++++++++ .../run067-grp-b-20260719-204849.md | 52 +++++++++++++++++++ .../run068-grp-b-20260719-205256.md | 52 +++++++++++++++++++ .../run069-grp-b-20260719-205745.md | 52 +++++++++++++++++++ .../run070-grp-b-20260719-210158.md | 52 +++++++++++++++++++ .../run071-grp-b-20260719-210655.md | 52 +++++++++++++++++++ .../run072-grp-b-20260719-211053.md | 52 +++++++++++++++++++ .../run073-grp-b-20260719-211335.md | 52 +++++++++++++++++++ .../run074-grp-b-20260719-211608.md | 52 +++++++++++++++++++ .../run075-grp-b-20260719-212006.md | 52 +++++++++++++++++++ .../run076-grp-b-20260719-212233.md | 52 +++++++++++++++++++ .../run077-grp-b-20260719-212714.md | 52 +++++++++++++++++++ .../run078-grp-b-20260719-212937.md | 52 +++++++++++++++++++ .../run079-grp-b-20260719-213221.md | 52 +++++++++++++++++++ .../run080-grp-b-20260719-214926.md | 52 +++++++++++++++++++ .../run081-grp-c-20260719-215853.md | 52 +++++++++++++++++++ .../run082-grp-c-20260719-220650.md | 52 +++++++++++++++++++ .../run083-grp-c-20260719-220914.md | 52 +++++++++++++++++++ .../run084-grp-c-20260719-221345.md | 52 +++++++++++++++++++ .../run085-grp-c-20260719-222052.md | 52 +++++++++++++++++++ 25 files changed, 1300 insertions(+) create mode 100644 docs/debug/repro-matrix/run061-grp-b-20260719-202401.md create mode 100644 docs/debug/repro-matrix/run062-grp-b-20260719-202947.md create mode 100644 docs/debug/repro-matrix/run063-grp-b-20260719-203423.md create mode 100644 docs/debug/repro-matrix/run064-grp-b-20260719-203715.md create mode 100644 docs/debug/repro-matrix/run065-grp-b-20260719-203953.md create mode 100644 docs/debug/repro-matrix/run066-grp-b-20260719-204340.md create mode 100644 docs/debug/repro-matrix/run067-grp-b-20260719-204849.md create mode 100644 docs/debug/repro-matrix/run068-grp-b-20260719-205256.md create mode 100644 docs/debug/repro-matrix/run069-grp-b-20260719-205745.md create mode 100644 docs/debug/repro-matrix/run070-grp-b-20260719-210158.md create mode 100644 docs/debug/repro-matrix/run071-grp-b-20260719-210655.md create mode 100644 docs/debug/repro-matrix/run072-grp-b-20260719-211053.md create mode 100644 docs/debug/repro-matrix/run073-grp-b-20260719-211335.md create mode 100644 docs/debug/repro-matrix/run074-grp-b-20260719-211608.md create mode 100644 docs/debug/repro-matrix/run075-grp-b-20260719-212006.md create mode 100644 docs/debug/repro-matrix/run076-grp-b-20260719-212233.md create mode 100644 docs/debug/repro-matrix/run077-grp-b-20260719-212714.md create mode 100644 docs/debug/repro-matrix/run078-grp-b-20260719-212937.md create mode 100644 docs/debug/repro-matrix/run079-grp-b-20260719-213221.md create mode 100644 docs/debug/repro-matrix/run080-grp-b-20260719-214926.md create mode 100644 docs/debug/repro-matrix/run081-grp-c-20260719-215853.md create mode 100644 docs/debug/repro-matrix/run082-grp-c-20260719-220650.md create mode 100644 docs/debug/repro-matrix/run083-grp-c-20260719-220914.md create mode 100644 docs/debug/repro-matrix/run084-grp-c-20260719-221345.md create mode 100644 docs/debug/repro-matrix/run085-grp-c-20260719-222052.md diff --git a/docs/debug/repro-matrix/run061-grp-b-20260719-202401.md b/docs/debug/repro-matrix/run061-grp-b-20260719-202401.md new file mode 100644 index 0000000..a53bab7 --- /dev/null +++ b/docs/debug/repro-matrix/run061-grp-b-20260719-202401.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-202401 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 61 started: 2026-07-19 20:24:01 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:18:39+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 20:25:25.305 blackoutd[8558:2493339] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-202538 + post-recover = docs/debug/blackoutd-diag-20260719-202616 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: M0. NOTE: Scheduled wake failed; I had to depress the Space bar to wake. +``` diff --git a/docs/debug/repro-matrix/run062-grp-b-20260719-202947.md b/docs/debug/repro-matrix/run062-grp-b-20260719-202947.md new file mode 100644 index 0000000..122e240 --- /dev/null +++ b/docs/debug/repro-matrix/run062-grp-b-20260719-202947.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-202947 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 62 started: 2026-07-19 20:29:47 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:18:39+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 20:30:46.023 blackoutd[8558:2493339] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-203059 + post-recover = docs/debug/blackoutd-diag-20260719-203137 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Depressed trackpad to trigger wake; scheduled wake failed. +``` diff --git a/docs/debug/repro-matrix/run063-grp-b-20260719-203423.md b/docs/debug/repro-matrix/run063-grp-b-20260719-203423.md new file mode 100644 index 0000000..a633a45 --- /dev/null +++ b/docs/debug/repro-matrix/run063-grp-b-20260719-203423.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-203423 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 63 started: 2026-07-19 20:34:23 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:18:39+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 20:35:28.255 blackoutd[8558:2493339] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-203541 + post-recover = docs/debug/blackoutd-diag-20260719-203619 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Scheduled wake failed; depressed Space bar to trigger wake. +``` diff --git a/docs/debug/repro-matrix/run064-grp-b-20260719-203715.md b/docs/debug/repro-matrix/run064-grp-b-20260719-203715.md new file mode 100644 index 0000000..bab44db --- /dev/null +++ b/docs/debug/repro-matrix/run064-grp-b-20260719-203715.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-203715 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 64 started: 2026-07-19 20:37:15 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:18:39+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 20:38:09.271 blackoutd[8558:2493339] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-203822 + post-recover = docs/debug/blackoutd-diag-20260719-203900 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Scheduled wake failed; manual wake via depressing Space bar. +``` diff --git a/docs/debug/repro-matrix/run065-grp-b-20260719-203953.md b/docs/debug/repro-matrix/run065-grp-b-20260719-203953.md new file mode 100644 index 0000000..a9b556b --- /dev/null +++ b/docs/debug/repro-matrix/run065-grp-b-20260719-203953.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-203953 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 65 started: 2026-07-19 20:39:53 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:18:39+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 20:40:43.411 blackoutd[8558:2493339] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-204057 + post-recover = docs/debug/blackoutd-diag-20260719-204135 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Scheduled wake failed; manual wake triggered by depressing Space bar. +``` diff --git a/docs/debug/repro-matrix/run066-grp-b-20260719-204340.md b/docs/debug/repro-matrix/run066-grp-b-20260719-204340.md new file mode 100644 index 0000000..e983ac6 --- /dev/null +++ b/docs/debug/repro-matrix/run066-grp-b-20260719-204340.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-204340 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 66 started: 2026-07-19 20:43:40 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:18:39+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 20:45:13.803 blackoutd[8558:2493339] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-204527 + post-recover = docs/debug/blackoutd-diag-20260719-204605 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Just because I saw a black built-in on wake doesn't mean it didn't have a cursor-on-black, only that I didn't see it. I could have missed it. Scheduled wake failed even when run as `sudo --validate && ./build/blackoutd repro --wake 15 --trigger extcycle --recover displaysleep --group B`, which was surprising. Manual wake triggered by depressing Space bar. +``` diff --git a/docs/debug/repro-matrix/run067-grp-b-20260719-204849.md b/docs/debug/repro-matrix/run067-grp-b-20260719-204849.md new file mode 100644 index 0000000..65923cf --- /dev/null +++ b/docs/debug/repro-matrix/run067-grp-b-20260719-204849.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-204849 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 67 started: 2026-07-19 20:48:49 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:18:39+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 20:49:47.640 blackoutd[8558:2493339] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-205001 + post-recover = docs/debug/blackoutd-diag-20260719-205040 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Confirmed by careful observation, preceding repro with sudo --validate results in failed wake and cursor-on-black only on external display; built-in remains off throughout. Wake triggered by depressing space bar. +``` diff --git a/docs/debug/repro-matrix/run068-grp-b-20260719-205256.md b/docs/debug/repro-matrix/run068-grp-b-20260719-205256.md new file mode 100644 index 0000000..bd3260b --- /dev/null +++ b/docs/debug/repro-matrix/run068-grp-b-20260719-205256.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-205256 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 68 started: 2026-07-19 20:52:56 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:18:39+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 20:53:50.045 blackoutd[8558:2493339] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-205403 + post-recover = docs/debug/blackoutd-diag-20260719-205442 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Scheduled wake failed; triggered by depressing touchpad. +``` diff --git a/docs/debug/repro-matrix/run069-grp-b-20260719-205745.md b/docs/debug/repro-matrix/run069-grp-b-20260719-205745.md new file mode 100644 index 0000000..c54b974 --- /dev/null +++ b/docs/debug/repro-matrix/run069-grp-b-20260719-205745.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-205745 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 69 started: 2026-07-19 20:57:45 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:57:37+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 20:58:36.390 blackoutd[12629:2559119] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-205849 + post-recover = docs/debug/blackoutd-diag-20260719-205935 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Scheduled wake failed, wake manually triggered (W1). +``` diff --git a/docs/debug/repro-matrix/run070-grp-b-20260719-210158.md b/docs/debug/repro-matrix/run070-grp-b-20260719-210158.md new file mode 100644 index 0000000..a59ea63 --- /dev/null +++ b/docs/debug/repro-matrix/run070-grp-b-20260719-210158.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-210158 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 70 started: 2026-07-19 21:01:58 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:57:37+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:02:57.108 blackoutd[12629:2559119] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-210256 + post-recover = docs/debug/blackoutd-diag-20260719-210342 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 but I depressed Space bar as repro announced capturing post-wake +``` diff --git a/docs/debug/repro-matrix/run071-grp-b-20260719-210655.md b/docs/debug/repro-matrix/run071-grp-b-20260719-210655.md new file mode 100644 index 0000000..359462c --- /dev/null +++ b/docs/debug/repro-matrix/run071-grp-b-20260719-210655.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-210655 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 71 started: 2026-07-19 21:06:55 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:57:37+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:07:57.235 blackoutd[12629:2559119] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-210810 + post-recover = docs/debug/blackoutd-diag-20260719-210855 + +@ wake (immediate — first look as the panels light): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1, surprising result with external active on wake. +``` diff --git a/docs/debug/repro-matrix/run072-grp-b-20260719-211053.md b/docs/debug/repro-matrix/run072-grp-b-20260719-211053.md new file mode 100644 index 0000000..b7fb110 --- /dev/null +++ b/docs/debug/repro-matrix/run072-grp-b-20260719-211053.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-211053 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 72 started: 2026-07-19 21:10:53 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:57:37+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:11:51.827 blackoutd[12629:2559119] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-211205 + post-recover = docs/debug/blackoutd-diag-20260719-211251 + +@ wake (immediate — first look as the panels light): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run073-grp-b-20260719-211335.md b/docs/debug/repro-matrix/run073-grp-b-20260719-211335.md new file mode 100644 index 0000000..9ea74eb --- /dev/null +++ b/docs/debug/repro-matrix/run073-grp-b-20260719-211335.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-211335 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 73 started: 2026-07-19 21:13:35 +Build: v0.4.0-8-g2187621 built: 2026-07-20T00:57:37+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:14:19.569 blackoutd[12629:2559119] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-211433 + post-recover = docs/debug/blackoutd-diag-20260719-211520 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run074-grp-b-20260719-211608.md b/docs/debug/repro-matrix/run074-grp-b-20260719-211608.md new file mode 100644 index 0000000..3b1e264 --- /dev/null +++ b/docs/debug/repro-matrix/run074-grp-b-20260719-211608.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-211608 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 74 started: 2026-07-19 21:16:08 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:17:11.398 blackoutd[15374:2596027] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-211710 + post-recover = docs/debug/blackoutd-diag-20260719-211757 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run075-grp-b-20260719-212006.md b/docs/debug/repro-matrix/run075-grp-b-20260719-212006.md new file mode 100644 index 0000000..6562c18 --- /dev/null +++ b/docs/debug/repro-matrix/run075-grp-b-20260719-212006.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-212006 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 75 started: 2026-07-19 21:20:06 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:21:02.585 blackoutd[15374:2596027] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-212109 + post-recover = docs/debug/blackoutd-diag-20260719-212154 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run076-grp-b-20260719-212233.md b/docs/debug/repro-matrix/run076-grp-b-20260719-212233.md new file mode 100644 index 0000000..2220e45 --- /dev/null +++ b/docs/debug/repro-matrix/run076-grp-b-20260719-212233.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-212233 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 76 started: 2026-07-19 21:22:33 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:23:34.354 blackoutd[15374:2596027] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-212347 + post-recover = docs/debug/blackoutd-diag-20260719-212434 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run077-grp-b-20260719-212714.md b/docs/debug/repro-matrix/run077-grp-b-20260719-212714.md new file mode 100644 index 0000000..142703d --- /dev/null +++ b/docs/debug/repro-matrix/run077-grp-b-20260719-212714.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-212714 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 77 started: 2026-07-19 21:27:14 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:28:04.835 blackoutd[15374:2596027] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-212818 + post-recover = docs/debug/blackoutd-diag-20260719-212904 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run078-grp-b-20260719-212937.md b/docs/debug/repro-matrix/run078-grp-b-20260719-212937.md new file mode 100644 index 0000000..e717be2 --- /dev/null +++ b/docs/debug/repro-matrix/run078-grp-b-20260719-212937.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-212937 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 78 started: 2026-07-19 21:29:37 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:30:32.962 blackoutd[15374:2596027] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-213046 + post-recover = docs/debug/blackoutd-diag-20260719-213131 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run079-grp-b-20260719-213221.md b/docs/debug/repro-matrix/run079-grp-b-20260719-213221.md new file mode 100644 index 0000000..91fa61b --- /dev/null +++ b/docs/debug/repro-matrix/run079-grp-b-20260719-213221.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-213221 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 79 started: 2026-07-19 21:32:21 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:33:42.023 blackoutd[15374:2596027] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-213355 + post-recover = docs/debug/blackoutd-diag-20260719-213441 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run080-grp-b-20260719-214926.md b/docs/debug/repro-matrix/run080-grp-b-20260719-214926.md new file mode 100644 index 0000000..cd9019d --- /dev/null +++ b/docs/debug/repro-matrix/run080-grp-b-20260719-214926.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-214926 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 80 started: 2026-07-19 21:49:26 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:50:20.456 blackoutd[18563:2652232] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-215032 + post-recover = docs/debug/blackoutd-diag-20260719-215117 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: W1 +``` diff --git a/docs/debug/repro-matrix/run081-grp-c-20260719-215853.md b/docs/debug/repro-matrix/run081-grp-c-20260719-215853.md new file mode 100644 index 0000000..feae664 --- /dev/null +++ b/docs/debug/repro-matrix/run081-grp-c-20260719-215853.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-215853 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 81 started: 2026-07-19 21:58:53 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ none + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 21:59:11.789 blackoutd[18563:2652232] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-215925 + post-recover = docs/debug/blackoutd-diag-20260719-220015 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [x] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: I should clarify "Did the recovery clear the external panel? n" because it did clear it but then both displays went black. The system was not asleep because I heard the announcements. I triggered display wake by depressing Space bar after hearing "Recovery complete", then unlocked via TouchID, and answered the prompts while built-in was active and external black. +``` diff --git a/docs/debug/repro-matrix/run082-grp-c-20260719-220650.md b/docs/debug/repro-matrix/run082-grp-c-20260719-220650.md new file mode 100644 index 0000000..a9fd481 --- /dev/null +++ b/docs/debug/repro-matrix/run082-grp-c-20260719-220650.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-220650 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 82 started: 2026-07-19 22:06:50 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ none + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 22:07:11.061 blackoutd[18563:2652232] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-220723 + post-recover = docs/debug/blackoutd-diag-20260719-220807 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run083-grp-c-20260719-220914.md b/docs/debug/repro-matrix/run083-grp-c-20260719-220914.md new file mode 100644 index 0000000..914599f --- /dev/null +++ b/docs/debug/repro-matrix/run083-grp-c-20260719-220914.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-220914 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 83 started: 2026-07-19 22:09:14 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ none + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 22:10:26.608 blackoutd[18563:2652232] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-220942 + post-recover = docs/debug/blackoutd-diag-20260719-221028 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: I might have recorded built-in at wake wrong; it might have been B0 then. Cursor-on-black at settled. Recovery caused both displays to go black. I recovered them by depressing Space bar after hearing the "Repro complete" announcement, then unlocked via TouchID. Both displays are active. +``` diff --git a/docs/debug/repro-matrix/run084-grp-c-20260719-221345.md b/docs/debug/repro-matrix/run084-grp-c-20260719-221345.md new file mode 100644 index 0000000..d66e9c6 --- /dev/null +++ b/docs/debug/repro-matrix/run084-grp-c-20260719-221345.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-221345 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 84 started: 2026-07-19 22:13:45 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ none + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 22:14:03.991 blackoutd[18563:2652232] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-221418 + post-recover = docs/debug/blackoutd-diag-20260719-221507 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On recovery the external went black -> built-in active -> both black. Recovered after hearing "Repro complete" announcement via depressing Space bar and unlocking via TouchID to active built-in and black external. +``` diff --git a/docs/debug/repro-matrix/run085-grp-c-20260719-222052.md b/docs/debug/repro-matrix/run085-grp-c-20260719-222052.md new file mode 100644 index 0000000..f07861f --- /dev/null +++ b/docs/debug/repro-matrix/run085-grp-c-20260719-222052.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-222052 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 85 started: 2026-07-19 22:20:52 +Build: v0.4.0-8-g2187621 built: 2026-07-20T01:16:03+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ none + recovery applied ....... extcycle + settle ................. 20 s + daemon settled ......... 2026-07-19 22:21:11.712 blackoutd[18563:2652232] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-222125 + post-recover = docs/debug/blackoutd-diag-20260719-222222 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Recovery: active built-in -> black external -> black built-in -> manual display wake after the "Repro complete" announcement -> unlock via TouchID to E0/B0. +``` From 96b2c3a002f68790c15b8169fb047dcc9d578a1e Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 22:51:56 -0400 Subject: [PATCH 10/24] fix: repro wake ordering; add awake cue, --lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/main.m | 119 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 30 deletions(-) diff --git a/src/main.m b/src/main.m index 803e76f..5ede61d 100644 --- a/src/main.m +++ b/src/main.m @@ -977,9 +977,12 @@ static void printUsage(void) { " repro Sleep/wake repro with labeled captures + recovery;\n" " emits a numbered, prefilled run sheet and copies\n" " bundles to docs/debug/ when run from the repo root\n" - " (--wake N, --settle S, --recover METHOD [as above],\n" + " (--wake N [seconds; 0 = manual wake], --settle S,\n" + " --recover METHOD [as above],\n" " --trigger extcycle [pre-sleep software analog of\n" " the C1 cable trigger — see the matrix legend],\n" + " --lock [lock the session just before sleep;\n" + " keep the Apple Watch off/out of range],\n" " --group a..e [matrix coverage row, any case],\n" " --no-copy, --no-prompt, --silent, --dry-run)\n" "\n" @@ -1688,17 +1691,17 @@ static NSInteger promptChoice(NSString *question, int maxIndex, } // blackoutd repro [--wake N] [--settle S] [--recover METHOD] [--group G] -// [--trigger extcycle] [--no-copy] [--no-prompt] [--silent] -// [--dry-run] -// Schedules an auto-wake (sudo — only the schedule needs it), optionally -// fires a pre-sleep trigger (the software analog of the C1 cable trigger), -// sleeps, then on wake captures a labeled bundle, optionally runs a -// recovery, and captures again. Built to run blind: it narrates each step -// via `say`. +// [--trigger extcycle] [--lock] [--no-copy] [--no-prompt] +// [--silent] [--dry-run] +// Primes sudo, optionally fires a pre-sleep trigger (the software analog +// of the C1 cable trigger), schedules an auto-wake (sudo — only the +// schedule needs it), optionally locks the session, sleeps, then on wake +// captures a labeled bundle, optionally runs a recovery, and captures +// again. Built to run blind: it narrates each step via `say`. static int reproCommand(int argc, const char *argv[]) { int wake = 15, settle = 20; NSString *recover = nil, *group = nil, *trigger = nil; - BOOL silent = NO, dryRun = NO, noCopy = NO, noPrompt = NO; + BOOL silent = NO, dryRun = NO, noCopy = NO, noPrompt = NO, lock = NO; for (int i = 2; i < argc; i++) { const char *opt = argv[i]; if (strcmp(opt, "--silent") == 0) { @@ -1709,6 +1712,8 @@ static int reproCommand(int argc, const char *argv[]) { noCopy = YES; } else if (strcmp(opt, "--no-prompt") == 0) { noPrompt = YES; + } else if (strcmp(opt, "--lock") == 0) { + lock = YES; } else if (strcmp(opt, "--trigger") == 0) { if (i + 1 >= argc) { fprintf(stderr, "blackoutd: --trigger requires a value\n"); @@ -1781,21 +1786,49 @@ static int reproCommand(int argc, const char *argv[]) { printf("repro: started %s — build %s (built %s)\n", [human stringFromDate:started].UTF8String, BD_BUILD_GIT, BD_BUILD_TIME); - // Schedule the auto-wake first (only this needs sudo). wake=0 means the - // maintainer will wake the machine manually (e.g. by opening the lid). + // Prime sudo credentials up front (one interactive prompt at most, + // screen lit, nothing captured yet), so the schedule step below never + // prompts. Previously the matrix doc asked for a manual `sudo --validate + // &&` prefix; building it in removes that failure mode. + if (wake > 0) { + printf("repro: priming sudo credentials (sudo --validate)\n"); + if (runStep(@"/usr/bin/sudo", @[ @"--validate" ], dryRun) != 0) { + fprintf(stderr, "repro: sudo --validate failed; aborting\n"); + return 1; + } + } + + // Pre-sleep trigger, BEFORE the wake is scheduled: the cycle takes + // ~12-20 s (two cues, two 5 s settles, CG transactions), which is most + // of a --wake 15 lead — scheduling first left the wake time in the past + // by the time the machine slept, the cause of every W1 "scheduled wake + // failed" run (61-80). A failed trigger aborts: nothing is captured + // yet, and a run whose trigger silently misfired would poison the + // matrix's C1-vs-soft data. + if (trigger.length) { + sayCue(@"triggering external cycle", silent, dryRun); + int trigRC = performExternalCycle(dryRun); + if (trigRC != 0) { + fprintf(stderr, + "repro: trigger '%s' failed (rc=%d); aborting pre-sleep\n", + trigger.UTF8String, trigRC); + return 1; + } + } + + // Schedule the auto-wake, computing the wake time only now so the + // trigger's duration cannot eat the lead. wake=0 means the maintainer + // will wake the machine manually (e.g. by opening the lid). if (wake > 0) { NSDateFormatter *f = [[NSDateFormatter alloc] init]; f.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; f.dateFormat = @"MM/dd/yy HH:mm:ss"; NSString *when = [f stringFromDate:[NSDate dateWithTimeIntervalSinceNow:wake]]; - // Probe with --non-interactive first: when credentials are primed - // (sudo --validate, per the matrix doc) this never prompts. If they are - // not — including policies that disable credential caching entirely - // (timestamp_timeout=0), where priming cannot help — fall back to one - // interactive prompt. The fallback is safe by construction: this is - // repro's first step, before any sleep, with the screen lit; the blind - // phase contains no sudo calls. + // Probe with --non-interactive first (credentials were primed above). + // The interactive fallback remains for policies that disable credential + // caching entirely (timestamp_timeout=0), where priming cannot help; it + // is safe by construction — pre-sleep, screen lit, before --lock. printf("repro: scheduling wake at %s (sudo)\n", when.UTF8String); int schedRC = runStep(@"/usr/bin/sudo", @@ -1814,20 +1847,27 @@ static int reproCommand(int argc, const char *argv[]) { } } - // Pre-sleep trigger, after the schedule step (so a sudo prompt cannot - // stretch the trigger-to-sleep gap) and before the condition read (the - // cycle flips lock-adjacent display state the read should see settled). - // A failed trigger aborts: nothing is captured yet, and a run whose - // trigger silently misfired would poison the matrix's C1-vs-soft data. - if (trigger.length) { - sayCue(@"triggering external cycle", silent, dryRun); - int trigRC = performExternalCycle(dryRun); - if (trigRC != 0) { + // Lock the session last before sleep (after every possible sudo prompt), + // so the condition read below records the locked state. Same mechanism + // as the manual group C invocation this replaces: a ctrl-cmd-q keystroke + // via System Events (needs Accessibility permission for the terminal + // app). Remember to keep the Apple Watch off/out of range for the run. + if (lock) { + sayCue(@"locking session", silent, dryRun); + NSString *lockScript = + @"Application(\"System Events\").keystroke(\"q\", {using:[\"control " + @"down\",\"command down\"]})"; + int lockRC = runStep(@"/usr/bin/osascript", + @[ @"-l", @"JavaScript", @"-e", lockScript ], dryRun); + if (lockRC != 0) { fprintf(stderr, - "repro: trigger '%s' failed (rc=%d); aborting pre-sleep\n", - trigger.UTF8String, trigRC); + "repro: session lock failed (rc=%d); aborting — a run " + "recorded as locked must actually be locked\n", + lockRC); return 1; } + if (!dryRun) + [NSThread sleepForTimeInterval:2.0]; } // Conditions are read twice — just before sleep and again just before @@ -1841,7 +1881,26 @@ static int reproCommand(int argc, const char *argv[]) { sayCue(@"sleeping now", silent, dryRun); if (runStep(@"/usr/bin/pmset", @[ @"sleepnow" ], dryRun) != 0) return 1; - // System sleeps here; execution resumes on the scheduled wake. + // System sleeps here. Detect the wake instant rather than sleeping a + // fixed interval: 1 s thread sleeps do not run while the system is + // asleep, so a tick whose wall-clock duration jumps past 5 s means the + // machine slept across it and is now awake. Works for scheduled and + // manual wakes alike (a manual overnight wake just loops longer), and + // anchors the settle countdown at the wake itself, so the capture is a + // consistent `settle` seconds post-wake regardless of how long the + // machine slept. + if (!dryRun) { + for (;;) { + NSDate *tick = NSDate.date; + [NSThread sleepForTimeInterval:1.0]; + if (-[tick timeIntervalSinceNow] > 5.0) + break; + } + } + // First cue after wake — as close to the wake instant as the tick + // granularity allows (~1 s), so the eyewitness can anchor the + // "immediate" observation to it while the panels first light. + sayCue(@"awake", silent, dryRun); if (!dryRun) [NSThread sleepForTimeInterval:settle]; From bf5f482065c1e2ed2f5d3d6c46f8a2fd1519c8ea Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 22:52:30 -0400 Subject: [PATCH 11/24] docs: record soft-trigger series; attribute cause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/debug/cursor-on-black-matrix.md | 114 ++++++++++++++++++++++++--- docs/technical-debt.md | 62 +++++++++++++++ 2 files changed, 167 insertions(+), 9 deletions(-) diff --git a/docs/debug/cursor-on-black-matrix.md b/docs/debug/cursor-on-black-matrix.md index 40db06e..5fb2af9 100644 --- a/docs/debug/cursor-on-black-matrix.md +++ b/docs/debug/cursor-on-black-matrix.md @@ -33,17 +33,23 @@ time; their bundles' `version.txt` is authoritative). From a terminal with the panels visible (before sleep): ```sh -sudo --validate # primes sudo so the schedule-wake step never prompts - # (repro falls back to one pre-sleep prompt if it must) - # Baseline (detection): wake, capture, no recovery. Group A of the # coverage table below. ./build/blackoutd repro --wake 15 --group A # With recovery: wake, capture, display-sleep cycle, capture again. ./build/blackoutd repro --wake 15 --recover displaysleep --group B + +# Locked (group C): --lock issues the ctrl-cmd-q lock keystroke just +# before sleep (keep the Apple Watch off/out of range). +./build/blackoutd repro --wake 15 --recover displaysleep --group C --lock ``` +`repro` primes sudo itself (`sudo --validate` as its first step — one +interactive prompt at most, pre-sleep; builds before 2026-07-19 +evening needed a manual `sudo --validate &&` prefix and a separate +osascript lock command). + To emulate the C1 cable trigger in software (no cable handling), add `--trigger extcycle`: after scheduling the wake and before sleeping, `repro` disables the external display via CG, waits 5 s for the @@ -60,6 +66,11 @@ invisible during the black, but retained, so after recovery the notification log shows when each stage ran and pairs with the bundles' timestamps. Watch the **panels**, not the terminal. When you hear: +- **"awake"** — spoken within ~1 s of the wake instant (repro detects + the wake rather than sleeping a fixed interval). Anchor the + *immediate* observation to this cue; the settle countdown also + starts here, so the capture is a consistent `--settle` seconds + post-wake. - **"capturing post wake"** — look at both panels, fill the *post-wake* rows. - **"recovering"** then **"capturing post recover"** — look again, fill the *post-recover* rows. @@ -142,13 +153,22 @@ open B2/E2 and settle to B0/E1 within seconds): - `R0` — no recovery needed (the run settled clean; the group B/C displaysleep cycle ran anyway but had nothing to clear) -- `M1` — Music.app playing during the run +- `M1` — Music.app playing during the run (`M0` — explicitly not + playing) - `C1` — pre-run cable trigger: the external's **USB-C end** of its USB-C → HDMI cable was unplugged from the MacBook and replugged *before* the run, leaving it unplugged until the built-in fully redrew. Mostly (not completely) reliably provokes cursor-on-black on the following wake — the first on-demand repro lever (found run 49; missed on run 55). +- `W1` — scheduled wake failed; the maintainer woke the machine + manually (Space bar or trackpad press). Applies to every + `--trigger` run 61–80: a repro bug scheduled the wake *before* the + trigger, whose ~15 s of cueing and settling left the wake time in + the past by the time the machine slept. Fixed the same evening + (trigger now runs first; the wake time is computed after it). The + W1 runs stay valid — they additionally show the black reproduces + on manual wakes, so the failure is not scheduled-wake-specific. > **Built-in immediate-state caveat (2026-07-19).** The built-in > panel is small and glare-prone, and a cursor is easy to miss — it @@ -355,6 +375,57 @@ E0/B0) — well past the n ≥ 2 consistency bar; cohort 1's four manual clears corroborate. The group's question is answered: yes, the display-sleep cycle clears it. +### Group B — soft-trigger series (runs 061–080) + +All twenty runs used `--trigger extcycle` (the software C1 analog) and +carry `W1` (see the legend; wake was manual — Space bar or trackpad). +Runs 61–68 recovered with `displaysleep`, 69–80 with `extcycle`. + +| run | sheet | recovery | settled E/B | black? | cleared? | +|-----|--------------------------------------------------------|--------------|-------------|--------|----------| +| 61 | [run061](repro-matrix/run061-grp-b-20260719-202401.md) | displaysleep | E1/B0 | yes | yes | +| 62 | [run062](repro-matrix/run062-grp-b-20260719-202947.md) | displaysleep | E1/B0 | yes | yes | +| 63 | [run063](repro-matrix/run063-grp-b-20260719-203423.md) | displaysleep | E1/B0 | yes | yes | +| 64 | [run064](repro-matrix/run064-grp-b-20260719-203715.md) | displaysleep | E1/B0 | yes | yes | +| 65 | [run065](repro-matrix/run065-grp-b-20260719-203953.md) | displaysleep | E1/B0 | yes | yes | +| 66 | [run066](repro-matrix/run066-grp-b-20260719-204340.md) | displaysleep | E1/B0 | yes | yes | +| 67 | [run067](repro-matrix/run067-grp-b-20260719-204849.md) | displaysleep | E1/B0 | yes | yes | +| 68 | [run068](repro-matrix/run068-grp-b-20260719-205256.md) | displaysleep | E1/B0 | yes | yes | +| 69 | [run069](repro-matrix/run069-grp-b-20260719-205745.md) | extcycle | E1/B0 | yes | yes | +| 70 | [run070](repro-matrix/run070-grp-b-20260719-210158.md) | extcycle | E1/B0 | yes¹ | yes | +| 71 | [run071](repro-matrix/run071-grp-b-20260719-210655.md) | extcycle | E0/B0 | no | n/a | +| 72 | [run072](repro-matrix/run072-grp-b-20260719-211053.md) | extcycle | E0/B0 | no | n/a | +| 73 | [run073](repro-matrix/run073-grp-b-20260719-211335.md) | extcycle | E1/B0 | yes² | yes | +| 74 | [run074](repro-matrix/run074-grp-b-20260719-211608.md) | extcycle | E1/B0 | yes | yes | +| 75 | [run075](repro-matrix/run075-grp-b-20260719-212006.md) | extcycle | E1/B0 | yes² | yes | +| 76 | [run076](repro-matrix/run076-grp-b-20260719-212233.md) | extcycle | E1/B0 | yes | yes | +| 77 | [run077](repro-matrix/run077-grp-b-20260719-212714.md) | extcycle | E1/B0 | yes | yes | +| 78 | [run078](repro-matrix/run078-grp-b-20260719-212937.md) | extcycle | E1/B0 | yes | yes | +| 79 | [run079](repro-matrix/run079-grp-b-20260719-213221.md) | extcycle | E1/B0 | yes | yes | +| 80 | [run080](repro-matrix/run080-grp-b-20260719-214926.md) | extcycle | E1/B0 | yes | yes | + +¹ Run 70: the maintainer pressed the Space bar just as the "capturing +post wake" cue played (the press was already in motion) — treat the +settled record as possibly contaminated; the session was also locked +by capture time. +² Runs 73 and 75: the maintainer flags the *immediate* observation as +possibly incomplete (run 73 likely missed a built-in cursor-on-black +preceding the external's). Runs 76, 77, and 80 were carefully +observed; run 76's sequence was E2/B1 → brief E1/B1 → E1/B0 → +recovery to E0/B0. + +**Soft-trigger verdict**: `--trigger extcycle` provoked cursor-on-black +in **18 of 20** runs (misses 71, 72) — the repro is now fully +software-driven; no cable handling, no physical HPD event. `M1` on +none of 61–80 (run 61 notes `M0`). Codes: `W1` on all twenty. + +**extcycle-as-recovery verdict (split by lock state)**: while +**unlocked**, extcycle cleared 10/10 blacks (69–70, 73–80). While +**locked** (group C runs 81–85 below), it failed on both blacks and +*induced* a black external (or both panels black) even on clean wakes. +`displaysleep` remains the recovery of record; extcycle is usable only +in an unlocked session. + ### Group C (runs 058+) — recovery while locked Working invocation (run 59 first; lock the session **before** @@ -384,16 +455,41 @@ it is effectively a group B data point. Codes: `M1` on 58–60; `C1` on maintainer later recalled the cable trigger may have been applied there too, so do not cite run 58 as a trigger-free natural repro. Run 60 is the first **locked** black: the displaysleep recovery cleared -it while the session stayed locked (n=1; group C stays open until -n ≥ 2). +it while the session stayed locked (n=1 for displaysleep-locked). + +### Group C — extcycle recovery while locked (runs 081–085) + +Same locked invocation (osascript lock, then repro), `--recover +extcycle`, no trigger, Apple Watch off. These runs answer a different +question than planned: extcycle **must not be used while locked**. + +| run | sheet | settled E/B | black? | recovery outcome | +|-----|--------------------------------------------------------|-------------|--------|----------------------------------------------------| +| 81 | [run081](repro-matrix/run081-grp-c-20260719-215853.md) | E1/B0 | yes | cleared, then **both panels black**¹ | +| 82 | [run082](repro-matrix/run082-grp-c-20260719-220650.md) | E0/B0 | no | n/a (R0) | +| 83 | [run083](repro-matrix/run083-grp-c-20260719-220914.md) | E1/B0 | yes | **both panels black**; manual Space + unlock fixed | +| 84 | [run084](repro-matrix/run084-grp-c-20260719-221345.md) | E0/B0 | no | **induced** black external; left it CG-offline² | +| 85 | [run085](repro-matrix/run085-grp-c-20260719-222052.md) | E0/B0 | no | **induced** transient both-black; Space restored | + +¹ Run 81: after the both-black state, Space restored the built-in but +the external stayed black; four `blackoutd off`/`on` cycles did not +restore it; the next run's sleep/wake (run 82) did. +² Run 84's aftermath: a subsequent `--trigger extcycle` attempt failed +with `extcycle: no external display online` — the external was not +merely dark but absent from CGGetOnlineDisplayList. Run 85's +sleep/wake re-onlined it. An extcycle failure mode can therefore +drop the external below CG visibility entirely. **Detection result (2026-07-19)**: the field-by-field diff over this cohort found a WindowServer log marker that splits black from clean — 10/10 black wakes vs 0/15 clean across both cohorts, extended to **19/19 vs 0/26** by the run 41–60 extension (including the C1-miss -run 55 and the locked runs). See `docs/technical-debt.md` P29 -(2026-07-19 updates) for the diff record, mechanism reading, and -caveats. +run 55 and the locked runs), and to **39/39 vs 0/31** by the +soft-trigger series 61–85 — where the two trigger-but-clean runs (71, +72) show the trigger's own pre-sleep hotplug pair does **not** plant +the marker, and every marker timestamp sits at the wake, not at the +trigger. See `docs/technical-debt.md` P29 (2026-07-19 updates) for +the diff record, mechanism reading, and caveats. ## What we do with the data diff --git a/docs/technical-debt.md b/docs/technical-debt.md index 6d0bf26..cb3e0df 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -1879,6 +1879,68 @@ vs 0/26; a manual repro lever (C1); recovery question answered.** `--recover extcycle` against them — does a CG-level cycle clear what the no-op recommit could not? +**Update (2026-07-19, evening) — soft trigger works (18/20); marker +at 39/39 vs 0/31; extcycle recovery is lock-state-dependent; the bug +is host-side.** Runs 61–85 (matrix "soft-trigger series" and group C +extension tables). + +- *The repro is now fully software.* `--trigger extcycle` provoked + cursor-on-black in **18 of 20** runs (blacks 61–70, 73–80; misses + 71–72) with **no cable handling and no physical HPD event** — the + external only entered powersave during the cycle. Healthy-state + sanity: 9/9 manual `recover --method extcycle` cycles ran the same + sequence (external drops → built-in lights at native resolution → + external powersave → built-in re-mirrors the external's resolution + → built-in re-blacks → external returns). +- *All 20 trigger runs were `W1` (scheduled wake failed, manual + Space/trackpad wake) — a repro bug, not a power quirk*: 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. Fixed same evening: `sudo --validate` is now built into + repro (no more manual prefix), the trigger runs before the + schedule step and the wake time is computed after it, a new + **"awake" cue** speaks within ~1 s of the detected wake (the + settle countdown now anchors at the wake, not at sleepnow), and a + new `--lock` flag replaces the manual osascript lock chain. + Silver lining recorded: the W1 runs show the black reproduces on + *manual* wakes too — it is not scheduled-wake-specific. +- *Marker: 20/20 new blacks, 0/5 new cleans — running total 39/39 + vs 0/31.* Critically, the two trigger-but-clean runs (71, 72) + show the trigger's own pre-sleep hotplug pair does **not** plant + the marker, and every marker timestamp sits at the wake instant + (e.g. run 61: start 20:24:01, marker 20:25:20 at the manual + wake), not at the trigger. The detector survives the soft-trigger + regime intact. +- *extcycle as recovery: usable only unlocked.* Unlocked (runs + 69–80): cleared **10/10** blacks — the first recovery besides the + display-sleep class, and evidence that a CG-level teardown/rebuild + *can* re-arm the scanout (where the no-op recommit could not). + Locked (runs 81–85): it failed on both blacks (81, 83: cleared + then **both panels went black**) and **induced** a black external + on clean wakes (84, 85); run 84's aftermath left the external + absent from CGGetOnlineDisplayList entirely (`extcycle: no + external display online`) until the next sleep/wake re-onlined + it, and `blackoutd off`/`on` cycles did not restore it after run + 81. **displaysleep remains the recovery of record** (works locked + and unlocked); any future automatic wiring must either gate + extcycle on an unlocked session or just use displaysleep. +- *Attribution (maintainer asked: OS, cable, display, or interfering + software?).* The evidence points at the **macOS display stack + (SkyLight/DCP wake path), host-side**: the soft trigger reproduces + the black with the cable untouched and no physical HPD (cable and + monitor-side link exonerated as necessary conditions); the + detector is WindowServer's own hotplug-coalescing bookkeeping; and + every recovery that works (hot corner, displaysleep, unlocked + extcycle, physical replug) is a host-side pipeline rebuild. The + SP2309W/adapter's slow re-attach remains a plausible + *frequency* contributor (it shapes when a hotplug pair straddles + the wake), and one third-party confound is untested: + MonitorControlLite (software brightness shade) was running during + all recorded runs. Cheap control series: quit MonitorControlLite + (slider at 100%), run ~6 soft-trigger runs — if blacks persist, + the confound is retired; Brisync was never running and is + irrelevant. + **Files**: `src/main.m` (the `diagnose` `dcp.txt` / `connection-mode.txt` readers); investigation otherwise. Relates to P2, P20, P28, ADR 0003, ADR 0009 (SP2309W YCbCr), and the inject_edid `--mode` reader (of which From 9bf3c65f57fcbba279698dc5027272939c0c8dd5 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 23:27:14 -0400 Subject: [PATCH 12/24] fix: detect wake via NSWorkspace notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/main.m | 149 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 52 deletions(-) diff --git a/src/main.m b/src/main.m index 5ede61d..ba1e48d 100644 --- a/src/main.m +++ b/src/main.m @@ -1816,42 +1816,11 @@ static int reproCommand(int argc, const char *argv[]) { } } - // Schedule the auto-wake, computing the wake time only now so the - // trigger's duration cannot eat the lead. wake=0 means the maintainer - // will wake the machine manually (e.g. by opening the lid). - if (wake > 0) { - NSDateFormatter *f = [[NSDateFormatter alloc] init]; - f.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; - f.dateFormat = @"MM/dd/yy HH:mm:ss"; - NSString *when = - [f stringFromDate:[NSDate dateWithTimeIntervalSinceNow:wake]]; - // Probe with --non-interactive first (credentials were primed above). - // The interactive fallback remains for policies that disable credential - // caching entirely (timestamp_timeout=0), where priming cannot help; it - // is safe by construction — pre-sleep, screen lit, before --lock. - printf("repro: scheduling wake at %s (sudo)\n", when.UTF8String); - int schedRC = - runStep(@"/usr/bin/sudo", - @[ @"--non-interactive", @"pmset", @"schedule", @"wake", when ], - dryRun); - if (schedRC != 0 && !dryRun) { - fprintf(stderr, "repro: sudo credentials not cached; prompting now " - "(pre-sleep)\n"); - schedRC = runStep(@"/usr/bin/sudo", - @[ @"pmset", @"schedule", @"wake", when ], NO); - } - if (schedRC != 0) { - fprintf(stderr, "repro: could not schedule wake (rc=%d); aborting\n", - schedRC); - return 1; - } - } - - // Lock the session last before sleep (after every possible sudo prompt), - // so the condition read below records the locked state. Same mechanism - // as the manual group C invocation this replaces: a ctrl-cmd-q keystroke - // via System Events (needs Accessibility permission for the terminal - // app). Remember to keep the Apple Watch off/out of range for the run. + // Lock the session before the conditions read records the lock state. + // Same mechanism as the manual group C invocation this replaces: a + // ctrl-cmd-q keystroke via System Events (needs Accessibility permission + // for the terminal app). Remember to keep the Apple Watch off/out of + // range for the run. if (lock) { sayCue(@"locking session", silent, dryRun); NSString *lockScript = @@ -1873,33 +1842,109 @@ static int reproCommand(int argc, const char *argv[]) { // Conditions are read twice — just before sleep and again just before // the capture — so the sheet shows both ends of the run; only "did the // lid move DURING sleep" stays a maintainer field (unobservable from - // either end). Read AFTER the schedule step: a sudo prompt can sit for - // minutes, and a pre-prompt reading would be stale. + // either end). The only interactive prompt (sudo --validate) already + // ran, so this reading cannot go stale behind a prompt. NSString *lidPre = clamshellStateShort(); NSString *lockPre = sessionLockState(); NSString *powerPre = powerSource(); + + // Schedule the auto-wake LAST, immediately before sleepnow, so the lead + // is consumed only by falling asleep — cue speech, lock, and condition + // reads all happen before the clock starts. (The machine takes ~5-10 s + // to commit to sleep after sleepnow; a wake time that passes during + // that window is silently dropped, which is what made short effective + // leads unreliable.) wake=0 means the maintainer will wake the machine + // manually (e.g. by opening the lid). + if (wake > 0) { + NSDateFormatter *f = [[NSDateFormatter alloc] init]; + f.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + f.dateFormat = @"MM/dd/yy HH:mm:ss"; + NSString *when = + [f stringFromDate:[NSDate dateWithTimeIntervalSinceNow:wake]]; + printf("repro: scheduling wake at %s (sudo)\n", when.UTF8String); + // Non-interactive only when locked (nobody can answer a prompt); the + // interactive fallback remains for unlocked runs under policies that + // disable credential caching (timestamp_timeout=0), where the + // --validate priming cannot help. + int schedRC = + runStep(@"/usr/bin/sudo", + @[ @"--non-interactive", @"pmset", @"schedule", @"wake", when ], + dryRun); + if (schedRC != 0 && !dryRun && !lock) { + fprintf(stderr, "repro: sudo credentials not cached; prompting now " + "(pre-sleep)\n"); + schedRC = runStep(@"/usr/bin/sudo", + @[ @"pmset", @"schedule", @"wake", when ], NO); + } + if (schedRC != 0) { + fprintf(stderr, "repro: could not schedule wake (rc=%d); aborting\n", + schedRC); + return 1; + } + } + + // Register for the sleep/wake notifications BEFORE issuing sleepnow so + // neither can be missed. NSWorkspaceDidWakeNotification is the + // deterministic wake signal (fires however short the sleep was — a + // wall-clock tick-jump heuristic missed sub-5 s sleeps, the shakedown + // failure after run 85); the will-sleep observer feeds a 60 s + // did-not-sleep guard so a sleepnow that silently fails cannot hang the + // run. The wake side has no ceiling on purpose: a manual overnight wake + // is a legitimate run shape. + __block BOOL sleepSeen = NO, wakeSeen = NO; + NSNotificationCenter *wsCenter = + NSWorkspace.sharedWorkspace.notificationCenter; + id willSleepObs = + [wsCenter addObserverForName:NSWorkspaceWillSleepNotification + object:nil + queue:nil + usingBlock:^(NSNotification *__unused note) { + sleepSeen = YES; + }]; + id didWakeObs = + [wsCenter addObserverForName:NSWorkspaceDidWakeNotification + object:nil + queue:nil + usingBlock:^(NSNotification *__unused note) { + wakeSeen = YES; + }]; sayCue(@"sleeping now", silent, dryRun); - if (runStep(@"/usr/bin/pmset", @[ @"sleepnow" ], dryRun) != 0) + int sleepRC = runStep(@"/usr/bin/pmset", @[ @"sleepnow" ], dryRun); + if (sleepRC != 0) { + [wsCenter removeObserver:willSleepObs]; + [wsCenter removeObserver:didWakeObs]; return 1; - // System sleeps here. Detect the wake instant rather than sleeping a - // fixed interval: 1 s thread sleeps do not run while the system is - // asleep, so a tick whose wall-clock duration jumps past 5 s means the - // machine slept across it and is now awake. Works for scheduled and - // manual wakes alike (a manual overnight wake just loops longer), and - // anchors the settle countdown at the wake itself, so the capture is a - // consistent `settle` seconds post-wake regardless of how long the - // machine slept. + } + // System sleeps here. Run the run loop in 1 s slices (delivering the + // observers' notifications) until the wake arrives. The tick-jump check + // stays as a fallback in case notification delivery ever fails in a + // CLI process: a slice that spans > 5 s of wall clock straddled a + // sleep. if (!dryRun) { + NSDate *sleepCmdAt = NSDate.date; for (;;) { NSDate *tick = NSDate.date; - [NSThread sleepForTimeInterval:1.0]; - if (-[tick timeIntervalSinceNow] > 5.0) + [[NSRunLoop currentRunLoop] + runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + if (wakeSeen || -[tick timeIntervalSinceNow] > 5.0) break; + if (!sleepSeen && -[sleepCmdAt timeIntervalSinceNow] > 60.0) { + fprintf(stderr, + "repro: no sleep observed within 60 s of sleepnow (or the " + "sleep notification was missed); aborting\n"); + [wsCenter removeObserver:willSleepObs]; + [wsCenter removeObserver:didWakeObs]; + return 1; + } } } - // First cue after wake — as close to the wake instant as the tick - // granularity allows (~1 s), so the eyewitness can anchor the - // "immediate" observation to it while the panels first light. + [wsCenter removeObserver:willSleepObs]; + [wsCenter removeObserver:didWakeObs]; + // First cue after wake — within ~1 s of the wake instant, so the + // eyewitness can anchor the "immediate" observation to it while the + // panels first light. The settle countdown starts here, so the capture + // is a consistent `settle` seconds post-wake regardless of how long the + // machine slept. sayCue(@"awake", silent, dryRun); if (!dryRun) [NSThread sleepForTimeInterval:settle]; From e07d882ce3a39abe691b6e4ea4ddceaefe016eea Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Sun, 19 Jul 2026 23:27:37 -0400 Subject: [PATCH 13/24] docs: record shakedown failure and wake rework 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 --- docs/technical-debt.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/technical-debt.md b/docs/technical-debt.md index cb3e0df..bfacc42 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -1924,6 +1924,28 @@ extension tables). 81. **displaysleep remains the recovery of record** (works locked and unlocked); any future automatic wiring must either gate extcycle on an unlocked session or just use displaysleep. +- *Shakedown after run 85 (23:01, unnumbered — Ctrl-C'd, no sheet): + the tick-loop wake detector was defective and is replaced.* On the + reordered build the trigger again produced the black (E2/B1 → + E1/B1 → E1/B0, held indefinitely; hot corner recovered), but repro + went silent: no "awake" cue, no capture. Root cause: the loop + detected a wake only when one 1 s tick spanned > 5 s of wall + clock, and with the wake lead consumed by fall-asleep time the + actual sleep span can be under 5 s — the scheduled wake most + likely fired (the panels lit unprompted) and the detector missed + it, spinning forever (it had no ceiling). Reworked same night: + repro now registers `NSWorkspaceWillSleepNotification` / + `NSWorkspaceDidWakeNotification` observers before `sleepnow` and + waits on the did-wake notification in a run loop (deterministic + for arbitrarily short sleeps), keeps the tick-jump check only as + a fallback, aborts if no will-sleep arrives within 60 s of + `sleepnow` (a sleep that never happens must error, not hang; the + wake side deliberately has no ceiling — overnight manual wakes + are legitimate), and schedules the wake immediately before + `sleepnow` so lock/conditions/cue time no longer shaves the + sleep span. For the record: the 1 s ticks cannot cause or prevent + a wake — sleeping threads hold no power assertions and userspace + timers do not program RTC wakes. - *Attribution (maintainer asked: OS, cable, display, or interfering software?).* The evidence points at the **macOS display stack (SkyLight/DCP wake path), host-side**: the soft trigger reproduces From b8654bed03c5b70c76759469db8f4a3b7edaf747 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 01:09:00 -0400 Subject: [PATCH 14/24] docs: record run sheets 86-104 (MCL controls) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../run086-grp-b-20260719-233431.md | 52 +++++++++++++++++++ .../run087-grp-b-20260719-235246.md | 52 +++++++++++++++++++ .../run088-grp-b-20260719-235545.md | 52 +++++++++++++++++++ .../run089-grp-b-20260719-235755.md | 52 +++++++++++++++++++ .../run090-grp-b-20260720-000202.md | 52 +++++++++++++++++++ .../run091-grp-b-20260720-000746.md | 52 +++++++++++++++++++ .../run092-grp-b-20260720-001020.md | 52 +++++++++++++++++++ .../run093-grp-b-20260720-001334.md | 52 +++++++++++++++++++ .../run094-grp-b-20260720-001600.md | 52 +++++++++++++++++++ .../run095-grp-b-20260720-002033.md | 52 +++++++++++++++++++ .../run096-grp-b-20260720-002246.md | 52 +++++++++++++++++++ .../run097-grp-b-20260720-002550.md | 52 +++++++++++++++++++ .../run098-grp-c-20260720-002935.md | 52 +++++++++++++++++++ .../run099-grp-c-20260720-003235.md | 52 +++++++++++++++++++ .../run100-grp-c-20260720-003513.md | 52 +++++++++++++++++++ .../run101-grp-c-20260720-003736.md | 52 +++++++++++++++++++ .../run102-grp-c-20260720-004031.md | 52 +++++++++++++++++++ .../run103-grp-e-20260720-004529.md | 52 +++++++++++++++++++ .../run104-grp-e-20260720-004902.md | 52 +++++++++++++++++++ 19 files changed, 988 insertions(+) create mode 100644 docs/debug/repro-matrix/run086-grp-b-20260719-233431.md create mode 100644 docs/debug/repro-matrix/run087-grp-b-20260719-235246.md create mode 100644 docs/debug/repro-matrix/run088-grp-b-20260719-235545.md create mode 100644 docs/debug/repro-matrix/run089-grp-b-20260719-235755.md create mode 100644 docs/debug/repro-matrix/run090-grp-b-20260720-000202.md create mode 100644 docs/debug/repro-matrix/run091-grp-b-20260720-000746.md create mode 100644 docs/debug/repro-matrix/run092-grp-b-20260720-001020.md create mode 100644 docs/debug/repro-matrix/run093-grp-b-20260720-001334.md create mode 100644 docs/debug/repro-matrix/run094-grp-b-20260720-001600.md create mode 100644 docs/debug/repro-matrix/run095-grp-b-20260720-002033.md create mode 100644 docs/debug/repro-matrix/run096-grp-b-20260720-002246.md create mode 100644 docs/debug/repro-matrix/run097-grp-b-20260720-002550.md create mode 100644 docs/debug/repro-matrix/run098-grp-c-20260720-002935.md create mode 100644 docs/debug/repro-matrix/run099-grp-c-20260720-003235.md create mode 100644 docs/debug/repro-matrix/run100-grp-c-20260720-003513.md create mode 100644 docs/debug/repro-matrix/run101-grp-c-20260720-003736.md create mode 100644 docs/debug/repro-matrix/run102-grp-c-20260720-004031.md create mode 100644 docs/debug/repro-matrix/run103-grp-e-20260720-004529.md create mode 100644 docs/debug/repro-matrix/run104-grp-e-20260720-004902.md diff --git a/docs/debug/repro-matrix/run086-grp-b-20260719-233431.md b/docs/debug/repro-matrix/run086-grp-b-20260719-233431.md new file mode 100644 index 0000000..89096fe --- /dev/null +++ b/docs/debug/repro-matrix/run086-grp-b-20260719-233431.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-233431 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 86 started: 2026-07-19 23:34:31 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 23:35:09.810 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-233537 + post-recover = docs/debug/blackoutd-diag-20260719-233615 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run087-grp-b-20260719-235246.md b/docs/debug/repro-matrix/run087-grp-b-20260719-235246.md new file mode 100644 index 0000000..dc713d6 --- /dev/null +++ b/docs/debug/repro-matrix/run087-grp-b-20260719-235246.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-235246 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 87 started: 2026-07-19 23:52:46 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 23:53:23.686 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-235346 + post-recover = docs/debug/blackoutd-diag-20260719-235424 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On AC power, did not hear the "Awake" announcement. +``` diff --git a/docs/debug/repro-matrix/run088-grp-b-20260719-235545.md b/docs/debug/repro-matrix/run088-grp-b-20260719-235545.md new file mode 100644 index 0000000..6fae406 --- /dev/null +++ b/docs/debug/repro-matrix/run088-grp-b-20260719-235545.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-235545 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 88 started: 2026-07-19 23:55:45 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 23:56:20.805 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-235645 + post-recover = docs/debug/blackoutd-diag-20260719-235723 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run089-grp-b-20260719-235755.md b/docs/debug/repro-matrix/run089-grp-b-20260719-235755.md new file mode 100644 index 0000000..adf814f --- /dev/null +++ b/docs/debug/repro-matrix/run089-grp-b-20260719-235755.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260719-235755 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 89 started: 2026-07-19 23:57:55 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-19 23:58:29.720 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260719-235855 + post-recover = docs/debug/blackoutd-diag-20260719-235933 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Wake: Very brief E2/B1 -> E1/B1 -> E1/B0. On AC power. +``` diff --git a/docs/debug/repro-matrix/run090-grp-b-20260720-000202.md b/docs/debug/repro-matrix/run090-grp-b-20260720-000202.md new file mode 100644 index 0000000..89b1d53 --- /dev/null +++ b/docs/debug/repro-matrix/run090-grp-b-20260720-000202.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-000202 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 90 started: 2026-07-20 00:02:02 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:02:36.536 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-000301 + post-recover = docs/debug/blackoutd-diag-20260720-000339 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On AC power, MonitorControlLite not running. +``` diff --git a/docs/debug/repro-matrix/run091-grp-b-20260720-000746.md b/docs/debug/repro-matrix/run091-grp-b-20260720-000746.md new file mode 100644 index 0000000..f69c76e --- /dev/null +++ b/docs/debug/repro-matrix/run091-grp-b-20260720-000746.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-000746 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 91 started: 2026-07-20 00:07:46 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:08:23.489 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-000848 + post-recover = docs/debug/blackoutd-diag-20260720-000926 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On AP power, MCL not running. +``` diff --git a/docs/debug/repro-matrix/run092-grp-b-20260720-001020.md b/docs/debug/repro-matrix/run092-grp-b-20260720-001020.md new file mode 100644 index 0000000..3de0d8d --- /dev/null +++ b/docs/debug/repro-matrix/run092-grp-b-20260720-001020.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-001020 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 92 started: 2026-07-20 00:10:20 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:10:56.427 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-001120 + post-recover = docs/debug/blackoutd-diag-20260720-001158 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Did not hear the "Awake" cue. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run093-grp-b-20260720-001334.md b/docs/debug/repro-matrix/run093-grp-b-20260720-001334.md new file mode 100644 index 0000000..0a596f3 --- /dev/null +++ b/docs/debug/repro-matrix/run093-grp-b-20260720-001334.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-001334 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 93 started: 2026-07-20 00:13:34 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:14:08.892 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-001433 + post-recover = docs/debug/blackoutd-diag-20260720-001512 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Did not hear the awake cue. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run094-grp-b-20260720-001600.md b/docs/debug/repro-matrix/run094-grp-b-20260720-001600.md new file mode 100644 index 0000000..56c6e29 --- /dev/null +++ b/docs/debug/repro-matrix/run094-grp-b-20260720-001600.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-001600 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 94 started: 2026-07-20 00:16:00 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:16:36.162 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-001700 + post-recover = docs/debug/blackoutd-diag-20260720-001739 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Awake cue not heard. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run095-grp-b-20260720-002033.md b/docs/debug/repro-matrix/run095-grp-b-20260720-002033.md new file mode 100644 index 0000000..d467859 --- /dev/null +++ b/docs/debug/repro-matrix/run095-grp-b-20260720-002033.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-002033 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 95 started: 2026-07-20 00:20:33 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:21:08.506 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-002132 + post-recover = docs/debug/blackoutd-diag-20260720-002211 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Awake cue not heard. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run096-grp-b-20260720-002246.md b/docs/debug/repro-matrix/run096-grp-b-20260720-002246.md new file mode 100644 index 0000000..2efcdd8 --- /dev/null +++ b/docs/debug/repro-matrix/run096-grp-b-20260720-002246.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-002246 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 96 started: 2026-07-20 00:22:46 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:23:22.300 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-002349 + post-recover = docs/debug/blackoutd-diag-20260720-002428 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On AC power. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run097-grp-b-20260720-002550.md b/docs/debug/repro-matrix/run097-grp-b-20260720-002550.md new file mode 100644 index 0000000..da08125 --- /dev/null +++ b/docs/debug/repro-matrix/run097-grp-b-20260720-002550.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-002550 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 97 started: 2026-07-20 00:25:50 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. B + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:26:26.458 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-002650 + post-recover = docs/debug/blackoutd-diag-20260720-002729 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On AC power. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run098-grp-c-20260720-002935.md b/docs/debug/repro-matrix/run098-grp-c-20260720-002935.md new file mode 100644 index 0000000..d52dbb4 --- /dev/null +++ b/docs/debug/repro-matrix/run098-grp-c-20260720-002935.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-002935 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 98 started: 2026-07-20 00:29:35 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:30:19.955 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-003045 + post-recover = docs/debug/blackoutd-diag-20260720-003124 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On AC power. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run099-grp-c-20260720-003235.md b/docs/debug/repro-matrix/run099-grp-c-20260720-003235.md new file mode 100644 index 0000000..62373ee --- /dev/null +++ b/docs/debug/repro-matrix/run099-grp-c-20260720-003235.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-003235 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 99 started: 2026-07-20 00:32:35 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:33:19.447 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-003345 + post-recover = docs/debug/blackoutd-diag-20260720-003424 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On AC power. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run100-grp-c-20260720-003513.md b/docs/debug/repro-matrix/run100-grp-c-20260720-003513.md new file mode 100644 index 0000000..722d7b6 --- /dev/null +++ b/docs/debug/repro-matrix/run100-grp-c-20260720-003513.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-003513 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 100 started: 2026-07-20 00:35:13 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:35:55.528 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-003619 + post-recover = docs/debug/blackoutd-diag-20260720-003659 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Awake cue not heard. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run101-grp-c-20260720-003736.md b/docs/debug/repro-matrix/run101-grp-c-20260720-003736.md new file mode 100644 index 0000000..770b62a --- /dev/null +++ b/docs/debug/repro-matrix/run101-grp-c-20260720-003736.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-003736 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 101 started: 2026-07-20 00:37:36 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:38:18.824 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-003844 + post-recover = docs/debug/blackoutd-diag-20260720-003923 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Awake cue heard. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run102-grp-c-20260720-004031.md b/docs/debug/repro-matrix/run102-grp-c-20260720-004031.md new file mode 100644 index 0000000..2548b15 --- /dev/null +++ b/docs/debug/repro-matrix/run102-grp-c-20260720-004031.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-004031 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 102 started: 2026-07-20 00:40:31 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. C + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ locked -> locked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:41:13.871 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-004140 + post-recover = docs/debug/blackoutd-diag-20260720-004219 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [x] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Awake cue heard. MCL not running. +``` diff --git a/docs/debug/repro-matrix/run103-grp-e-20260720-004529.md b/docs/debug/repro-matrix/run103-grp-e-20260720-004529.md new file mode 100644 index 0000000..d4d81fa --- /dev/null +++ b/docs/debug/repro-matrix/run103-grp-e-20260720-004529.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-004529 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 103 started: 2026-07-20 00:45:29 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. E + lid .................... open -> open + moved during sleep? ... [ ] no [x] yes: closed + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:46:05.164 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-004631 + post-recover = docs/debug/blackoutd-diag-20260720-004711 + +@ wake (immediate — first look as the panels light): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [ ] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: On AC power. MCL not running. Awake cue heard. +``` diff --git a/docs/debug/repro-matrix/run104-grp-e-20260720-004902.md b/docs/debug/repro-matrix/run104-grp-e-20260720-004902.md new file mode 100644 index 0000000..84d4022 --- /dev/null +++ b/docs/debug/repro-matrix/run104-grp-e-20260720-004902.md @@ -0,0 +1,52 @@ + + +# repro run sheet — 20260720-004902 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 104 started: 2026-07-20 00:49:02 +Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. E + lid .................... open -> open + moved during sleep? ... [ ] no [x] yes: opened + session ................ unlocked -> unlocked + power .................. battery -> battery + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... displaysleep + settle ................. 20 s + daemon settled ......... 2026-07-20 00:50:11.325 blackoutd[26174:2796319] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-005039 + post-recover = docs/debug/blackoutd-diag-20260720-005119 + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-recover capture: + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + recovery cleared the external? ... [x] yes [ ] no [ ] partial + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: Scheduled wake did not occur with clamshell closed on battery power. MCL not running. I closed the lid during sleep, waited, and only after I reopened it did the external display wake (to E1). Awake cue heard. +``` From ab5168a2716efe2925f28281ea9bc854fb5fd924 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 01:12:43 -0400 Subject: [PATCH 15/24] docs: relabel AC runs 87-97 group B to D MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...grp-b-20260719-235246.md => run087-grp-d-20260719-235246.md} | 2 +- ...grp-b-20260719-235755.md => run089-grp-d-20260719-235755.md} | 2 +- ...grp-b-20260720-000202.md => run090-grp-d-20260720-000202.md} | 2 +- ...grp-b-20260720-000746.md => run091-grp-d-20260720-000746.md} | 2 +- ...grp-b-20260720-002246.md => run096-grp-d-20260720-002246.md} | 2 +- ...grp-b-20260720-002550.md => run097-grp-d-20260720-002550.md} | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename docs/debug/repro-matrix/{run087-grp-b-20260719-235246.md => run087-grp-d-20260719-235246.md} (98%) rename docs/debug/repro-matrix/{run089-grp-b-20260719-235755.md => run089-grp-d-20260719-235755.md} (98%) rename docs/debug/repro-matrix/{run090-grp-b-20260720-000202.md => run090-grp-d-20260720-000202.md} (98%) rename docs/debug/repro-matrix/{run091-grp-b-20260720-000746.md => run091-grp-d-20260720-000746.md} (98%) rename docs/debug/repro-matrix/{run096-grp-b-20260720-002246.md => run096-grp-d-20260720-002246.md} (98%) rename docs/debug/repro-matrix/{run097-grp-b-20260720-002550.md => run097-grp-d-20260720-002550.md} (98%) diff --git a/docs/debug/repro-matrix/run087-grp-b-20260719-235246.md b/docs/debug/repro-matrix/run087-grp-d-20260719-235246.md similarity index 98% rename from docs/debug/repro-matrix/run087-grp-b-20260719-235246.md rename to docs/debug/repro-matrix/run087-grp-d-20260719-235246.md index dc713d6..c9487b1 100644 --- a/docs/debug/repro-matrix/run087-grp-b-20260719-235246.md +++ b/docs/debug/repro-matrix/run087-grp-d-20260719-235246.md @@ -16,7 +16,7 @@ Run #: 87 started: 2026-07-19 23:52:46 Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) Conditions (pre-sleep -> at capture): - group .................. B + group .................. D lid .................... open -> open moved during sleep? ... [x] no [ ] yes: ______ session ................ unlocked -> unlocked diff --git a/docs/debug/repro-matrix/run089-grp-b-20260719-235755.md b/docs/debug/repro-matrix/run089-grp-d-20260719-235755.md similarity index 98% rename from docs/debug/repro-matrix/run089-grp-b-20260719-235755.md rename to docs/debug/repro-matrix/run089-grp-d-20260719-235755.md index adf814f..5b9da60 100644 --- a/docs/debug/repro-matrix/run089-grp-b-20260719-235755.md +++ b/docs/debug/repro-matrix/run089-grp-d-20260719-235755.md @@ -16,7 +16,7 @@ Run #: 89 started: 2026-07-19 23:57:55 Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) Conditions (pre-sleep -> at capture): - group .................. B + group .................. D lid .................... open -> open moved during sleep? ... [x] no [ ] yes: ______ session ................ unlocked -> unlocked diff --git a/docs/debug/repro-matrix/run090-grp-b-20260720-000202.md b/docs/debug/repro-matrix/run090-grp-d-20260720-000202.md similarity index 98% rename from docs/debug/repro-matrix/run090-grp-b-20260720-000202.md rename to docs/debug/repro-matrix/run090-grp-d-20260720-000202.md index 89b1d53..faa85c6 100644 --- a/docs/debug/repro-matrix/run090-grp-b-20260720-000202.md +++ b/docs/debug/repro-matrix/run090-grp-d-20260720-000202.md @@ -16,7 +16,7 @@ Run #: 90 started: 2026-07-20 00:02:02 Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) Conditions (pre-sleep -> at capture): - group .................. B + group .................. D lid .................... open -> open moved during sleep? ... [x] no [ ] yes: ______ session ................ unlocked -> unlocked diff --git a/docs/debug/repro-matrix/run091-grp-b-20260720-000746.md b/docs/debug/repro-matrix/run091-grp-d-20260720-000746.md similarity index 98% rename from docs/debug/repro-matrix/run091-grp-b-20260720-000746.md rename to docs/debug/repro-matrix/run091-grp-d-20260720-000746.md index f69c76e..b641305 100644 --- a/docs/debug/repro-matrix/run091-grp-b-20260720-000746.md +++ b/docs/debug/repro-matrix/run091-grp-d-20260720-000746.md @@ -16,7 +16,7 @@ Run #: 91 started: 2026-07-20 00:07:46 Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) Conditions (pre-sleep -> at capture): - group .................. B + group .................. D lid .................... open -> open moved during sleep? ... [x] no [ ] yes: ______ session ................ unlocked -> unlocked diff --git a/docs/debug/repro-matrix/run096-grp-b-20260720-002246.md b/docs/debug/repro-matrix/run096-grp-d-20260720-002246.md similarity index 98% rename from docs/debug/repro-matrix/run096-grp-b-20260720-002246.md rename to docs/debug/repro-matrix/run096-grp-d-20260720-002246.md index 2efcdd8..9659af7 100644 --- a/docs/debug/repro-matrix/run096-grp-b-20260720-002246.md +++ b/docs/debug/repro-matrix/run096-grp-d-20260720-002246.md @@ -16,7 +16,7 @@ Run #: 96 started: 2026-07-20 00:22:46 Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) Conditions (pre-sleep -> at capture): - group .................. B + group .................. D lid .................... open -> open moved during sleep? ... [x] no [ ] yes: ______ session ................ unlocked -> unlocked diff --git a/docs/debug/repro-matrix/run097-grp-b-20260720-002550.md b/docs/debug/repro-matrix/run097-grp-d-20260720-002550.md similarity index 98% rename from docs/debug/repro-matrix/run097-grp-b-20260720-002550.md rename to docs/debug/repro-matrix/run097-grp-d-20260720-002550.md index da08125..0396f32 100644 --- a/docs/debug/repro-matrix/run097-grp-b-20260720-002550.md +++ b/docs/debug/repro-matrix/run097-grp-d-20260720-002550.md @@ -16,7 +16,7 @@ Run #: 97 started: 2026-07-20 00:25:50 Build: v0.4.0-13-g2287185 built: 2026-07-20T03:34:23+00:00 (UTC) Conditions (pre-sleep -> at capture): - group .................. B + group .................. D lid .................... open -> open moved during sleep? ... [x] no [ ] yes: ______ session ................ unlocked -> unlocked From 902e4f7fb313f021654a39c525cf1d6a71eb62d5 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 01:29:23 -0400 Subject: [PATCH 16/24] feat: auto-recover cursor-on-black at wake settle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- src/AppDelegate.m | 8 ++- src/DisplayController.h | 9 +++ src/DisplayController.m | 142 ++++++++++++++++++++++++++++++++++++++++ src/main.m | 55 ++++++++++++++++ 4 files changed, 212 insertions(+), 2 deletions(-) diff --git a/src/AppDelegate.m b/src/AppDelegate.m index f44bba9..d6727de 100644 --- a/src/AppDelegate.m +++ b/src/AppDelegate.m @@ -78,8 +78,9 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification { _displayController.delegate = self; [self reloadPreferences]; - NSLog(@"[startup] — verbosityLevel=%ld", - (long)_displayController.verbosityLevel); + NSLog(@"[startup] — verbosityLevel=%ld recoveryStrategy=%@", + (long)_displayController.verbosityLevel, + _displayController.recoveryStrategy); [self setupMenuBar]; [self setupSignalHandlers]; [self setupSleepWakeObservers]; @@ -152,6 +153,9 @@ - (void)reloadPreferences { ? [_defaults integerForKey:@"verbosityLevel"] : 1; _displayController.verbosityLevel = verbosity; + NSString *recovery = + [_defaults stringForKey:@"recoveryStrategy"] ?: @"displaysleep"; + _displayController.recoveryStrategy = recovery; } // MARK: - Quitting diff --git a/src/DisplayController.h b/src/DisplayController.h index 0e00d4c..460479c 100644 --- a/src/DisplayController.h +++ b/src/DisplayController.h @@ -35,6 +35,15 @@ NS_ASSUME_NONNULL_BEGIN /// killall -HUP blackoutd @property(nonatomic, assign) NSInteger verbosityLevel; +/// Post-wake automatic recovery for the cursor-on-black failure +/// (technical-debt.md P20/P29). At wake-settle time, when the WindowServer +/// hotplug-coalescing marker is present in this wake's unified-log window, +/// the daemon runs the configured recovery. Values: "none" (disabled) or +/// "displaysleep" (display-sleep cycle; the default). Unknown values fall +/// back to "none". Set via `blackoutd recovery ` or +/// reload via SIGHUP like verbosityLevel. +@property(nonatomic, copy) NSString *recoveryStrategy; + /// Set to YES during system sleep. Display change events are still logged /// but state changes are not acted on until wake. @property(nonatomic, assign) BOOL systemSleeping; diff --git a/src/DisplayController.m b/src/DisplayController.m index f4fa33a..b6bb227 100644 --- a/src/DisplayController.m +++ b/src/DisplayController.m @@ -201,6 +201,12 @@ @implementation DisplayController { // invalidateDisplayState). See incidents 14:42 and 17:28 on 2026-04-29 // in docs/debug/. NSInteger _failedActionRetries; + // Cursor-on-black wake recovery (P20/P29): timestamp of the last system + // wake (bounds the marker query window) and a once-per-wake latch so + // settle-timer re-fires (err=1014 retries re-arm it) cannot run the + // recovery twice for one wake. + NSDate *_lastWakeAt; + BOOL _wakeRecoveryAttempted; } - (instancetype)init { @@ -238,6 +244,23 @@ - (void)setVerbosityLevel:(NSInteger)verbosityLevel { NSLog(@"[prefs] verbosityLevel=%ld", (long)verbosityLevel); } +- (void)setRecoveryStrategy:(NSString *)recoveryStrategy { + // Unknown values disable the recovery rather than guessing: a typo in the + // pref must not silently pick an action. + NSString *normalized = recoveryStrategy; + if (![normalized isEqualToString:@"none"] && + ![normalized isEqualToString:@"displaysleep"]) { + NSLog(@"[prefs] recoveryStrategy=%@ unknown (known: none, displaysleep); " + @"using none", + normalized); + normalized = @"none"; + } + BOOL changed = ![normalized isEqualToString:_recoveryStrategy]; + _recoveryStrategy = [normalized copy]; + if (changed) + NSLog(@"[prefs] recoveryStrategy=%@", normalized); +} + - (BOOL)isBlackedOut { return _isBlackedOut; } @@ -329,6 +352,8 @@ - (BOOL)invalidateDisplayState { // MARK: - Wake Settle Timer (P0 / P2) - (void)handleSystemWake { + _lastWakeAt = NSDate.date; + _wakeRecoveryAttempted = NO; [self resetWakeSettleTimer]; } @@ -406,6 +431,123 @@ - (void)wakeSettleTimerFired { @"blackout action"); [self applyEnable:NO]; } + if (hasExternal) + [self attemptCursorOnBlackRecovery]; +} + +// MARK: - Cursor-on-black wake recovery (P20 / P29) + +// The one host-visible signal that separates cursor-on-black wakes from +// clean ones: SkyLight coalescing a still-pending hotplug "out" with the +// wake's "in" for the external display. Validated 56/56 black vs 0/33 +// clean across matrix runs 1-104 (docs/debug/cursor-on-black-matrix.md). +// Private log wording — pinned to macOS 26 behavior; absence is treated as +// "clean or unknown", never as an error. +static NSString *const kBDHotplugCoalescedNeedle = + @"for state \"out\" with \"in\""; + +// Queries the unified log for the coalescing marker between `since` and +// now, via /usr/bin/log show — the same unprivileged path `diagnose` uses +// for windowserver.txt. The predicate matches the stable message prefix; +// the exact out/in direction is confirmed in-process so a future +// in-with-out sibling line cannot false-positive. Runs off the main queue +// (the query takes ~1 s). Returns NO on any spawn/read failure. +static BOOL BDWakeMarkerPresentSince(NSDate *since) { + NSDateFormatter *f = [[NSDateFormatter alloc] init]; + f.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + f.dateFormat = @"yyyy-MM-dd HH:mm:ss"; + NSTask *task = [[NSTask alloc] init]; + task.executableURL = [NSURL fileURLWithPath:@"/usr/bin/log"]; + NSString *predicate = + @"process == \"WindowServer\" AND eventMessage CONTAINS \"Replacing " + @"existing hotplug event\""; + task.arguments = @[ + @"show", @"--style", @"compact", @"--start", [f stringFromDate:since], + @"--predicate", predicate + ]; + NSPipe *pipe = [NSPipe pipe]; + task.standardOutput = pipe; + task.standardError = [NSFileHandle fileHandleWithNullDevice]; + NSError *err = nil; + if (![task launchAndReturnError:&err]) { + NSLog(@"[wake] recovery marker query failed to launch: %@", + err.localizedDescription); + return NO; + } + NSData *data = [pipe.fileHandleForReading readDataToEndOfFile]; + [task waitUntilExit]; + if (task.terminationStatus != 0) { + NSLog(@"[wake] recovery marker query exited rc=%d", + (int)task.terminationStatus); + return NO; + } + NSString *out = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + return [out containsString:kBDHotplugCoalescedNeedle]; +} + +// Runs one display-sleep cycle: pmset displaysleepnow (unprivileged), a 2 s +// pause, then caffeinate -u to re-declare user activity and relight the +// panels. The one recovery the matrix confirms end-to-end, locked and +// unlocked (runs 37-104: every programmatic clear). Called off the main +// queue. +static void BDRunDisplaySleepCycle(void) { + NSTask *sleepTask = [[NSTask alloc] init]; + sleepTask.executableURL = [NSURL fileURLWithPath:@"/usr/bin/pmset"]; + sleepTask.arguments = @[ @"displaysleepnow" ]; + NSError *err = nil; + if (![sleepTask launchAndReturnError:&err]) { + NSLog(@"[wake] recovery displaysleep failed to launch: %@", + err.localizedDescription); + return; + } + [sleepTask waitUntilExit]; + [NSThread sleepForTimeInterval:2.0]; + NSTask *wakeTask = [[NSTask alloc] init]; + wakeTask.executableURL = [NSURL fileURLWithPath:@"/usr/bin/caffeinate"]; + wakeTask.arguments = @[ @"-u", @"-t", @"2" ]; + if (![wakeTask launchAndReturnError:&err]) { + NSLog(@"[wake] recovery caffeinate failed to launch: %@", + err.localizedDescription); + return; + } + [wakeTask waitUntilExit]; + NSLog(@"[wake] recovery=displaysleep cycle complete (pmset rc=%d)", + (int)sleepTask.terminationStatus); +} + +// At settle time, decide whether this wake needs the recovery. Gated on the +// recoveryStrategy pref, once per wake, and on the marker being present in +// this wake's log window. The display-sleep cycle produces screen (not +// system) sleep notifications, so it cannot re-arm handleSystemWake and +// loop; the once-per-wake latch is belt and suspenders. +- (void)attemptCursorOnBlackRecovery { + if (![_recoveryStrategy isEqualToString:@"displaysleep"]) + return; + if (_wakeRecoveryAttempted || !_lastWakeAt) + return; + _wakeRecoveryAttempted = YES; + // Query from shortly before the wake: the marker lands at the wake + // instant, and the previous run's marker is minutes older, outside the + // window. + NSDate *since = [_lastWakeAt dateByAddingTimeInterval:-30.0]; + __weak typeof(self) weakSelf = self; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + BOOL present = BDWakeMarkerPresentSince(since); + if (!present) { + NSLog(@"[wake] recovery=displaysleep marker=absent — no action"); + return; + } + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf || strongSelf->_systemSleeping) { + NSLog(@"[wake] recovery=displaysleep marker=present but system " + @"sleeping — skipped"); + return; + } + NSLog(@"[wake] recovery=displaysleep marker=present — issuing " + @"display-sleep cycle"); + BDRunDisplaySleepCycle(); + }); } // MARK: - Private diff --git a/src/main.m b/src/main.m index ba1e48d..3a0446c 100644 --- a/src/main.m +++ b/src/main.m @@ -31,6 +31,7 @@ static NSString *const kSuiteName = @"blackoutd"; static NSString *const kAutoBlackoutKey = @"autoBlackoutOnExternalConnect"; static NSString *const kVerbosityKey = @"verbosityLevel"; +static NSString *const kRecoveryKey = @"recoveryStrategy"; static NSString *const kAgentLabel = @BD_BUNDLE_ID; static NSString *agentPlistPath(void) { @@ -575,6 +576,8 @@ static void appendConnectionModes(NSMutableString *r) { [r appendFormat:@"daemon log tail : %@\n", state]; [r appendFormat:@"auto-blackout : %s\n", autoMode ? "enabled" : "disabled"]; [r appendFormat:@"verbosity : %ld\n", (long)verbosity]; + [r appendFormat:@"recovery : %@\n", + [defaults stringForKey:kRecoveryKey] ?: @"displaysleep"]; [r appendFormat:@"bundle-id : %s\n", kBundleID.UTF8String]; [r appendFormat:@"lid : %@\n", clamshellState()]; [r appendFormat:@"session : %@\n", sessionLockState()]; @@ -834,6 +837,8 @@ static int printStatus(void) { builtInIsOnline() ? "active" : "blacked out"); printf(" auto-blackout : %s\n", autoMode ? "enabled" : "disabled"); printf(" verbosity : %ld\n", (long)verbosity); + printf(" recovery : %s\n", + ([defaults stringForKey:kRecoveryKey] ?: @"displaysleep").UTF8String); return pid > 0 ? 0 : 1; } @@ -851,6 +856,45 @@ static int setAutoBlackout(const char *value) { return sendSignalToDaemon(SIGHUP); } +// Sets the daemon's post-wake cursor-on-black recovery strategy, persisted +// in NSUserDefaults and applied live via SIGHUP — same pattern as +// setVerbosity below. "displaysleep" (the default) runs a display-sleep +// cycle at wake-settle when the WindowServer hotplug-coalescing marker is +// present (P20/P29); "none" disables it — required before matrix baseline +// data collection, which must observe the un-recovered wake. +static int setRecoveryStrategy(const char *value) { + static const char *kRecoveryUsage = + "Usage: blackoutd recovery \n"; + if (value == NULL || + (strcmp(value, "none") != 0 && strcmp(value, "displaysleep") != 0)) { + fprintf(stderr, "%s", kRecoveryUsage); + return 1; + } + NSUserDefaults *defaults = + [[NSUserDefaults alloc] initWithSuiteName:kSuiteName]; + [defaults setObject:@(value) forKey:kRecoveryKey]; + [defaults synchronize]; + NSString *applied = [defaults stringForKey:kRecoveryKey]; + pid_t pid = daemonPid(); + if (pid > 0) { + if (kill(pid, SIGHUP) != 0) { + if (errno == ESRCH) { + printf("recovery: %s (daemon exited before signal delivery; takes " + "effect on next start)\n", + applied.UTF8String); + return 0; + } + perror("blackoutd: kill SIGHUP"); + return 1; + } + printf("recovery: %s (daemon notified)\n", applied.UTF8String); + } else { + printf("recovery: %s (daemon not running; takes effect on next start)\n", + applied.UTF8String); + } + return 0; +} + // Sets the daemon's log verbosity level, persisted in NSUserDefaults so it // survives daemon restart, and signals the running daemon (if any) to // reload preferences immediately. Single-step replacement for the @@ -958,6 +1002,11 @@ static void printUsage(void) { " auto on|off Enable or disable auto-blackout on external connect\n" " verbosity Set daemon log verbosity (0=quiet, 1=normal," " 2=verbose)\n" + " recovery Set post-wake cursor-on-black auto-recovery:\n" + " displaysleep (default) runs a display-sleep cycle\n" + " when the wake carries the WindowServer marker;\n" + " none disables (required before repro baseline\n" + " data collection)\n" " diagnose Collect a diagnostic bundle for bug reports\n" " (auto-bounds the window to the last wake; override\n" " with --minutes N or --start \"T\" --end \"T\";\n" @@ -2178,6 +2227,12 @@ int main(int argc, const char *argv[]) { return 1; } return setVerbosity(argv[2]); + } else if (strcmp(cmd, "recovery") == 0) { + if (argc < 3) { + fprintf(stderr, "Usage: blackoutd recovery \n"); + return 1; + } + return setRecoveryStrategy(argv[2]); } // "daemon" with no subcommand falls through to daemon run loop below. if (strcmp(cmd, "daemon") != 0) { From 981032bb5e1851513eeb95be9cddd2a801728182 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 01:32:25 -0400 Subject: [PATCH 17/24] docs: record runs 86-104; auto-recovery notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 4 ++ docs/debug/cursor-on-black-matrix.md | 71 +++++++++++++++++++++++++++- docs/technical-debt.md | 47 +++++++++++++++++- 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 641fdf9..3530171 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,6 +129,10 @@ Keys: - `autoBlackoutOnExternalConnect` (BOOL, default YES) - `blackoutActive` (BOOL) — persisted blackout intent - `verbosityLevel` (Integer, default 1; 2 enables `[verbose=2]`-tagged log lines) +- `recoveryStrategy` (String, default `displaysleep`; `none` disables the + post-wake cursor-on-black auto-recovery — set via + `blackoutd recovery `; see P20/P29 and + `docs/debug/cursor-on-black-matrix.md`) ## Key technical details diff --git a/docs/debug/cursor-on-black-matrix.md b/docs/debug/cursor-on-black-matrix.md index 5fb2af9..0548648 100644 --- a/docs/debug/cursor-on-black-matrix.md +++ b/docs/debug/cursor-on-black-matrix.md @@ -50,6 +50,15 @@ interactive prompt at most, pre-sleep; builds before 2026-07-19 evening needed a manual `sudo --validate &&` prefix and a separate osascript lock command). +> **Daemon auto-recovery vs. data collection.** As of 2026-07-20 the +> daemon itself runs the displaysleep recovery at wake-settle when the +> WindowServer marker is present (`recoveryStrategy` pref, default +> `displaysleep`). **Before collecting matrix data, run +> `blackoutd recovery none`** — otherwise the daemon clears the black +> a few seconds after wake and the post-wake capture and eyewitness +> "settled" states record the recovered panel, not the failure. +> Restore with `blackoutd recovery displaysleep` afterward. + To emulate the C1 cable trigger in software (no cable handling), add `--trigger extcycle`: after scheduling the wake and before sleeping, `repro` disables the external display via CG, waits 5 s for the @@ -480,6 +489,63 @@ merely dark but absent from CGGetOnlineDisplayList. Run 85's sleep/wake re-onlined it. An extcycle failure mode can therefore drop the external below CG visibility entirely. +### Runs 086–104 — shakedowns, MCL controls, groups C/D/E + +All on the notification-based wake build, all `--trigger extcycle`, +all `--recover displaysleep`, no W1 (the scheduled wake fired on every +lid-open run). MonitorControlLite was quit for runs 90–104 (`MCL?` = +running/quit). Runs 87–97 on AC are group **D** (relabeled per the +maintainer); locked AC runs 98–99 stay group C (D is defined +unlocked) with AC on record here and in the sheets' power field. + +| run | sheet | grp | power | session | settled E/B | black? | cleared? | MCL? | +|-----|--------------------------------------------------------|-----|-------|----------|-------------|--------|----------|---------| +| 86 | [run086](repro-matrix/run086-grp-b-20260719-233431.md) | B | batt | unlocked | E1/B0 | yes | yes | running | +| 87 | [run087](repro-matrix/run087-grp-d-20260719-235246.md) | D | AC | unlocked | E1/B0 | yes | yes | running | +| 88 | [run088](repro-matrix/run088-grp-b-20260719-235545.md) | B | batt | unlocked | E1/B0 | yes | yes | running | +| 89 | [run089](repro-matrix/run089-grp-d-20260719-235755.md) | D | AC | unlocked | E1/B0 | yes | yes | running | +| 90 | [run090](repro-matrix/run090-grp-d-20260720-000202.md) | D | AC | unlocked | E1/B0 | yes | yes | quit | +| 91 | [run091](repro-matrix/run091-grp-d-20260720-000746.md) | D | AC | unlocked | E1/B0 | yes | yes | quit | +| 92 | [run092](repro-matrix/run092-grp-b-20260720-001020.md) | B | batt | unlocked | E1/B0 | yes | yes | quit | +| 93 | [run093](repro-matrix/run093-grp-b-20260720-001334.md) | B | batt | unlocked | E1/B0 | yes | yes | quit | +| 94 | [run094](repro-matrix/run094-grp-b-20260720-001600.md) | B | batt | unlocked | E1/B0 | yes | yes | quit | +| 95 | [run095](repro-matrix/run095-grp-b-20260720-002033.md) | B | batt | unlocked | E1/B0 | yes | yes | quit | +| 96 | [run096](repro-matrix/run096-grp-d-20260720-002246.md) | D | AC | unlocked | E1/B0 | yes | yes | quit | +| 97 | [run097](repro-matrix/run097-grp-d-20260720-002550.md) | D | AC | unlocked | E1/B0 | yes | yes | quit | +| 98 | [run098](repro-matrix/run098-grp-c-20260720-002935.md) | C | AC | locked | E1/B0 | yes | yes | quit | +| 99 | [run099](repro-matrix/run099-grp-c-20260720-003235.md) | C | AC | locked | E1/B0 | yes | yes | quit | +| 100 | [run100](repro-matrix/run100-grp-c-20260720-003513.md) | C | batt | locked | E1/B0 | yes | yes | quit | +| 101 | [run101](repro-matrix/run101-grp-c-20260720-003736.md) | C | batt | locked | E1/B0 | yes | yes | quit | +| 102 | [run102](repro-matrix/run102-grp-c-20260720-004031.md) | C | batt | locked | E0/B0 | no | n/a (R0) | quit | +| 103 | [run103](repro-matrix/run103-grp-e-20260720-004529.md) | E | AC | unlocked | E0/B0 | no | n/a (R0) | quit | +| 104 | [run104](repro-matrix/run104-grp-e-20260720-004902.md) | E | batt | unlocked | E1/B0 | yes | yes | quit | + +Verdicts from this block: + +- **MonitorControlLite is retired as a confound**: 15 runs with MCL + quit produced 13 blacks, indistinguishable from the MCL-running + runs. The host-side macOS attribution stands. +- **Locked displaysleep is n=5** (runs 60, 98, 99, 100, 101 — every + locked black cleared). Group C's question is answered: yes, + recovery works while locked. +- **Group D has its answer** (runs 87, 89, 90, 91, 96, 97 — six AC + blacks): the failure is not power-dependent, now at n=6 under the + scripted trigger (retiring the P29 "AC" open question for good). +- **Group E**: run 103 (AC, clamshell closed through the scheduled + wake) woke *clean* — the external, as the sole active display in + clamshell mode, came up rendering; the lid was reopened after wake. + Run 104 (battery): the scheduled wake did not fire with the lid + closed (expected without AC), and the manual lid-open wake went + black (E1), cleared by displaysleep — the lived scenario reproduced + and recovered. +- The **"awake" cue is intermittently inaudible** (heard: 89, 101, + 102, 103, 104; not heard: 87, 92–95, 100 — both power sources on + both lists). Wake detection is fine in every case (captures and + sheets landed on schedule); the loss is in post-wake audio-device + bring-up racing the `say` call. The Notification Center banner + carries the timestamp either way. Known quirk, not queued for a + fix. + **Detection result (2026-07-19)**: the field-by-field diff over this cohort found a WindowServer log marker that splits black from clean — 10/10 black wakes vs 0/15 clean across both cohorts, extended to @@ -488,8 +554,9 @@ run 55 and the locked runs), and to **39/39 vs 0/31** by the soft-trigger series 61–85 — where the two trigger-but-clean runs (71, 72) show the trigger's own pre-sleep hotplug pair does **not** plant the marker, and every marker timestamp sits at the wake, not at the -trigger. See `docs/technical-debt.md` P29 (2026-07-19 updates) for -the diff record, mechanism reading, and caveats. +trigger. Runs 86–104 bring the all-time tally to **56/56 black vs +0/33 clean**. See `docs/technical-debt.md` P29 (2026-07-19/20 +updates) for the diff record, mechanism reading, and caveats. ## What we do with the data diff --git a/docs/technical-debt.md b/docs/technical-debt.md index bfacc42..56fb521 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -796,7 +796,11 @@ to survive n ≥ 3: a WindowServer hotplug-coalescing log marker, 10/10 black wakes vs 0/15 clean across both cohorts, supporting hypothesis A (link drop during sleep, coalesced on wake). Full diff record, mechanism reading, caveats, and next steps under P29 (2026-07-19 -update). +update). **2026-07-20**: the marker stands at 56/56 vs 0/33 across +runs 1–104, and the detector-gated displaysleep auto-recovery is +implemented in the daemon (`recoveryStrategy` pref, `blackoutd +recovery` subcommand) — the acceptance-criteria recovery path for +hypothesis A/C is landed; see P29 (2026-07-20 update). **Architecture analysis confirming the gap**: @@ -1963,6 +1967,47 @@ extension tables). the confound is retired; Brisync was never running and is irrelevant. +**Update (2026-07-20) — confound retired; auto-recovery implemented.** +Runs 86–104 (matrix "shakedowns, MCL controls, groups C/D/E" table): + +- *Marker all-time tally: 56/56 black vs 0/33 clean* (17 more blacks, + 2 more cleans; every capture regime — cable, soft trigger, locked, + AC, lid). +- *MonitorControlLite retired as a confound*: 15 runs with MCL quit + produced 13 blacks, indistinguishable from before. Attribution + stands: a macOS SkyLight/DCP wake-path defect, host-side. +- *Recovery evidence complete*: locked displaysleep n=5 (runs 60, + 98–101); group D answered (six AC blacks — not power-dependent); + group E's lived scenario (run 104 lid-open wake) reproduced black + and recovered. Every programmatic displaysleep recovery across the + whole matrix has cleared its black. +- *Auto-recovery is now wired into the daemon.* At + `wakeSettleTimerFired`, when an external is present, the daemon + queries the unified log (spawning `log show` over a window opening + 30 s before the recorded wake — the same unprivileged path + `diagnose` uses) for the coalescing marker; if present it runs the + displaysleep cycle (`pmset displaysleepnow`, 2 s, `caffeinate -u`) + off the main queue. Gated by the new `recoveryStrategy` pref + (String, default `displaysleep`; `none` disables; unknown values + fall back to none), set via the new `blackoutd recovery + ` subcommand (SIGHUP reload, P23 pattern) and + shown in `status`/`config.txt`. Guards: once-per-wake latch, + `_systemSleeping` re-check, marker absence = no action; the + display-sleep cycle emits screen (not system) sleep notifications, + so it cannot re-arm the wake path and loop. **Matrix data + collection must now disable it first** (`blackoutd recovery none`) + or captures record the recovered panel — see the matrix how-to. +- *Flicker-free recovery and BetterDisplay RE remain open* (the + displaysleep cycle blinks both panels): candidates stay + `IOMobileFramebufferRequestPowerChange` (per-framebuffer, private) + and a BD `_reinitializeOnWake` RE pass — behind the now-standing + detector, as a follow-up PR. The extcycle recovery is ruled out for + automatic use (locked-session hazard, 2026-07-19 evening update). +- *Wake-cue audibility*: the "awake" `say` cue is intermittently + inaudible right after wake (audio-device bring-up race; both power + sources affected). Detection and captures are unaffected; the + Notification Center banner carries the timestamp. Known quirk. + **Files**: `src/main.m` (the `diagnose` `dcp.txt` / `connection-mode.txt` readers); investigation otherwise. Relates to P2, P20, P28, ADR 0003, ADR 0009 (SP2309W YCbCr), and the inject_edid `--mode` reader (of which From 2daa916d0783bcb3ff883afe2285da8c96905eb9 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 01:39:32 -0400 Subject: [PATCH 18/24] feat: add fbpower probe recover method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/main.m | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/src/main.m b/src/main.m index 3a0446c..d0029c0 100644 --- a/src/main.m +++ b/src/main.m @@ -11,6 +11,7 @@ #import #import #import +#import #import #import #import @@ -1021,8 +1022,10 @@ static void printUsage(void) { " Methods: displaysleep — display-sleep cycle (pmset\n" " displaysleepnow + caffeinate -u; the validated\n" " recovery); extcycle — disable/re-enable the external\n" - " display via CG (experimental, narrower; no flicker\n" - " on the built-in beyond the daemon's own restore)\n" + " display via CG (unlocked sessions ONLY — hazardous\n" + " while locked); fbpower — experimental private\n" + " IOMobileFramebuffer power-cycle probe (unverified\n" + " signatures; flicker-free candidate)\n" " repro Sleep/wake repro with labeled captures + recovery;\n" " emits a numbered, prefilled run sheet and copies\n" " bundles to docs/debug/ when run from the repo root\n" @@ -1209,6 +1212,60 @@ static int performExternalCycle(BOOL dryRun) { return 0; } +// Experimental flicker-free recovery candidate (P29): power-cycle only the +// external framebuffer via private IOMobileFramebuffer.framework calls, +// resolved with dlsym so the binary carries no link-time dependency. The +// signatures are reconstructed from public reverse-engineering headers and +// are UNVERIFIED on macOS 26 — a crash of this CLI invocation is a data +// point, not a regression. Even a refusal at open answers a standing +// question (whether a third-party process can reach the IOMFB user client +// at all — the same gate the HPD-notification detection idea depends on). +// CLI-only: the daemon's recoveryStrategy accepts only none|displaysleep. +static int performFramebufferPowerCycle(BOOL dryRun) { + if (dryRun) { + printf(" [dry-run] fbpower: open external-0 via IOMobileFramebuffer, " + "power 0, wait 2 s, power 1, wait 5 s\n"); + return 0; + } + void *fw = dlopen("/System/Library/PrivateFrameworks/" + "IOMobileFramebuffer.framework/IOMobileFramebuffer", + RTLD_LAZY); + if (!fw) { + fprintf(stderr, "blackoutd: fbpower: dlopen failed: %s\n", dlerror()); + return 1; + } + typedef int (*BDIOMFBOpenByNameFn)(CFStringRef, void **); + typedef int (*BDIOMFBRequestPowerChangeFn)(void *, uint32_t); + BDIOMFBOpenByNameFn openByName = + (BDIOMFBOpenByNameFn)dlsym(fw, "IOMobileFramebufferOpenByName"); + BDIOMFBRequestPowerChangeFn requestPower = (BDIOMFBRequestPowerChangeFn)dlsym( + fw, "IOMobileFramebufferRequestPowerChange"); + if (!openByName || !requestPower) { + fprintf(stderr, + "blackoutd: fbpower: symbol lookup failed (OpenByName=%d " + "RequestPowerChange=%d)\n", + openByName != NULL, requestPower != NULL); + return 1; + } + void *fb = NULL; + // "external-0" is the framebuffer name WindowServer's own open logs show + // for the external display on this machine. + int rc = openByName(CFSTR("external-0"), &fb); + printf("fbpower: OpenByName(external-0) rc=0x%x fb=%p\n", rc, fb); + if (rc != 0 || fb == NULL) + return 1; + // Power values mirror IOMFB's own update_power_state logging (0 = off, + // 1 = on); whether this API level uses the same scale is part of what + // the probe answers. + rc = requestPower(fb, 0); + printf("fbpower: RequestPowerChange(0) rc=0x%x\n", rc); + [NSThread sleepForTimeInterval:2.0]; + int rc2 = requestPower(fb, 1); + printf("fbpower: RequestPowerChange(1) rc=0x%x\n", rc2); + [NSThread sleepForTimeInterval:5.0]; + return (rc == 0 && rc2 == 0) ? 0 : 1; +} + // Performs one recovery attempt. Methods: // displaysleep — programmatic display-sleep cycle (pmset displaysleepnow, // then caffeinate -u to re-declare user activity and wake @@ -1217,15 +1274,20 @@ static int performExternalCycle(BOOL dryRun) { // the matrix confirms it reliably recovers regardless of // lock state (10/10 through run 60). // extcycle — external display disable/re-enable cycle (see -// performExternalCycle above). Experimental: candidate -// narrower recovery, untested against a black as of run 60. +// performExternalCycle above). Clears blacks unlocked +// (10/10, runs 69-80) but is hazardous while locked +// (runs 81-85) — never wire it to automatic use. +// fbpower — experimental IOMobileFramebuffer probe (see +// performFramebufferPowerCycle above). static int performRecovery(NSString *method, BOOL dryRun) { if ([method isEqualToString:@"extcycle"]) return performExternalCycle(dryRun); + if ([method isEqualToString:@"fbpower"]) + return performFramebufferPowerCycle(dryRun); if (![method isEqualToString:@"displaysleep"]) { fprintf(stderr, "blackoutd: unknown recovery method '%s' (known: displaysleep, " - "extcycle)\n", + "extcycle, fbpower)\n", method.UTF8String); return 1; } From 710314b9eb603b4c90a312a650c4f3aa68dada42 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 01:40:43 -0400 Subject: [PATCH 19/24] docs: add ADR 0010; architecture recovery note 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 --- AGENTS.md | 6 +- docs/architecture.md | 12 ++ ...-cursor-on-black-detection-and-recovery.md | 138 ++++++++++++++++++ docs/technical-debt.md | 6 + 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 docs/decisions/0010-cursor-on-black-detection-and-recovery.md diff --git a/AGENTS.md b/AGENTS.md index 3530171..2c31b11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -345,8 +345,10 @@ For non-trivial work, an agent should also read: 1. `docs/technical-debt.md` — current priorities and known issues 2. `ROADMAP.md` — milestone plan (cross-references the P-numbers above) -3. `docs/decisions/` — accepted ADRs (4 today: Trojan Source, daemon - presence, wake-settle timer, merge strategy) +3. `docs/decisions/` — accepted ADRs (9 today, numbered through 0010; + 0009 is cited in docs but not yet written. For display work start + with 0003 wake-settle timer and 0010 cursor-on-black + detection/recovery) 4. `CONTRIBUTING.md` — encoding policy, REUSE/SPDX conventions, commit and PR conventions diff --git a/docs/architecture.md b/docs/architecture.md index ada66de..efd73a9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -110,6 +110,18 @@ fires reconfiguration callbacks as side effects. A 2-second window connect/disconnect events. A safety check at window close catches any real events that arrived during the window. +**Detector-gated wake recovery** — Some wakes leave the external display +black (cursor only) with every CG/WindowServer-visible field healthy — a +DCP/scanout stall below WindowServer. The one host-visible signal is a +WindowServer log line (SkyLight coalescing a pending hotplug "out" with +the wake's "in"; 56/56 black vs 0/33 clean across the empirical matrix). +At wake-settle, with an external present, the daemon queries the unified +log for that marker and, when present, runs a display-sleep cycle +(`pmset displaysleepnow` + `caffeinate -u`) — the recovery validated +locked and unlocked. Gated by the `recoveryStrategy` preference +(`blackoutd recovery `). See ADR 0010 and +technical-debt P20/P29. + ### Companion Project [displayrecommitd](https://github.com/toobuntu/displayrecommitd/) is a diff --git a/docs/decisions/0010-cursor-on-black-detection-and-recovery.md b/docs/decisions/0010-cursor-on-black-detection-and-recovery.md new file mode 100644 index 0000000..be5fb5c --- /dev/null +++ b/docs/decisions/0010-cursor-on-black-detection-and-recovery.md @@ -0,0 +1,138 @@ +--- +# SPDX-FileCopyrightText: Copyright 2026 Todd Schulman +# +# SPDX-License-Identifier: GPL-3.0-or-later + +number: 10 +title: Detector-gated displaysleep recovery for cursor-on-black +status: accepted +date: 2026-07-20 +decision-makers: + - toobuntu +--- + +# Detector-gated displaysleep recovery for cursor-on-black + +## Context and Problem Statement + +The cursor-on-black failure (technical-debt P20/P29) leaves the external +display black with only the mouse cursor after a sleep/wake cycle, while +every CoreGraphics- and WindowServer-visible field reports a healthy, +fully configured display: the mode is set (`current == preferred`), the +framebuffer is found, `dcp.txt` / `connection-mode.txt` / `ioreg` state +is byte-identical to a clean wake. The failure is a DCP/scanout stall +below WindowServer. Automatic recovery therefore needs two things +blackoutd did not have: a **detection signal** (so recovery does not +flicker every healthy wake) and a **recovery action** stronger than the +ADR-0003 no-op recommit (empirically insufficient, early and late). + +The empirical matrix (`docs/debug/cursor-on-black-matrix.md`, runs +1–104) settled both: + +- **Detection.** One WindowServer log line separates black wakes from + clean ones: `[ Display:Hotplug ] Replacing existing hotplug event for + state "out" with "in", display id: 2` — SkyLight coalescing a + still-pending hotplug "out" with the wake's "in". Tally: present in + the wake window of **56/56** eyewitness-confirmed black wakes, absent + from **33/33** clean ones, across cable-triggered, software-triggered + (`repro --trigger extcycle`), natural, locked, AC, and lid-open + regimes. Control runs show the pre-sleep trigger itself never plants + the marker; every marker timestamp sits at the wake instant. +- **Recovery.** A display-sleep cycle (`pmset displaysleepnow`, then + `caffeinate -u` to relight) cleared **every** programmatic recovery + attempt in the matrix, locked (n=5) and unlocked alike, plus all + manual hot-corner clears. It needs no privileges. + +## Decision + +At `wakeSettleTimerFired` (the ADR-0003 quiet point), when an external +display is present, the daemon: + +1. queries the unified log for the coalescing marker over a window + opening 30 s before the recorded wake, by spawning + `/usr/bin/log show --predicate …` (the same unprivileged path + `diagnose` uses) off the main queue; +2. if — and only if — the marker is present, runs the display-sleep + cycle; +3. logs the verdict either way (`[wake] recovery=… marker=…`). + +The behavior is gated by a `recoveryStrategy` NSUserDefaults key +(String; default `displaysleep`, `none` disables, unknown values fall +back to `none`), settable live via `blackoutd recovery +` (SIGHUP reload, same pattern as `verbosity`). + +Guard rails: a once-per-wake latch (the settle timer re-fires on +err=1014 retries), a `_systemSleeping` re-check after the query, and +the fact that a display-sleep cycle emits screen — not system — sleep +notifications, so it cannot re-arm the wake path and loop. + +## Consequences + +* Good, because recovery is targeted: healthy wakes (marker absent) see + no flicker at all, and black wakes recover in seconds without user + action, including at the lock screen. +* Good, because both halves are validated at n = 56 + 33 rather than + inferred from a mechanism theory. +* Bad, because the detector is the *wording of a private SkyLight log + line*, pinned to macOS 26 behavior. A macOS update can silently break + detection. Mitigation: absence is always treated as clean-or-unknown + (fail quiet, never fail loud), the daemon logs the verdict every wake + so a broken detector is visible in the log, and the repro matrix + remains the revalidation loop after OS updates. +* Bad, because the recovery blinks both panels (display sleep is + global). Accepted for now; a flicker-free, external-only action is + the follow-up (candidates: `IOMobileFramebufferRequestPowerChange`, + BetterDisplay's `_reinitializeOnWake` approach — see P29). +* Bad, because matrix data collection now requires `blackoutd recovery + none` first, or captures record the recovered panel (documented in + the matrix how-to). + +## Considered Options and Alternatives + +### Reconfiguration-flag keying (`0x133e` flap vs `0x111e`) + +* Falsified 2026-05-26: a confirmed black wake reconnected at `0x111e`. + Not a classifier. + +### Below-CG state polling (`DCPPowerState`, `NormalModeActive`, ioreg) + +* Dead. Role-attributed reads are identical on black and clean wakes; + the cohort-2 diff found the normalized `dcpext` subtrees + byte-identical between black and clean captures. The stall is not + surfaced to ioreg. + +### Unconditional recovery on every wake-with-external + +* Rejected: flickers every healthy wake — the majority. The detector + exists precisely to avoid this. + +### `extcycle` (CG disable/re-enable of the external) as the recovery + +* Rejected for automatic use. It clears blacks reliably **unlocked** + (10/10) but while **locked** it fails and *induces* blacks — twice + turning a clean wake black, once leaving the external absent from + `CGGetOnlineDisplayList` until the next sleep/wake. Retained as a + CLI-only experimental method. + +### IOMobileFramebuffer HPD notifications as the detector + +* Deferred, not rejected: private but linkable + (`IOMobileFramebufferEnableHotPlugDetectNotifications`), and would + observe hotplug upstream of SkyLight's coalescing with no log-wording + dependency. Needs a validation series of its own before it can + replace the marker query. + +### OSLogStore instead of spawning `log show` + +* Deferred. In-process and structured, but its system-scope access + rights for a user LaunchAgent are unverified, while `log show` is + proven unprivileged on this machine by every diagnose bundle. Revisit + if the spawn ever becomes a problem (one ~1 s spawn per wake today). + +## More Information + +* `docs/debug/cursor-on-black-matrix.md` — the run-sheet matrix this + decision rests on (runs 1–104, tallies and per-run eyewitness data). +* technical-debt P20 (recovery gap, hypotheses) and P29 (mechanism, + falsifications, the 2026-07-19/20 update trail). +* ADR 0003 — the wake-settle quiet timer this recovery hangs off. diff --git a/docs/technical-debt.md b/docs/technical-debt.md index 56fb521..8630307 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -2003,6 +2003,12 @@ Runs 86–104 (matrix "shakedowns, MCL controls, groups C/D/E" table): and a BD `_reinitializeOnWake` RE pass — behind the now-standing detector, as a follow-up PR. The extcycle recovery is ruled out for automatic use (locked-session hazard, 2026-07-19 evening update). + A first probe is in the tree: `recover --method fbpower` (CLI-only, + dlsym-resolved, signatures unverified — a crash is a data point). + Smoke it against a soft-trigger black; even a refusal at + `IOMobileFramebufferOpenByName` is an answer, since the + HPD-notification detection idea rides on the same user-client + access. - *Wake-cue audibility*: the "awake" `say` cue is intermittently inaudible right after wake (audio-device bring-up race; both power sources affected). Detection and captures are unaffected; the From 172fe202a165165cc937664adf983116d33df35b Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 02:55:12 -0400 Subject: [PATCH 20/24] fix: address review of the wake-recovery query 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 --- src/DisplayController.m | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/DisplayController.m b/src/DisplayController.m index b6bb227..77e97d6 100644 --- a/src/DisplayController.m +++ b/src/DisplayController.m @@ -455,7 +455,12 @@ - (void)wakeSettleTimerFired { static BOOL BDWakeMarkerPresentSince(NSDate *since) { NSDateFormatter *f = [[NSDateFormatter alloc] init]; f.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; - f.dateFormat = @"yyyy-MM-dd HH:mm:ss"; + // Explicit zone and offset: log(1) documents "YYYY-MM-DD HH:MM:SSZZZZZ" + // as an accepted --start form, so emit the offset (Z pattern -> "-0400") + // rather than relying on the formatter's local-zone default matching + // log's local-zone interpretation of an unqualified timestamp. + f.timeZone = NSTimeZone.localTimeZone; + f.dateFormat = @"yyyy-MM-dd HH:mm:ssZ"; NSTask *task = [[NSTask alloc] init]; task.executableURL = [NSURL fileURLWithPath:@"/usr/bin/log"]; NSString *predicate = @@ -502,6 +507,11 @@ static void BDRunDisplaySleepCycle(void) { return; } [sleepTask waitUntilExit]; + if (sleepTask.terminationStatus != 0) { + NSLog(@"[wake] recovery displaysleep exited rc=%d — skipping wake nudge", + (int)sleepTask.terminationStatus); + return; + } [NSThread sleepForTimeInterval:2.0]; NSTask *wakeTask = [[NSTask alloc] init]; wakeTask.executableURL = [NSURL fileURLWithPath:@"/usr/bin/caffeinate"]; @@ -528,8 +538,11 @@ - (void)attemptCursorOnBlackRecovery { return; _wakeRecoveryAttempted = YES; // Query from shortly before the wake: the marker lands at the wake - // instant, and the previous run's marker is minutes older, outside the - // window. + // instant. 30 s comfortably covers the skew between the marker's log + // timestamp and _lastWakeAt (the NSWorkspace notification can trail the + // hardware wake by a few seconds) plus log-flush latency, while staying + // far inside the >= 2-3 min spacing of consecutive repro wakes — so the + // previous wake's marker can never leak into this window. NSDate *since = [_lastWakeAt dateByAddingTimeInterval:-30.0]; __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ From 4739f4df6f83c4c206adabba6d4b0da7489acc99 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 02:55:33 -0400 Subject: [PATCH 21/24] docs: write ADR 0009 (SP2309W color quirk) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 8 +- docs/decisions/0009-sp2309w-color-quirk.md | 135 +++++++++++++++++++++ 2 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 docs/decisions/0009-sp2309w-color-quirk.md diff --git a/AGENTS.md b/AGENTS.md index 2c31b11..52acb7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -345,10 +345,10 @@ For non-trivial work, an agent should also read: 1. `docs/technical-debt.md` — current priorities and known issues 2. `ROADMAP.md` — milestone plan (cross-references the P-numbers above) -3. `docs/decisions/` — accepted ADRs (9 today, numbered through 0010; - 0009 is cited in docs but not yet written. For display work start - with 0003 wake-settle timer and 0010 cursor-on-black - detection/recovery) +3. `docs/decisions/` — ADRs 0001–0010 (0009 is `proposed`, the rest + accepted). For display work start with 0003 wake-settle timer and + 0010 cursor-on-black detection/recovery; 0009 is the SP2309W color + quirk (repo-foundation has its own, unrelated 0009 on logging) 4. `CONTRIBUTING.md` — encoding policy, REUSE/SPDX conventions, commit and PR conventions diff --git a/docs/decisions/0009-sp2309w-color-quirk.md b/docs/decisions/0009-sp2309w-color-quirk.md new file mode 100644 index 0000000..ea7cb2c --- /dev/null +++ b/docs/decisions/0009-sp2309w-color-quirk.md @@ -0,0 +1,135 @@ +--- +# SPDX-FileCopyrightText: Copyright 2026 Todd Schulman +# +# SPDX-License-Identifier: GPL-3.0-or-later + +number: 9 +title: SP2309W color-cast correction as an opt-in quirk module +status: proposed +date: 2026-07-20 +decision-makers: + - toobuntu +--- + +# SP2309W color-cast correction as an opt-in quirk module + +> Recorded retroactively. The decision was sketched 2026-05-21/26 in +> technical-debt P26, the 2026-05-25 session handoff, and the +> `inject_edid` project's ADRs 0001–0002, all of which already cite +> "ADR 0009"; this document writes down what those sources agreed on. +> Status stays `proposed` until the module lands (or the standalone +> alternative is chosen instead) — P26's first acceptance criterion. + +## Context and Problem Statement + +The maintainer's Dell SP2309W (2008, 8-bit RGB TN, native 2048×1152) +ships a *defective* EDID: block 0 declares RGB, but the CTA-861 +extension (byte 131, bits 5–4) wrongly advertises YCbCr 4:4:4/4:2:2. +macOS trusts the extension and negotiates YCbCr — which the panel has +no decode path for — producing the pink cast (green when the OSD is +set to YPbPr). A 2010 production run shipped a corrected RGB-only +EDID; no firmware update exists for this unit, so correction is +host-side forever. + +The `inject_edid` project settled the mechanism and the host: + +- **ADR 0001 (inject_edid)** — runtime virtual-EDID injection via the + private `IOAVServiceSetVirtualEDIDMode`, patching only CEA byte 131 + (clear bits 5–4, fix the extension checksum). The filesystem + override mechanism is non-functional on Apple Silicon; DDC-driven + re-set is reactive and proprietary; a hardware EDID emulator fails + the no-spend constraint. +- **ADR 0002 (inject_edid)** — the correction is a non-resident + one-shot (the mapping survives process exit and display sleep, + reverts on full system sleep) fired from **blackoutd** on external + connect and after the wake-settle, because blackoutd is the single + actor on the reconfiguration callback and renegotiation is driven + by the same CG recommit blackoutd already owns; a second daemon + reintroduces the two-actor race behind the 2026-05-19 12:10 + incident. + +That leaves the blackoutd-side question this ADR answers: **how does a +general-purpose daemon carry a correction that applies to exactly one +monitor model, without burdening every other user with dead code and +extra private-API surface?** + +Orthogonality note (P29 insists): this is the *color-encoding* defect. +It is unrelated to cursor-on-black, which is the absence of scanout — +the two must not be entangled in code or triggers. + +## Decision + +Host the correction as an **opt-in, isolated quirk module**: + +* Its own translation unit, `src/quirks/DisplayColorController.{h,m}` + — a sibling to `DisplayController`, not code woven into it. +* Compiled **only** under `make QUIRKS=sp2309w` + (`-DBLACKOUTD_QUIRK_SP2309W`). A default `make` produces a binary + with zero quirk symbols and no reference to + `IOAVServiceSetVirtualEDIDMode`. +* Runtime-keyed to vendor `0x10AC` / product `0xD01D` even when + compiled in — a quirk build attached to any other monitor does + nothing. +* Attached through **one narrow hook** in the core (external-connect + and post-wake-settle, before the blackout commit — never during + pipeline churn), not a generic quirk framework: no other quirks are + planned, and a framework for n=1 is YAGNI. +* The module re-derives the external `DCPAVServiceProxy` on each + event and holds no resident `IOAVServiceRef`; the induced + reconfiguration is marked self-originated (an + `_actionInProgress`-style suppression) **without weakening the + unconditional safety-invariant check**. + +## Consequences + +* Good: other users' builds carry no Dell-specific code, no extra + private-API dependency, and no behavioral difference — the quirk + cannot regress the core for anyone who does not opt in. +* Good: the quirk user gets a preventive fix (YCbCr never negotiated) + applied at exactly the right moments by the actor that already owns + the timing, retiring the BetterDisplay + `restore_rgb.sh` watchdog. +* Bad: `IOAVServiceSetVirtualEDIDMode` is private and fragile across + macOS updates — but that surface exists only in quirk builds. +* Bad: a compile-time flag means no single released binary serves + both audiences, and the `QUIRKS=sp2309w` configuration needs its + own build check (or knowingly stays maintainer-built-only). +* Accepting this ADR is P26's first acceptance criterion; if the + standalone-daemon alternative is chosen instead, this ADR moves to + `rejected` and inject_edid ADR 0002 gets a superseding note. + +## Considered Options and Alternatives + +### Compile the quirk in unconditionally, gate at runtime only + +* Rejected: every user carries dead Dell code and the private-API + reference; the runtime key alone does not remove the fragility + surface from their binaries. + +### A generic quirk framework (registry, per-display hooks) + +* Rejected as YAGNI: exactly one quirk exists or is planned. One + `#ifdef`-guarded hook is cheaper to read and delete. + +### Standalone resident daemon + +* Rejected in inject_edid ADR 0002: reintroduces the two-actor race + on the reconfiguration callback (nested CG transactions, err=1014). + +### Do nothing (keep BetterDisplay + watchdog) + +* Rejected: proprietary, reactive (the cast appears, then clears), + and a competing display actor. MonitorControlLite is retained for + brightness precisely because it avoids DDC and does not trigger + renegotiation. + +## More Information + +* Technical-debt **P26** — the tracking entry whose acceptance + criteria mirror this ADR. +* `inject_edid` ADRs 0001 (why runtime injection) and 0002 (why + blackoutd hosts the one-shot), `inject_edid/docs/sp2309w-display-notes.md` + (EDID decode, firmware investigation), and + `inject_edid/docs/investigations/connection-mode.md` (the + YCbCr→RGB flip measurement; CEA byte 131 `0xF1` → `0xC1`). +* `docs/SESSION-HANDOFF-2026-05-25.md` — the panel-defect summary and + the do-not-entangle instruction (track D). From ea0bff7a2d931db475f3aa6c676b76a3ebcdd109 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 02:56:21 -0400 Subject: [PATCH 22/24] docs: record auto-recovery validation runs 105-107 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../repro-matrix/run105-20260720-020909.md | 47 +++++++++++++++++++ .../repro-matrix/run106-20260720-021026.md | 47 +++++++++++++++++++ .../repro-matrix/run107-20260720-021403.md | 47 +++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 docs/debug/repro-matrix/run105-20260720-020909.md create mode 100644 docs/debug/repro-matrix/run106-20260720-021026.md create mode 100644 docs/debug/repro-matrix/run107-20260720-021403.md diff --git a/docs/debug/repro-matrix/run105-20260720-020909.md b/docs/debug/repro-matrix/run105-20260720-020909.md new file mode 100644 index 0000000..4de6607 --- /dev/null +++ b/docs/debug/repro-matrix/run105-20260720-020909.md @@ -0,0 +1,47 @@ + + +# repro run sheet — 20260720-020909 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 105 started: 2026-07-20 02:09:09 +Build: v0.4.0-19-gb271787 built: 2026-07-20T06:06:47+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. ____ (not given; pass --group a..e) + lid .................... open -> open + moved during sleep? ... [ ] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-20 02:09:37.684 blackoutd[38840:3040108] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-021005 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [ ] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: ____________________________________________________________ +``` diff --git a/docs/debug/repro-matrix/run106-20260720-021026.md b/docs/debug/repro-matrix/run106-20260720-021026.md new file mode 100644 index 0000000..a9a4a94 --- /dev/null +++ b/docs/debug/repro-matrix/run106-20260720-021026.md @@ -0,0 +1,47 @@ + + +# repro run sheet — 20260720-021026 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 106 started: 2026-07-20 02:10:26 +Build: v0.4.0-19-gb271787 built: 2026-07-20T06:06:47+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. ____ (not given; pass --group a..e) + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-20 02:11:03.989 blackoutd[38840:3040108] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-021130 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [ ] E1 [x] E2 [ ] E3 + built-in ... [ ] B0 [x] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [x] yes [ ] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: displaysleep auto-recovery validation run: ./build/blackoutd recovery displaysleep && ./build/blackoutd repro --wake 15 --trigger extcycle. Wake E2/B1 -> E1/B0 -> E0/B0 with no flicker prior to "capturing post wake" cue. +``` diff --git a/docs/debug/repro-matrix/run107-20260720-021403.md b/docs/debug/repro-matrix/run107-20260720-021403.md new file mode 100644 index 0000000..d8c82b0 --- /dev/null +++ b/docs/debug/repro-matrix/run107-20260720-021403.md @@ -0,0 +1,47 @@ + + +# repro run sheet — 20260720-021403 + +Machine-filled by `blackoutd repro`. Fill in the eyewitness +checkboxes and notes; the matrix +(`docs/debug/cursor-on-black-matrix.md`) indexes these sheets +per group. + +```text +Run #: 107 started: 2026-07-20 02:14:03 +Build: v0.4.0-19-gb271787 built: 2026-07-20T06:06:47+00:00 (UTC) + +Conditions (pre-sleep -> at capture): + group .................. ____ (not given; pass --group a..e) + lid .................... open -> open + moved during sleep? ... [x] no [ ] yes: ______ + session ................ unlocked -> unlocked + power .................. AC -> AC + wake ................... scheduled (--wake 15) + trigger ................ extcycle + recovery applied ....... none + settle ................. 20 s + daemon settled ......... 2026-07-20 02:14:36.913 blackoutd[38840:3040108] [wake] — display pipeline settled + +Bundles: + post-wake = docs/debug/blackoutd-diag-20260720-021500 + post-recover = n/a + +@ wake (immediate — first look as the panels light): + external ... [ ] E0 [x] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +@ post-wake capture (settled — at the "capturing post wake" + cue; the state the bundle records): + external ... [x] E0 [ ] E1 [ ] E2 [ ] E3 + built-in ... [x] B0 [ ] B1 [ ] B2 + +Outcome: + cursor-on-black occurred this run? ... [ ] yes [x] no + (yes = settled external E1; note E2/E3 variants in Notes) +Notes: displaysleep auto-recovery validation: ./build/blackoutd recovery displaysleep && ./build/blackoutd repro --wake 15 --trigger extcycle. Wake E1/B0 -> E0/B0 with no flicker prior to "capturing post wake cue". Awake cue not heard. +``` From db0849a7f310b0838cb53cd45439547db8571750 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 02:58:43 -0400 Subject: [PATCH 23/24] docs: note live auto-recovery validation in P29 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/technical-debt.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/technical-debt.md b/docs/technical-debt.md index 8630307..d5b9d22 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -2013,6 +2013,15 @@ Runs 86–104 (matrix "shakedowns, MCL controls, groups C/D/E" table): inaudible right after wake (audio-device bring-up race; both power sources affected). Detection and captures are unaffected; the Notification Center banner carries the timestamp. Known quirk. +- *Live validation (runs 106–107).* With `recoveryStrategy + displaysleep` active and no repro-side `--recover`, a soft-trigger + black wake (run 106) cleared to E0/B0 before the "capturing post + wake" cue, and a clean wake (run 107) passed through untouched — + and the maintainer perceived **no flicker**: firing the cycle at + wake-settle lands it inside the wake transition, before the panels + have visibly stabilized. That takes most of the urgency out of the + flicker-free follow-up (fbpower probe untried as of run 107); it + remains worth one smoke test on principle. **Files**: `src/main.m` (the `diagnose` `dcp.txt` / `connection-mode.txt` readers); investigation otherwise. Relates to P2, P20, P28, ADR 0003, ADR From db4d0f77cfe1ffb1de89c06ae00ff59af4223f55 Mon Sep 17 00:00:00 2001 From: Todd Schulman Date: Mon, 20 Jul 2026 03:15:10 -0400 Subject: [PATCH 24/24] docs: restructure ADRs 0009-0010 to MADR 4.0 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 --- docs/decisions/0009-sp2309w-color-quirk.md | 73 ++++++++++++++----- ...-cursor-on-black-detection-and-recovery.md | 39 +++++++++- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/docs/decisions/0009-sp2309w-color-quirk.md b/docs/decisions/0009-sp2309w-color-quirk.md index ea7cb2c..149e25b 100644 --- a/docs/decisions/0009-sp2309w-color-quirk.md +++ b/docs/decisions/0009-sp2309w-color-quirk.md @@ -57,9 +57,31 @@ Orthogonality note (P29 insists): this is the *color-encoding* defect. It is unrelated to cursor-on-black, which is the absence of scanout — the two must not be entangled in code or triggers. -## Decision +## Decision Drivers -Host the correction as an **opt-in, isolated quirk module**: +* Other users must carry no Dell-specific dead code and no additional + private-API surface in their binaries. +* One display actor: the correction must ride blackoutd's existing + callback and wake-settle timing (inject_edid ADR 0002). +* The single-monitor scope argues against any generality beyond one + guarded hook (YAGNI). +* The unconditional safety invariant must be untouched. + +## Considered Options + +* **Opt-in, isolated quirk module compiled under `make QUIRKS=sp2309w`** + (chosen, proposed). +* Compile the quirk in unconditionally, gate at runtime only. +* A generic quirk framework (registry, per-display hooks). +* Standalone resident daemon. +* Do nothing (keep BetterDisplay + watchdog). + +## Decision Outcome + +Chosen: host the correction as an **opt-in, isolated quirk module**, +because it keeps every non-Dell build free of the quirk's code and +private-API fragility while giving the quirk build blackoutd's +single-actor timing: * Its own translation unit, `src/quirks/DisplayColorController.{h,m}` — a sibling to `DisplayController`, not code woven into it. @@ -80,24 +102,35 @@ Host the correction as an **opt-in, isolated quirk module**: `_actionInProgress`-style suppression) **without weakening the unconditional safety-invariant check**. -## Consequences - -* Good: other users' builds carry no Dell-specific code, no extra - private-API dependency, and no behavioral difference — the quirk - cannot regress the core for anyone who does not opt in. -* Good: the quirk user gets a preventive fix (YCbCr never negotiated) - applied at exactly the right moments by the actor that already owns - the timing, retiring the BetterDisplay + `restore_rgb.sh` watchdog. -* Bad: `IOAVServiceSetVirtualEDIDMode` is private and fragile across - macOS updates — but that surface exists only in quirk builds. -* Bad: a compile-time flag means no single released binary serves - both audiences, and the `QUIRKS=sp2309w` configuration needs its - own build check (or knowingly stays maintainer-built-only). -* Accepting this ADR is P26's first acceptance criterion; if the - standalone-daemon alternative is chosen instead, this ADR moves to - `rejected` and inject_edid ADR 0002 gets a superseding note. - -## Considered Options and Alternatives +### Consequences + +* Good, because other users' builds carry no Dell-specific code, no + extra private-API dependency, and no behavioral difference — the + quirk cannot regress the core for anyone who does not opt in. +* Good, because the quirk user gets a preventive fix (YCbCr never + negotiated) applied at exactly the right moments by the actor that + already owns the timing, retiring the BetterDisplay + + `restore_rgb.sh` watchdog. +* Bad, because `IOAVServiceSetVirtualEDIDMode` is private and fragile + across macOS updates — but that surface exists only in quirk builds. +* Bad, because a compile-time flag means no single released binary + serves both audiences, and the `QUIRKS=sp2309w` configuration needs + its own build check (or knowingly stays maintainer-built-only). +* Neutral, because accepting this ADR is P26's first acceptance + criterion; if the standalone-daemon alternative is chosen instead, + this ADR moves to `rejected` and inject_edid ADR 0002 gets a + superseding note. + +### Confirmation + +Pending — the module has not landed. The underlying mechanism is +confirmed in the standalone tool (inject_edid ADR 0002: cast cleared, +`YCbCr 4:4:4 Limited` → `RGB Full`, CEA byte 131 `0xF1` → `0xC1`); +what remains is the hosted form: no competing actor, no safety- +invariant interaction, re-application on each wake via the +wake-settle hook. + +## Pros and Cons of the Options ### Compile the quirk in unconditionally, gate at runtime only diff --git a/docs/decisions/0010-cursor-on-black-detection-and-recovery.md b/docs/decisions/0010-cursor-on-black-detection-and-recovery.md index be5fb5c..c6b328a 100644 --- a/docs/decisions/0010-cursor-on-black-detection-and-recovery.md +++ b/docs/decisions/0010-cursor-on-black-detection-and-recovery.md @@ -43,7 +43,32 @@ The empirical matrix (`docs/debug/cursor-on-black-matrix.md`, runs attempt in the matrix, locked (n=5) and unlocked alike, plus all manual hot-corner clears. It needs no privileges. -## Decision +## Decision Drivers + +* Recovery must not flicker healthy wakes — the majority — so it needs + a detection gate, not a schedule. +* Both halves must rest on the recorded run matrix, not on mechanism + theory (every theory-first candidate in P29 was falsified). +* Must work at the lock screen (the lived scenario includes locked + wakes). +* No new resident actors or privileges; fail quiet on any uncertainty. + +## Considered Options + +* **Marker-gated displaysleep cycle at wake-settle** (chosen). +* Reconfiguration-flag keying (`0x133e` flap vs `0x111e`). +* Below-CG state polling (`DCPPowerState`, `NormalModeActive`, ioreg). +* Unconditional recovery on every wake-with-external. +* `extcycle` (CG disable/re-enable of the external) as the recovery. +* IOMobileFramebuffer HPD notifications as the detector. +* OSLogStore instead of spawning `log show`. + +## Decision Outcome + +Chosen: **marker-gated displaysleep cycle at wake-settle**, because it +is the only detector that survived n ≥ 3 validation (56/56 vs 0/33) +paired with the only recovery that cleared every recorded black, +locked and unlocked. At `wakeSettleTimerFired` (the ADR-0003 quiet point), when an external display is present, the daemon: @@ -66,7 +91,7 @@ err=1014 retries), a `_systemSleeping` re-check after the query, and the fact that a display-sleep cycle emits screen — not system — sleep notifications, so it cannot re-arm the wake path and loop. -## Consequences +### Consequences * Good, because recovery is targeted: healthy wakes (marker absent) see no flicker at all, and black wakes recover in seconds without user @@ -87,7 +112,15 @@ notifications, so it cannot re-arm the wake path and loop. none` first, or captures record the recovered panel (documented in the matrix how-to). -## Considered Options and Alternatives +### Confirmation + +Live-validated on the target hardware (matrix runs 106–107): with +`recoveryStrategy displaysleep` active and no repro-side recovery, a +soft-trigger black wake cleared to E0/B0 before the post-wake capture +cue, and a clean wake passed through untouched — with no perceived +flicker, the cycle landing inside the wake transition. + +## Pros and Cons of the Options ### Reconfiguration-flag keying (`0x133e` flap vs `0x111e`)