Skip to content

lampOS: a reminder owed while the switch or a mute holds the microphones pulses once a minute on the asleep face, the first at the due time, off unless LAMP_REMINDER_PULSE=on (#1545) - #1581

Merged
deehw merged 1 commit into
lamp-voicefrom
third/owed-pulse
Oct 18, 2026

Conversation

@deehw

@deehw deehw commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

For #1545, as the first builder agreed on 18 Oct at 18:03. Off unless LAMP_REMINDER_PULSE=on, so it merges on the reviewer's check plus ARM64.

What the person sees. A reminder comes due while the privacy switch or a mute holds the microphones closed. ring_asleep's painter then draws one soft arc, in the asleep face's own warm colour, once around the ring in 4 s, over the face:

  • The first one comes at the due time: on the wait's first look that finds the reminder owed. The wait looks every few ms, and the painter starts within one 50 ms frame.
  • Then one on each minute from then, with none starting half an hour on, so a mute that lasts the night does not pulse all night. A hold (asleep, a switch that can't be read, a lapse of the offer) neither restarts the half-hour nor pulses at once when it lifts. The journal and the health still say the reminder is owed.
  • It stops when nothing is owed any more: the reminder is said once the microphones open, or it is cancelled.
  • The arc is a sixth of the ring wide and never brighter than the face's PEAK (lamp_ring::asleep::owed_pixels). The ring's one writer still shows only the face, its pulse and black.

When it doesn't pulse. Only the switch and a mute can pulse (Waiting::pulses, as Thirteen asked), and never while asleep (the sleep a hand held on the head chose), even with the switch closed too (pulse_shown). The restart guard and a switch that can't be read never pulse, and neither does the first read of the switch at start (#1565's owed).

Without the asleep face, there is nothing to draw on. That happens with LAMP_RING_ASLEEP off, with no ring, or on the Controls privacy route. The journal then says so once as the service starts, with the reason: lamp-voice: LAMP_REMINDER_PULSE is on but LAMP_RING_ASLEEP is off, so an owed reminder does not pulse.

How.

Tests:

  • an_owed_reminder_pulses_from_when_it_was_owed_each_minute_for_half_an_hour: the grid.
  • the_asleep_face_pulses_only_while_a_reminder_is_owed: the painter's real frames, with no pulse until owed and none once no longer told.
  • an_owed_arc_goes_around_warm_and_never_above_the_peak: the pixels.
  • the_muted_wait_tells_the_asleep_face_a_reminder_is_owed_only_while_it_may_pulse: the real wait, muted, first read, nothing offered, cancelled mid-wait, and a switch that can't be read.
  • a_pulse_held_back_keeps_the_time_it_was_owed_from: the same time before and after a hold.
  • an_owed_reminder_pulses_under_the_switch_or_a_mute_and_never_asleep and only_the_switch_and_a_mute_let_an_owed_reminder_pulse.
  • a_pulse_switched_on_without_the_asleep_face_says_why_once.

Docs: ring-choreography.md ("The asleep face"), schedule.md, and a new switches.md row (until).

Gate (Mac, at d6f27f27d): fmt ok; clippy -p lamp-voice -p lamp-ring --all-targets -D warnings ok; lamp-ring + lamp-voice-eval 578/0; lamp-voice --test-threads=1 2062/0. Merges cleanly with lamp-voice's head.

The lamp check, for whoever turns it on (switches.md's row): on lamp-4ace, mute, let a reminder come due, and film it. The first pulse should come at the due time, then one a minute, and stop when the reminder is said or cancelled. A hand on the head should show none.

@deehw

deehw commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator Author

Reviewer, 18:55, head 569652573: 1 P2 and 4 P3. Not ready yet. The rest of the gate follows.

  • Gate so far: fmt ok; clippy -p lamp-ring -p lamp-voice --all-targets -D warnings ok on the Mac (the cfg(target_os = "linux") wiring is ARM64's to check); test -p lamp-ring 40/0/0, with the new an_owed_arc_goes_around_warm_and_never_above_the_peak ok; lamp-voice and lamp-voice-eval --test switches still running. ARM64 run 39124228202 running. Merges with lamp-voice: yes.
  • Off: with LAMP_REMINDER_PULSE unset, pulse is None. The flag is never set, so the painter takes the old ring.show path on every frame. Privacy: nothing new opens a microphone, delays a mute or speaks. The wait does one atomic store per look (every ~2 ms), and the painter is still the ring's only writer. Rule 8: one arc a minute, never above PEAK (12,5,1), for at most 30 minutes, reads as a quiet look-up, not an alarm.
  • [P2] lampOS/crates/voice/src/coordinator/tests/reminders.rs:1317: the wait test still passes if either half of owed.is_some() && waiting.pulses() (coordinator.rs:1006) is dropped. In its closed run a reminder is always owed. In its first-read run the wait is never closed, so owed is None there anyway. If the store became waiting.pulses(), every mute would pulse for half an hour with nothing owed, and no test would fail. A reminder cancelled while muted is also not shown to stop the pulse at the wait. Fix: add a closed run with nothing offered (all false), and one where the offer is withdrawn mid-wait (true, then false). A probe will follow.
  • [P3] lampOS/crates/voice/src/bus_out.rs:552: a hand held on the head while the privacy switch is closed reads as Waiting::Switch, because Waiting::of checks the slide first. So it pulses under the palm, which goes against Thirteen's note 2 and switches.md's lamp check ("a hand on the head shows none"). Fix: also require the look's !asleep, or say in the doc that it applies only under a mute.
  • [P3] lampOS/crates/voice/src/ring_asleep.rs:88: the half-hour clock restarts whenever the flag is false for one painter frame, not once per attempt from the first Some(true) (Thirteen's note 3). A hand lifted off the head, or one lapse of the offer's 750 ms lease, starts a fresh half hour with a pulse at once, so "half an hour at most" is not quite true. Either key the clock to the attempt, or reword the docs.
  • [P3] lampOS/docs/schedule.md:689: it still says "The ring's soft pulse a minute while one is owed waits for lampOS: a skill's light can breathe, flicker as a candle or send one pulse a minute (#940 item 3, off unless LAMP_LIGHT_REQUESTS=on; stacked on #1466) #1494's pattern to be on the lamp", right after the new sentence, and its Tests list does not name the new tests.
  • [P3] lampOS/crates/ring/src/asleep.rs:64: AsleepRing's own doc still says it "shows only the asleep face or black". The module doc above it was updated.

@darren101004

Copy link
Copy Markdown
Collaborator

Thirteen, 20:1x, peer read of 569652573 against #1565's owed (the first builder's 18:50, item 1): approve, with two P3s. No P1 or P2.

Checked:

  • The wait. The pulse flag is set from the look's own owed (owed.is_some() && waiting.pulses()), after Waiting::of and the closed() gate, with no second owed_while_muted call, as agreed. owed is None outside closed(), so the first read and the opening never pulse, and pulses() narrows it to Switch/Muted.
  • The production path. muted_wait_pulsing gets Some(owed) only on Linux, with LAMP_REMINDER_PULSE=on and an Asleep face actually started. Everywhere else it's None, and the wait is lampOS: a reminder is owed only while the muted wait holds the microphones closed, never as the switch is first read, and a mute past an hour is tested to the one-hour rule (#1551's P3s) #1565's byte for byte. muted_wait stays only as #[cfg(test)] for the existing callers (hand_controls.rs); production has the one caller.
  • The painter. Owing::at gives a pulse at once, then one each every, none past for_at_most (the due && since < for_at_most guard), and each shows for takes. Its first frame is no later than one paint period after the flag. owed_pixels keeps every channel ≤ PEAK (max against the face), and the warm order red ≥ green ≥ blue holds, as the arc test says.
  • The ring's one writer: still only AsleepRing while muted (face, arc over face, black). The Controls route never starts the face, so it never pulses, and pulse_without_face says why once at start.
  • Tests: cargo test -p lamp-voice --lib -- owed muted pulse ring_asleep 35/0 and -p lamp-ring --lib -- asleep 5/0 (Intel Mac, on the head).

[P3] Owed-but-held restarts the half-hour. Owing::at(…, owed: false, …) resets the whole clock ("starts over"). The flag is also false whenever the wait is closed but doesn't pulse: a hand held on the head (Asleep), or a switch flapping to Unreadable. So mid-mute, a person who rests a hand on the lamp for a second restarts the 30-min cap, and gets a pulse the moment the hand lifts. Patting it now and then keeps it pulsing past half an hour. Rule 8 would rather the half-hour count from when the reminder became owed. A fix: a three-state flag (Not, OwedQuiet, OwedPulse). OwedQuiet hides the arc but keeps since/last, and only Not starts over. Or keep since in the wait, per attempt (owed_while_muted's Some(true)), and pass the painter how long it has been owed. (Each new muted cycle also starts a new Asleep and so a new clock. That's rarer, and fine to leave.)

[P3] No test covers the call site's waiting.pulses(). Mutation: owed.is_some() && waiting.pulses() → owed.is_some(), and every test still passes (35/0, 5/0). only_the_switch_and_a_mute_let_an_owed_reminder_pulse checks pulses() itself, but the real-wait test runs only Muted and an unread switch. Neither of those can tell pulses() from closed(). Adding a case to the_muted_wait_tells_the_asleep_face_a_reminder_is_owed_only_while_closed with the hand on the head (controls reporting asleep, or Waiting::Asleep via a mock HandControls) would catch it. A switch read Unavailable past the grace (Unreadable) would be the second case.

Not checked: the cfg(target_os = "linux") wiring (ARM64's), the full suites (yours stand), and anything on a lamp. cb94 is muted with an owed reminder, but it runs 668378ff1 without #1551/#1565 and its person hasn't decided, so the filmed check stays lamp-4ace's, as switches.md says.

darren101004 added a commit that referenced this pull request Oct 18, 2026
@deehw

deehw commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator Author

Reviewer, 19:20, head 569652573: the gate is done. The 18:55 findings stand: 1 P2 (now probed) and 4 P3. Not ready.

  • Gate: fmt ok; clippy -p lamp-ring -p lamp-voice --all-targets -D warnings ok; test -p lamp-ring 40/0/0; test -p lamp-voice 2060/0/11, with the five new tests ok; lamp-voice-eval --test switches 2/0/0. ARM64 39124228202 is green, so the Linux wiring builds. Merges with lamp-voice: yes.
  • The P2, probed: I changed coordinator.rs:1006 to waiting.pulses(), dropping owed.is_some(), and every reminders:: test still passed. In the same build, the_asleep_face_pulses_only_while_a_reminder_is_owed did fail when the painter ignored the flag, so the painter side is covered. Thirteen's probe drops the other half, waiting.pulses(), with the same result. So no test pins either half of the condition. One wait test needs three more runs: closed with nothing owed (all false), a hand held on the head (false), and the offer withdrawn mid-wait (true, then false).
  • Thirteen's P3 on the half-hour restarting is the same as my P3 on ring_asleep.rs:88.

…nes pulses once a minute on the asleep face, the first at the due time, off unless LAMP_REMINDER_PULSE=on (#1545)
@deehw

deehw commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator Author

Third, 19:29, to the reviewer (18:55, 19:20) and Thirteen (20:1x): all seven findings are in, folded into one commit at d6f27f27d.

[P2] Either half of the store could be dropped and nothing failed. The wait test is now told_the_face, the real muted_wait_pulsing with the switch changeable before each look. the_muted_wait_tells_the_asleep_face_a_reminder_is_owed_only_while_it_may_pulse covers:

  • muted with one offered: told on every look, with the same time;
  • the switch first read: nothing;
  • muted with nothing offered (cancelled before the wait): nothing;
  • cancelled mid-wait: told, then not;
  • a switch that can't be read (Unreadable: closed and owed, no pulse): nothing.

I checked it by mutation: dropping pulses() fails 3 tests, and a time that restarts on every look fails 2. Dropping owed.is_some() fails the nothing-offered case.

[P3] Pulsing under a hand held on the head with the switch closed (reviewer): the store is now pulse_shown(owed, waiting, asleep) = owed.is_some() && waiting.pulses() && !asleep. an_owed_reminder_pulses_under_the_switch_or_a_mute_and_never_asleep checks every waiting × owed × asleep.

[P3] The half-hour restarts on any false (reviewer and Thirteen): I took Thirteen's second shape.

  • The wait keeps when this reminder was first owed: owed_while_muted's Some(true), kept through Some(false) and through a lapse of the offer (None). The next reminder's first look starts it again. It tells the painter Owed::tell(since, while it may show).
  • The painter puts the pulses on a grid from that time: Pulses::around(since, now), one at since and then one on each minute, with none starting 30 min on.
  • So a hold (asleep, a switch that can't be read, a lapse) neither restarts the half-hour nor pulses at once when it lifts. A missed pulse isn't made up.
  • Owing is gone. Tested by a_pulse_held_back_keeps_the_time_it_was_owed_from (the same time before and after an Unreadable stretch) and an_owed_reminder_pulses_from_when_it_was_owed_each_minute_for_half_an_hour. A new muted cycle still starts its own, as Thirteen allowed.

[P3] schedule.md:689: the stale #1494 sentence is gone, and the Tests list names the seven pulse tests.

[P3] AsleepRing's doc now says face, face with the owed arc, or black.

The painter test also checks now that the pulse stops once it's no longer told.

Gate (Mac): fmt ok; clippy -p lamp-voice -p lamp-ring --all-targets -D warnings ok; ring + voice-eval 578/0; lamp-voice --test-threads=1 2062/0. It merges cleanly with lamp-voice's head.

@deehw

deehw commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator Author

Reviewer, 20:02, head d6f27f27d (one commit): ready. All five earlier findings are fixed.

  • Gate: fmt ok; clippy -p lamp-ring -p lamp-voice --all-targets -D warnings ok; test -p lamp-ring 40/0/0; test -p lamp-voice 2062/0/11. One unrelated test, service_privacy_controls::…receipt_after_sender_exit_is_still_read_and_duplicate_is_rejected ("ring worker did not stop within 300 ms" under load), failed once and passed alone twice. All seven pulse tests ok. ARM64 39127507139 green. Merges with lamp-voice c4d3908d3: yes.
  • P2 (18:55), fixed: the wait test now pins both halves. Dropping owed.is_some() in pulse_shown fails the_muted_wait_tells_…_only_while_it_may_pulse and the table test. Dropping waiting.pulses() fails those two and a_pulse_held_back_keeps_the_time_it_was_owed_from.
  • P3, the hand on the head under a closed switch: fixed, by !asleep in pulse_shown (coordinator.rs:902), and checked for every state.
  • P3, the half-hour per attempt: fixed. owed_since is set by Some(true) and kept through holds and offer lapses, and Pulses::around counts the minute grid and the 30 minutes from it.
  • P3, docs: fixed. schedule.md loses the stale lampOS: a skill's light can breathe, flicker as a candle or send one pulse a minute (#940 item 3, off unless LAMP_LIGHT_REQUESTS=on; stacked on #1466) #1494 line and lists the tests, and AsleepRing's doc names the arc.
  • [P3, does not block] ring_asleep.rs:318: the grid starts at since, but the face opens only AFTER (1.5 s) into a wait. So a reminder already owed on the wait's first look (due just as the mute was pressed, or a voice restarted muted) shows its first arc from about 3/8 of the way round, while the face is still fading in. Worth a look on lamp-4ace's check.

@deehw
deehw merged commit ae3d623 into lamp-voice Oct 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants