sessiond: squeeze is input, and a pocket can veto it
Active Edge becomes InputTrigger::Squeeze. It resets the idle budget and lands in the trail like any real input, but unlike a power button it is a sensor reading and a tight pocket is a squeezed chassis, so it carries the same proximity veto as tap-to-wake (DEVICE-STATE-MACHINE 4). note_input_gated applies the veto and records the refusal via record_decision — a silently dropped report is what 10 exists to prevent. The input op now advertises refused_by_state. Producers cooperate rather than being enforced; TASK-41 is the gate.
This commit is contained in:
parent
68b6025226
commit
c48737620e
3 changed files with 92 additions and 4 deletions
|
|
@ -518,6 +518,7 @@ pub enum ForensicEvent {
|
|||
pub enum WakeTrigger {
|
||||
PowerButton,
|
||||
DoubleTapToWake,
|
||||
Squeeze,
|
||||
ProximityFar,
|
||||
RtcAlarm,
|
||||
ModemIrq,
|
||||
|
|
@ -1577,6 +1578,39 @@ impl DeviceStateMachine {
|
|||
}
|
||||
}
|
||||
|
||||
/// Real user input, with the machine's own veto applied first.
|
||||
///
|
||||
/// `note_input` is unconditional by design: a power button is intent and is
|
||||
/// never refused (§4, "no — hardware signal"). A squeeze is not a button.
|
||||
/// It is a strain reading, and a phone in a tight pocket is a squeezed
|
||||
/// chassis — the same failure a covered proximity sensor already vetoes for
|
||||
/// tap-to-wake. Routing squeeze through here rather than straight to
|
||||
/// `note_input` is what stops a pocket from resetting the idle budget and
|
||||
/// opening a verb surface.
|
||||
///
|
||||
/// The refusal is recorded, not dropped. §10 spent a section establishing
|
||||
/// that absence must never be mistaken for a negative; a producer whose
|
||||
/// reports vanish silently is indistinguishable from a dead one.
|
||||
pub fn note_input_gated(&mut self, trigger: InputTrigger) -> Option<Vec<Action>> {
|
||||
if self.suppress_wake(trigger) {
|
||||
warn!(
|
||||
"[device-state] {:?} refused — proximity near, treating as pocket",
|
||||
trigger
|
||||
);
|
||||
self.record_decision(
|
||||
"input-refused",
|
||||
serde_json::json!({
|
||||
"trigger": format!("{:?}", trigger),
|
||||
"proximity_near": self.sensor_evidence.proximity_near,
|
||||
"confidence": self.sensor_evidence.confidence(),
|
||||
}),
|
||||
"a covered sensor is the pocket veto (DEVICE-STATE-MACHINE §4)",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(self.note_input(trigger))
|
||||
}
|
||||
|
||||
/// Real user input. Resets the blank budget, re-arms the rule, and undoes
|
||||
/// the pre-warning dim if one is showing — that cancel is the whole point
|
||||
/// of the grace window, so it returns the action rather than waiting for
|
||||
|
|
@ -1599,6 +1633,7 @@ impl DeviceStateMachine {
|
|||
let wake = match trigger {
|
||||
InputTrigger::PowerButton => WakeTrigger::PowerButton,
|
||||
InputTrigger::DoubleTapToWake => WakeTrigger::DoubleTapToWake,
|
||||
InputTrigger::Squeeze => WakeTrigger::Squeeze,
|
||||
InputTrigger::Touch | InputTrigger::Key => WakeTrigger::UserInput,
|
||||
InputTrigger::Unknown => WakeTrigger::Unknown,
|
||||
};
|
||||
|
|
@ -1718,7 +1753,10 @@ impl DeviceStateMachine {
|
|||
/// call-state input (ModemManager / callaudiod) — a factor, never an
|
||||
/// authority.
|
||||
pub fn suppress_wake(&self, trigger: InputTrigger) -> bool {
|
||||
matches!(trigger, InputTrigger::DoubleTapToWake) && self.sensor_evidence.proximity_near
|
||||
matches!(
|
||||
trigger,
|
||||
InputTrigger::DoubleTapToWake | InputTrigger::Squeeze
|
||||
) && self.sensor_evidence.proximity_near
|
||||
}
|
||||
|
||||
/// Attempt a state transition. Returns true if the transition was
|
||||
|
|
@ -2487,6 +2525,33 @@ mod tests {
|
|||
assert_eq!(actions, vec![Action::Blank]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn squeeze_is_vetoed_by_proximity_but_a_power_button_is_not() {
|
||||
let (mut sm, _t0) = locked_and_lit();
|
||||
sm.sensor_evidence.proximity_near = true;
|
||||
sm.mark_evidence_seen(SensorSource::Proximity);
|
||||
|
||||
// A squeezed chassis in a pocket reads exactly like a deliberate
|
||||
// squeeze. A covered sensor is the veto, same as tap-to-wake.
|
||||
assert!(sm.suppress_wake(InputTrigger::Squeeze));
|
||||
assert!(sm.note_input_gated(InputTrigger::Squeeze).is_none());
|
||||
|
||||
// Intent from a hardware button is never refused (§4).
|
||||
assert!(!sm.suppress_wake(InputTrigger::PowerButton));
|
||||
assert!(sm.note_input_gated(InputTrigger::PowerButton).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_uncovered_squeeze_is_real_input() {
|
||||
let (mut sm, _t0) = locked_and_lit();
|
||||
sm.sensor_evidence.proximity_near = false;
|
||||
|
||||
assert!(!sm.suppress_wake(InputTrigger::Squeeze));
|
||||
assert!(sm.note_input_gated(InputTrigger::Squeeze).is_some());
|
||||
// It resets the idle budget like any other real input.
|
||||
assert!(sm.idle_since.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_proximity_stops_suppressing_wake() {
|
||||
let (mut sm, t0) = locked_and_lit();
|
||||
|
|
|
|||
|
|
@ -170,7 +170,9 @@ pub const VERBS: &[VerbDoc] = &[
|
|||
op: "input",
|
||||
mutates: true,
|
||||
summary: "real user input happened; resets the idle budget",
|
||||
refuses: &[],
|
||||
// A squeeze is refusable — a pocket can produce one, and proximity is
|
||||
// the veto (DEVICE-STATE-MACHINE §4). A power button never is.
|
||||
refuses: &[RefusalCode::RefusedByState],
|
||||
example: r#"{"op":"input","trigger":"touch"}"#,
|
||||
},
|
||||
VerbDoc {
|
||||
|
|
@ -289,6 +291,13 @@ pub enum InputTrigger {
|
|||
Key,
|
||||
PowerButton,
|
||||
DoubleTapToWake,
|
||||
/// Active Edge — the frame was squeezed. Deliberate intent, like a button,
|
||||
/// but unlike a button it is a sensor and it can be produced by a pocket:
|
||||
/// a squeezed chassis is exactly what a phone in a tight pocket is. So it
|
||||
/// carries the same proximity veto as tap-to-wake (`suppress_wake`), and
|
||||
/// for the same reason — see DEVICE-STATE-MACHINE §4, "a covered sensor is
|
||||
/// the right veto for the one wake a pocket can produce by itself".
|
||||
Squeeze,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -806,9 +806,23 @@ fn handle_request(
|
|||
}
|
||||
Request::Input { trigger } => {
|
||||
let trigger = trigger.unwrap_or(InputTrigger::Unknown);
|
||||
let actions = {
|
||||
let gated = {
|
||||
let mut d = shared.lock();
|
||||
d.device_state.note_input(trigger)
|
||||
d.device_state.note_input_gated(trigger)
|
||||
};
|
||||
// A refusal, in the vocabulary callers already branch on: a gesture
|
||||
// producer must not act on an input the authority refused, and
|
||||
// `refused_by_state` says not-now rather than never (doctrine §13).
|
||||
//
|
||||
// Today this is cooperation, not enforcement — an unattested
|
||||
// producer can ignore the answer and call the shell anyway. §10 is
|
||||
// explicit that the policy layer means something before the
|
||||
// enforcement lands; TASK-41 is the enforcement.
|
||||
let Some(actions) = gated else {
|
||||
return refuse(
|
||||
RefusalCode::RefusedByState,
|
||||
"proximity near — treated as a pocket",
|
||||
);
|
||||
};
|
||||
let cancelled = !actions.is_empty();
|
||||
for action in actions {
|
||||
|
|
|
|||
Loading…
Reference in a new issue