diff --git a/lampOS/crates/msgs/src/arbitration.rs b/lampOS/crates/msgs/src/arbitration.rs index f5daf48fb3..b72bcd8ff7 100644 --- a/lampOS/crates/msgs/src/arbitration.rs +++ b/lampOS/crates/msgs/src/arbitration.rs @@ -112,11 +112,43 @@ impl LightReached { } } +/// A light asked for on `light/request` that the ring holds but does not +/// show, because another request or the person's own light is over it +/// (`light/outranked`): said when the ring takes it so, when a request +/// shown loses its place, when what is over it changes, and again after +/// each renewal while it stays hidden, so its asker is told it lost (#940). +/// It keeps its claim, and is shown by itself once what is over it lapses +/// or is given back. Nothing is said for what hides every request alike +/// (the privacy switch, a turn's face, a self-test): that is not a loss. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct LightOutranked { + /// The hidden request's `claim.source`. + pub source: String, + /// The `claim.source` of the request shown over it, or + /// [`LightOutranked::PERSON`] for the person's own light. + pub by: String, +} + +impl LightOutranked { + pub const TYPE: &str = "lamp_msgs/LightOutranked"; + pub const VERSION: u32 = 1; + pub const TOPIC: &str = "light/outranked"; + /// `by` for the person's own light (`light/command`), over every + /// request below a person's that began before they asked. + pub const PERSON: &str = "person"; + + /// Each is a message of its own: one asker's must not hide another's. + pub fn declaration() -> Declaration { + Declaration::new(Self::TYPE, Self::VERSION, Kind::Event) + } +} + /// The requests to actuators beside the first topics (#940). pub fn arbitration_topics() -> Vec<(&'static str, Declaration)> { vec![ (LightRequest::TOPIC, LightRequest::declaration()), (LightReached::TOPIC, LightReached::declaration()), + (LightOutranked::TOPIC, LightOutranked::declaration()), ] } @@ -213,4 +245,26 @@ mod tests { (LightReached::TYPE, Kind::Event) ); } + + #[test] + fn a_light_outranked_names_its_asker_and_what_is_over_it() { + let outranked = LightOutranked { + source: "skill:stretch-break".into(), + by: "skill:desk-tutor".into(), + }; + let text = serde_json::to_string(&outranked).unwrap(); + assert_eq!( + text, + "{\"source\":\"skill:stretch-break\",\"by\":\"skill:desk-tutor\"}" + ); + assert_eq!( + serde_json::from_str::(&text).unwrap(), + outranked + ); + let declared = crate::declaration_of(LightOutranked::TOPIC).unwrap(); + assert_eq!( + (declared.type_name.as_str(), declared.kind), + (LightOutranked::TYPE, Kind::Event) + ); + } } diff --git a/lampOS/crates/skills/src/glow.rs b/lampOS/crates/skills/src/glow.rs index ab2ec0ceab..bc39e4dd62 100644 --- a/lampOS/crates/skills/src/glow.rs +++ b/lampOS/crates/skills/src/glow.rs @@ -4,10 +4,13 @@ //! again while its run keeps the light, and gives it back by asking with a //! time already passed. The ring shows it over the person's light until //! then, or until the person asks for theirs, and then shows theirs again -//! by itself. +//! by itself. When the ring holds the light but shows another over it, it +//! says so on `light/outranked`: the run says what was over it in its +//! journal and at its give-back, and keeps asking, so its light comes back +//! by itself once the other lapses (#940). use crate::Call; -use lamp_bus::{Bus, Publisher}; -use lamp_msgs::{Claim, Light, LightRequest, Priority}; +use lamp_bus::{Bus, Publisher, Subscriber}; +use lamp_msgs::{Claim, Light, LightOutranked, LightRequest, Priority}; use std::{ io, sync::{Arc, Mutex, MutexGuard, PoisonError, mpsc}, @@ -58,11 +61,42 @@ pub struct Glow { } /// The renewals of a skill's light: the light asked again, their stop, and -/// their thread. +/// their thread, which gives back what the light was outranked by. struct Renewing { held: Arc>, stop: mpsc::Sender<()>, - thread: JoinHandle<()>, + thread: JoinHandle>, +} + +/// `light/outranked`, subscribed to under a name of its own for each run, +/// before its light is first asked, so the ring's answer is not missed. +fn outranked(bus: &Bus) -> Option { + static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let number = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + bus.subscribe_typed( + LightOutranked::TOPIC, + &format!("skills_glow_{number}"), + LightOutranked::TYPE, + ) + .map_err(|error| eprintln!("lamp-skills: light/outranked not subscribed to: {error}")) + .ok() +} + +/// What `source`'s light was said outranked by since the last look, each +/// added to `by` once, in the order heard, and said in the journal. +fn heard(outranked: Option<&mut Subscriber>, source: &str, by: &mut Vec) { + let Some(outranked) = outranked else { + return; + }; + while let Ok(Some(message)) = outranked.recv(Duration::ZERO) { + let Ok(told) = message.parse::() else { + continue; + }; + if told.source == source && !by.contains(&told.by) { + eprintln!("lamp-skills: {source}'s light is outranked by {}", told.by); + by.push(told.by); + } + } } impl Glow { @@ -104,6 +138,11 @@ impl Glow { "{what}: left alone, since nothing takes light/request (the ring needs {REQUESTS_ENVIRONMENT}=on)" ); } + // Listened to before the first ask, and by the renewals after it. + let mut told = match self.renewing { + Some(_) => None, + None => outranked(&self.glows.bus), + }; let until_us = lamp_ipc::monotonic_us() + GLOW_HOLDS_US; if let Err(error) = ask_ring(&self.glows, &self.source, &light, until_us) { return format!("{what}: light/request not written ({error})"); @@ -117,17 +156,21 @@ impl Glow { let (glows, source, again) = (self.glows.clone(), self.source.clone(), Arc::clone(&held)); let thread = std::thread::spawn(move || { + let mut by = Vec::new(); // Asked again on the program's clock; each claim // holds on the host's, which the ring reads. while let Err(mpsc::RecvTimeoutError::Timeout) = crate::clock::recv_timeout(&stopped, glows.every) { + heard(told.as_mut(), &source, &mut by); let light = locked(&again).clone(); let until_us = lamp_ipc::monotonic_us() + GLOW_HOLDS_US; if let Err(error) = ask_ring(&glows, &source, &light, until_us) { eprintln!("lamp-skills: {source}'s light not asked again: {error}"); } } + heard(told.as_mut(), &source, &mut by); + by }); self.renewing = Some(Renewing { held, stop, thread }); } @@ -145,17 +188,22 @@ impl Glow { } = self; let Renewing { held, stop, thread } = renewing?; drop(stop); - let _ = thread.join(); + let by = thread.join().unwrap_or_default(); let light = locked(&held).clone(); // A time already passed: the ring lets it go at once, and shows the // person's light again. let until_us = lamp_ipc::monotonic_us().saturating_sub(1); - Some(match ask_ring(&glows, &source, &light, until_us) { + let back = match ask_ring(&glows, &source, &light, until_us) { Ok(()) => "light given back: asked again with a time already passed".into(), Err(error) => format!( "light not given back at once: light/request not written ({error}); the ring lets it go within {} s", GLOW_HOLDS_US / 1_000_000 ), + }; + Some(if by.is_empty() { + back + } else { + format!("{back}; outranked by {}", by.join(", ")) }) } } @@ -325,6 +373,47 @@ mod tests { assert!(taken(&mut ring).is_empty(), "the renewals have stopped"); } + /// #940: the ring says a run's light was outranked; the run says by + /// what at its give-back, each once, and not another's. + #[test] + fn a_light_outranked_is_said_at_the_give_back_by_what() { + let (_dir, glows, mut ring) = on_a_bus(Duration::from_millis(50)); + let mut says = glows + .bus + .publisher_declared( + LightOutranked::TOPIC, + &LightOutranked::declaration(), + "ring", + ) + .unwrap(); + let mut glow = Glow::new(&glows, "desk-tutor", Some(light(true, "blue", 3))); + glow.ask(&colour("white", Some(5))); + assert_eq!(taken(&mut ring).len(), 1); + let told = |source: &str, by: &str| LightOutranked { + source: source.into(), + by: by.into(), + }; + for (at, (source, by)) in [ + ("skill:desk-tutor", "skill:stretch-break"), + ("skill:stretch-break", "skill:desk-tutor"), + ("skill:desk-tutor", "skill:stretch-break"), + ("skill:desk-tutor", "person"), + ] + .into_iter() + .enumerate() + { + says.publish_json(at as u64 + 1, &told(source, by)).unwrap(); + } + std::thread::sleep(Duration::from_millis(120)); + assert_eq!( + glow.give_back().as_deref(), + Some( + "light given back: asked again with a time already passed; \ + outranked by skill:stretch-break, person" + ) + ); + } + #[test] fn nothing_is_asked_without_a_taker_or_a_light_to_dim_and_nothing_given_back() { let (_dir, glows) = a_bus(Duration::from_millis(50)); diff --git a/lampOS/crates/voice-eval/src/export.rs b/lampOS/crates/voice-eval/src/export.rs index e2669477cf..aac8fb67e5 100644 --- a/lampOS/crates/voice-eval/src/export.rs +++ b/lampOS/crates/voice-eval/src/export.rs @@ -674,6 +674,7 @@ const BUS_TOPICS: &[&str] = &[ "lamp/selftest", "light/command", "light/drawn", + "light/outranked", "light/reached", "light/request", "light/state", diff --git a/lampOS/crates/voice/src/ring_light.rs b/lampOS/crates/voice/src/ring_light.rs index 0fcdd58fdc..fb6124c52e 100644 --- a/lampOS/crates/voice/src/ring_light.rs +++ b/lampOS/crates/voice/src/ring_light.rs @@ -26,6 +26,9 @@ //! the asker renews it while it is shown, so a skill can wait for the light //! it asked for. A turn's face has colours of its own and never shows the //! lamp's light, so nothing is reached inside a turn (the reviewer on #1166). +//! A request the ring holds but does not show, because another request or +//! the person's light is over it, is said on `light/outranked`, naming +//! what is over it, so its asker is told it lost (#940). //! //! With `LAMP_LIGHT_KEEP=on` (the first builder on #1464) the person's light //! is also kept on disk ([`KEEP_PATH`]), written when it changes, at most @@ -40,7 +43,7 @@ //! (a colour that does not exist, a level outside 1 to 5) is passed over. use crate::{ring_face::Lit, ring_requests::Requests}; use lamp_bus::{Bus, Publisher, Subscriber}; -use lamp_msgs::{Light, LightReached, LightRequest}; +use lamp_msgs::{Light, LightOutranked, LightReached, LightRequest}; use std::{io, path::Path, time::Duration}; /// The name the ring's owner publishes under. @@ -196,6 +199,9 @@ pub struct RingLight { reached: Option, /// Where the person's light is kept across a restart, when switched on. keep: Option, + /// Where a request held but not shown is said outranked, when switched + /// on. + outranked: Option, /// The request the face shows, until its light is said reached. reaching: Option, } @@ -257,6 +263,7 @@ impl RingLight { requests: None, requested: None, reached: None, + outranked: None, reaching: None, keep: keep.map(|path| Keep { path: path.to_owned(), @@ -307,8 +314,14 @@ impl RingLight { let asked = bus.subscribe_typed(LightRequest::TOPIC, SOURCE, LightRequest::TYPE)?; let reached = bus.publisher_declared(LightReached::TOPIC, &LightReached::declaration(), SOURCE)?; + let outranked = bus.publisher_declared( + LightOutranked::TOPIC, + &LightOutranked::declaration(), + SOURCE, + )?; self.requests = Some((asked, Requests::new(clock_id))); self.reached = Some(reached); + self.outranked = Some(outranked); Ok(()) } @@ -413,7 +426,18 @@ impl RingLight { self.testing = None; changed = lit(&self.now); } - if let Some((_, requests)) = &self.requests { + if let Some((_, requests)) = &mut self.requests { + // Each asker that lost is told, whatever the face shows now: a + // turn or a test hides the winner too, but it still holds. + if let Some(outranked) = self.outranked.as_mut() { + requests.outranked(now_us, |source, by| { + let told = LightOutranked { + source: source.to_owned(), + by: by.to_owned(), + }; + let _ = outranked.publish_json(now_us, &told); + }); + } let shown = requests.shown(now_us); let light = shown.map(|request| &request.light); if light != self.requested.as_ref() { @@ -872,12 +896,15 @@ mod tests { /// The reviewer on #993: off unless switched on. A ring without /// `take_requests` takes no `light/request` at all: the same glow that a - /// switched-on ring shows leaves this one on the person's light. + /// switched-on ring shows leaves this one on the person's light, and + /// says nothing on `light/outranked` either, though the person's light + /// is what it shows (the reviewer on #1466). #[test] fn without_the_switch_a_light_request_changes_nothing() { let root = Root::new("light-request-off"); let (mut ring, _) = RingLight::on(&root.0).unwrap(); let bus = Bus::new(&root.0).unwrap(); + let mut outranked = bus.subscribe(LightOutranked::TOPIC, "skills").unwrap(); let mut asks = bus .publisher_declared(Light::TOPICS[0], &Light::declaration(), "actions") .unwrap(); @@ -901,6 +928,10 @@ mod tests { std::thread::sleep(Duration::from_millis(2)); } assert_eq!(ring.face(), &light(true, "blue", 2)); + assert!( + outranked.recv(Duration::ZERO).unwrap().is_none(), + "nothing said on light/outranked without the switch" + ); } /// #940: a skill's glow is shown over the person's light while its @@ -1129,6 +1160,63 @@ mod tests { assert_eq!(ring.face(), &light(true, "white", 5)); } + /// #940: two skills ask for the light at once. The newer is shown and + /// the other is told on `light/outranked` what is over it, once, and + /// again after its renewal; it is shown by itself once the winner's + /// lease ends, and under the person's light it is told it is theirs. + #[test] + fn a_skill_whose_light_is_not_shown_is_told_what_is_over_it() { + let root = Root::new("light-outranked"); + let (mut ring, _) = RingLight::on(&root.0).unwrap(); + ring.take_requests(&root.0, "boot-test").unwrap(); + let bus = Bus::new(&root.0).unwrap(); + let mut outranked = bus.subscribe(LightOutranked::TOPIC, "skills").unwrap(); + let mut asks = bus + .publisher_declared(Light::TOPICS[0], &Light::declaration(), "actions") + .unwrap(); + let mut glows = bus + .publisher_declared(LightRequest::TOPIC, &LightRequest::declaration(), "skills") + .unwrap(); + let told = |outranked: &mut lamp_bus::Subscriber| { + let mut said = Vec::new(); + while let Some(message) = outranked.recv(Duration::ZERO).unwrap() { + let told: LightOutranked = message.parse().unwrap(); + said.push(format!("{} by {}", told.source, told.by)); + } + said + }; + let mut at = 0u64; + ring.tick(at); + let skill = lamp_msgs::Priority::Skill; + let tutor = request("skill:tutor", skill, 10_000_000, "white"); + glows.publish_json(1, &tutor).unwrap(); + looked(&mut ring, &mut at); + assert!(told(&mut outranked).is_empty(), "alone, it is shown"); + glows + .publish_json(2, &request("skill:stretch", skill, 1_500_000, "green")) + .unwrap(); + looked(&mut ring, &mut at); + assert_eq!(ring.face(), &light(true, "green", 4)); + assert_eq!(told(&mut outranked), ["skill:tutor by skill:stretch"]); + looked(&mut ring, &mut at); + assert!(told(&mut outranked).is_empty(), "said once"); + // Its renewal is told again, and the newer still shown. + glows.publish_json(3, &tutor).unwrap(); + looked(&mut ring, &mut at); + assert_eq!(ring.face(), &light(true, "green", 4)); + assert_eq!(told(&mut outranked), ["skill:tutor by skill:stretch"]); + // The winner's lease ends: the loser is shown by itself, told + // nothing. + at = 1_500_000; + looked(&mut ring, &mut at); + assert_eq!(ring.face(), &light(true, "white", 4)); + assert!(told(&mut outranked).is_empty()); + asks.publish_json(4, &light(true, "blue", 2)).unwrap(); + looked(&mut ring, &mut at); + assert_eq!(ring.face(), &light(true, "blue", 2)); + assert_eq!(told(&mut outranked), ["skill:tutor by person"]); + } + /// The ring's own socket on `topic`, for a datagram made by hand. fn ring_socket(root: &Path, topic: &str) -> std::path::PathBuf { std::fs::read_dir(root.join(topic).join("subs")) diff --git a/lampOS/crates/voice/src/ring_requests.rs b/lampOS/crates/voice/src/ring_requests.rs index 2d7fa4cc20..4769e6219d 100644 --- a/lampOS/crates/voice/src/ring_requests.rs +++ b/lampOS/crates/voice/src/ring_requests.rs @@ -20,7 +20,11 @@ //! given way: there is no second list that could fill up. A request refused //! for want of a place is not held, so its asker's next request is a new //! claim, heard as begun when it comes (Twelve on #993). -use lamp_msgs::{Light, LightRequest, Priority}; +//! +//! The loser is told (#940): a request held but not shown is said +//! outranked, naming what is over it, once for each thing over it and +//! again after each renewal while it stays hidden ([`Requests::outranked`]). +use lamp_msgs::{Light, LightOutranked, LightRequest, Priority}; /// At most this many askers are kept at once, given way or not; another is /// passed over until one's claim lapses or is given back. @@ -36,6 +40,9 @@ struct Held { /// Sent before the person's light that was read in the same look, so /// given way as if it had come first. before_person: bool, + /// What it was last said outranked by, since it was taken: none while + /// it is shown, or before it is first said hidden. + told: Option, } pub struct Requests { @@ -103,6 +110,7 @@ impl Requests { order, since_us, before_person: was_before, + told: None, }); true } @@ -118,15 +126,50 @@ impl Requests { && (held.before_person || self.person_at_us.is_some_and(|at| held.since_us < at)) } - /// The request shown at `now_us`, if one holds and has not given way. - pub fn shown(&self, now_us: u64) -> Option<&LightRequest> { + /// Where the request shown at `now_us` is held, if one holds and has + /// not given way. + fn shown_at(&self, now_us: u64) -> Option { self.held .iter() - .filter(|held| { + .enumerate() + .filter(|(_, held)| { held.request.claim.holds_at(&self.clock_id, now_us) && !self.given_way(held) }) - .max_by_key(|held| (held.request.claim.priority, held.order)) - .map(|held| &held.request) + .max_by_key(|(_, held)| (held.request.claim.priority, held.order)) + .map(|(at, _)| at) + } + + /// The request shown at `now_us`, if one holds and has not given way. + pub fn shown(&self, now_us: u64) -> Option<&LightRequest> { + self.shown_at(now_us).map(|at| &self.held[at].request) + } + + /// Tells `tell(source, by)` of each request that holds at `now_us` but + /// is not shown: `by` is the source of the request shown over it, or + /// [`LightOutranked::PERSON`] for the person's own light. Each is told + /// once for what is over it, again when that changes, and again after + /// each renewal while it stays hidden; the one shown is told nothing. + pub fn outranked(&mut self, now_us: u64, mut tell: impl FnMut(&str, &str)) { + let shown = self.shown_at(now_us); + for at in 0..self.held.len() { + let held = &self.held[at]; + let by = if Some(at) == shown || !held.request.claim.holds_at(&self.clock_id, now_us) { + None + } else if self.given_way(held) { + Some(LightOutranked::PERSON) + } else { + // One holds that has not given way, so one is shown. + shown.map(|shown| self.held[shown].request.claim.source.as_str()) + }; + if held.told.as_deref() == by { + continue; + } + let by = by.map(str::to_owned); + if let Some(by) = &by { + tell(&held.request.claim.source, by); + } + self.held[at].told = by; + } } /// The light shown at `now_us`, if a request holds. @@ -315,6 +358,61 @@ mod tests { assert_eq!(colour(&requests, S), Some("white")); } + /// #940: the loser is told what is over it, once, again when that + /// changes or it renews, and never while it is shown or once lapsed. + #[test] + fn a_request_held_but_not_shown_is_told_what_is_over_it() { + let told = |requests: &mut Requests, now_us| { + let mut said = Vec::new(); + requests.outranked(now_us, |source, by| said.push(format!("{source} by {by}"))); + said.sort(); + said + }; + let mut requests = Requests::new(BOOT); + let a = |until_us| request("skill:a", Priority::Skill, until_us, "blue"); + let b = |until_us| request("skill:b", Priority::Skill, until_us, "red"); + requests.take(a(15 * S), 0, false); + assert!(told(&mut requests, 0).is_empty(), "alone, it is shown"); + // The newest claim of equal priority wins: a loses its place. + requests.take(b(15 * S), S, false); + assert_eq!(told(&mut requests, S), ["skill:a by skill:b"]); + assert!(told(&mut requests, 2 * S).is_empty(), "said once"); + // Its renewal keeps b over it, and it is told again; b's is shown + // and told nothing. + requests.take(a(20 * S), 5 * S, false); + assert_eq!(told(&mut requests, 5 * S), ["skill:a by skill:b"]); + requests.take(b(20 * S), 6 * S, false); + // Taken below the light shown: told at once. + requests.take( + request("idle", Priority::Idle, 20 * S, "warm"), + 6 * S, + false, + ); + assert_eq!(told(&mut requests, 6 * S), ["idle by skill:b"]); + // The conversation over all three, and b over the others again once + // it lapses. + let voice = request("voice", Priority::Conversation, 10 * S, "white"); + requests.take(voice, 7 * S, false); + assert_eq!( + told(&mut requests, 7 * S), + ["idle by voice", "skill:a by voice", "skill:b by voice"] + ); + assert_eq!( + told(&mut requests, 10 * S), + ["idle by skill:b", "skill:a by skill:b"] + ); + // The person's own light over every one. + requests.person_asked(11 * S); + assert_eq!( + told(&mut requests, 11 * S), + ["idle by person", "skill:a by person", "skill:b by person"] + ); + // One given back, or lapsed, is told nothing more. + assert!(!requests.take(b(0), 12 * S, false)); + assert!(told(&mut requests, 12 * S).is_empty()); + assert!(told(&mut requests, 20 * S).is_empty()); + } + /// The reviewer on #993 (11:35): sixteen skills, the person's light, a /// seventeenth skill, the person's light again. A claim given way keeps /// its place, so the seventeenth is not held and cannot show over the diff --git a/lampOS/docs/bus.md b/lampOS/docs/bus.md index de37045433..35ab40d4a4 100644 --- a/lampOS/docs/bus.md +++ b/lampOS/docs/bus.md @@ -358,6 +358,7 @@ of each, and its kind and period as a declaration, are in | `light/test`, `light/drawn` | A self-test's colour, shown for 2 s at most; the light the ring actually drew | latched | The self-test; the ring's owner | Asked once a colour; drawn when it changes, and once a second | | `light/request` | A light asked for that lapses, with its asker, priority and time (#940) | event | Skills and other parts (`lamp-cloud`'s preview, [cloud.md](cloud.md)) | Asked, and asked again to keep it, at most 30 s ahead | | `light/reached` | A requested light the ring has shown at rest for 350 ms, with its asker (#1146) | event | The ring's owner, with `LAMP_LIGHT_REQUESTS=on` | When a request's light has been shown 350 ms, and again after each renewal while it is; never inside a turn | +| `light/outranked` | A request the ring holds but doesn't show: its asker, and `by`, the asker shown over it or `person` (#940) | event | The ring's owner, with `LAMP_LIGHT_REQUESTS=on` | When it is taken hidden, loses its place, or what is over it changes, and again after each renewal while it stays hidden | | `diagnostics` | Each part's health: ok, warning, error or stale | stream | Every part | Once a second | | `policy/decisions` | What the policy decided, with the probability and each judgement behind it, and whether it is only watching ([policy.md](policy.md)) | stream | The policy | With each thing heard | @@ -416,6 +417,7 @@ that owns it: | `light/drawn` | the voice's ring worker, from its own write receipts ([ring-choreography.md](ring-choreography.md), "The lamp's own light") | the self-test | | `light/request` | `lamp-skills`, a skill's light, with `LAMP_LIGHT_REQUESTS=on` ([skills.md](skills.md)); `lamp-cloud`, the app's preview ([cloud.md](cloud.md)) | the voice's ring worker, with `LAMP_LIGHT_REQUESTS=on` | | `light/reached` | the voice's ring worker, with `LAMP_LIGHT_REQUESTS=on` ([ring-choreography.md](ring-choreography.md), "The lamp's own light") | nothing yet; `lamp-skills`, to wait for a skill's light before its look (#1146) | +| `light/outranked` | the voice's ring worker, with `LAMP_LIGHT_REQUESTS=on` ([ring-choreography.md](ring-choreography.md), "The lamp's own light") | `lamp-skills`, a run's own subscription: the run says what was over its light in its journal and at its give-back ([skills.md](skills.md)) | | `diagnostics` | every part that says how it is: the voice, the eyes, the body, the policy, and the board through the health program | the health program | | `lamp/health`, `board/state` | the health program ([health.md](health.md)) | `lamp-health`, the command | diff --git a/lampOS/docs/msgs.md b/lampOS/docs/msgs.md index 0b7b71f906..6abc3327cc 100644 --- a/lampOS/docs/msgs.md +++ b/lampOS/docs/msgs.md @@ -67,6 +67,7 @@ stream. `declaration_of()` recognizes every catalogue without changing | `BodyGesture` | `body/gesture` | event | none | What became of one of the body's gestures: its `name` (the clip's, the body's own `idle` and shifts included) and `state`, `began`, `over` (played out, let go, replaced, or the run stopped) or `dropped` (asked and dropped before it began, the anti-tip rule's refusal and #1239's `busy` and `held by a hand` included, or ended for a higher ask, so no `over` follows). Said by lamp-body with `LAMP_BUS=on` where its journal says the same ([body.md](body.md), "On the bus"). Read-only. `body_topics()` lists it | | `VoiceMute` | `voice/mute` | latest | 3 s | Whether the voice's software mute is on (`muted`), and why: `reason: "sleep"` when a hand held on the head put the lamp to sleep, absent for the mute as it always was (#711). Said by the voice with `LAMP_HEAD_TOUCH=on` and `--controls-mute`, when it changes and every second; Expression's `--head-touch` sleeps from it. `mute_topics()` lists it. Not the physical slide | | `LightRequest` | `light/request` | event | none | A light asked for that lapses (#940, [skills.md](skills.md)): a `claim` (`source`, a part or `skill:`; `priority`, one of `idle`, `skill`, `conversation`, `safety`, `person`, in the order they give way; the asker's `clock_id` and `until_us`, at most 30 s ahead, renewed by asking again) and the `light`. The ring shows the highest-priority request that holds, the newest claim of equal priority (a renewal keeps its place), and the person's lasting `light/command` once none does, with `LAMP_LIGHT_REQUESTS=on` ([ring-choreography.md](ring-choreography.md), "The lamp's own light"). `arbitration_topics()` lists it | +| `LightOutranked` | `light/outranked` | event | none | A light asked for on `light/request` that the ring holds but doesn't show: its `source` (the hidden request's `claim.source`) and `by`, the `claim.source` shown over it or `person` for the person's own light (#940). Said when it is taken hidden, when it loses its place and when what is over it changes, and again after each renewal while it stays hidden. The asker keeps its claim, which is shown by itself once what is over it lapses; nothing is said for what hides every request alike (the privacy switch, a turn's face, a self-test) ([ring-choreography.md](ring-choreography.md), "The lamp's own light"). `arbitration_topics()` lists it | | `LightReached` | `light/reached` | event | none | A light asked for on `light/request` that the ring has shown on resting frames for 350 ms, said then and again after each renewal while it is shown: its `source` (the request's `claim.source`) and the `light` (#1146). Nothing is said inside a turn, whose face never shows the lamp's light, nor for a light never shown that long ([ring-choreography.md](ring-choreography.md), "The lamp's own light"). `arbitration_topics()` lists it | | `Rules` | `memory/rules` | latest | none | What the people at the lamp asked it about speaking first, as typed facts for the policy (#813, [memory.md](memory.md#what-a-person-teaches-and-corrects-813)): `person` (the memory's id `p-` of the known person, or `everyone`), `quiet` (asked never to speak first, unprompted), `quiet_mornings` (asked not to speak first in the mornings, before noon local time, or never), `rules`, how many rules of every kind the room's and theirs hold, and `hello` / `news` (`Unprompted`: `never`, `not_before_minute`, `not_from_minute`, local minute of the day, `not_while_talking`; absent in an older message, nothing held), the greeting's and the unasked news's limits, every typed rule merged, the strictest winning. Said by the voice with `LAMP_MEMORY_RULES=on` at each session open and each rule kept or forgotten; kept until the next. Never a rule's words | | `Decision` | `policy/decisions` | event | none | What the policy decided ([policy.md](policy.md)): what it was about, the choice, its probability, the decision model's answer to each question by the question's name, who decided, how long the asking took, whether anything acts on it, the request's number if known, for a `progress` decision the work it was about (`task`, `::@`), and for a `hello` decision the arrival (`arrival`), both added 2026-10-15 as optional fields; for an answer to a skill's ask, the ask (`ask`, 2026-10-16, optional). Not what was heard | diff --git a/lampOS/docs/ring-choreography.md b/lampOS/docs/ring-choreography.md index e93dc8ce34..02471dfb4f 100644 --- a/lampOS/docs/ring-choreography.md +++ b/lampOS/docs/ring-choreography.md @@ -437,8 +437,19 @@ that takes the light (`src/ring_light.rs`). Started with off the ring, the person's light ends, or that lapses first. A waiter subscribes before it asks and bounds its wait. A skill waits for it before the step that speaks, never after: its own say is a turn, so its light is - reached only once that turn has ended (El on #1166). A boot clock that - cannot be read is said once and the ring takes the person's light alone. + reached only once that turn has ended (El on #1166). A request the ring + holds but doesn't show, because another request or the person's light is + over it, is said on `light/outranked` (`lamp_msgs/LightOutranked`: the + request's `source`, and `by`, the `claim.source` shown over it or + `person` for the person's light), so its asker is told it lost (#940). + It is said when the request is taken hidden, when a request shown loses + its place, when what is over it changes, and again after each renewal + while it stays hidden. A turn's face, a self-test and the privacy switch + hide every request alike and are no loss, so nothing is said for them; + a request outranked meanwhile is still told. The loser keeps its claim and is + shown by itself once what is over it lapses or is given back. A boot + clock that cannot be read is said once and the ring takes the person's + light alone. Whoever asks never writes the ring: the actions program publishes the light wanted and reads the state back ([actions.md](actions.md)). A bus diff --git a/lampOS/docs/skills.md b/lampOS/docs/skills.md index ffb6d73962..02f6874633 100644 --- a/lampOS/docs/skills.md +++ b/lampOS/docs/skills.md @@ -372,7 +372,14 @@ As built on 2026-10-16 (#938, `crates/skills`, not yet run on a lamp): the switch. The actions part is not called for the light. `done` says `light warm: asked of the ring at level 5` and - `light given back: asked again with a time already passed`. + `light given back: asked again with a time already passed`. When the + ring shows another light over the run's (a higher priority, a newer + skill's, or the person's), it says so on `light/outranked`. The run + listens from before its first ask. It writes `lamp-skills: skill:'s + light is outranked by ` in its journal at the next renewal, and + adds `; outranked by , ...` to the give-back line, each `by` once, + in the order heard (#940). It keeps asking, so its light comes back by + itself once the other lapses. - **With `LAMP_BODY_REQUESTS=on`** (#940), each gesture and move a skill sends on `mood.sock` says who asks: `{"gesture":"nod","source":"skill:","priority":"skill"}`, and the