diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index e793470f..d92095ee 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -117,9 +117,11 @@ impl Sim { match &msg.to { MessageEndpoint::Player | MessageEndpoint::External(_) => true, MessageEndpoint::Person(id) => match msg.channel { - MessageChannel::Email | MessageChannel::Phone => { - self.person_room_at_tick(*id, tick).is_some() - } + MessageChannel::Email => self.person_room_at_tick(*id, tick).is_some(), + // Phone reachability is not workplace presence. B1 has no + // separate sleep-state model, so an existing person can read + // a phone message anywhere, including while off-site. + MessageChannel::Phone => self.people.get(*id).is_some(), MessageChannel::InPerson => match msg.from.person() { Some(from) => { self.person_room_at_tick(*id, tick).is_some() diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs index caa2e73c..a45de33b 100644 --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -1,5 +1,94 @@ +use super::super::communications::MessageDraft; use super::*; use crate::intel::{IntelKind, IntelPolicyMatch, IntelPolicyOutcome, IntelRoutineClass}; +use crate::messages::MessageOrigin; + +#[test] +fn phone_reads_off_site_while_email_waits_for_a_work_block() { + let mut sim = Sim::with_seed(6); + sim.tick = (2 * Sim::DAY_TICKS / 24) + 1; + assert_eq!( + sim.person_room_at_tick(1, sim.tick), + None, + "Dana is off-site at 02:00" + ); + + let phone_id = sim.append_message(MessageDraft { + channel: MessageChannel::Phone, + from: MessageEndpoint::Player, + to: MessageEndpoint::Person(1), + payload: MessagePayload::Note { + label: "phone check".into(), + }, + summary: "phone check".into(), + origin: MessageOrigin::Player, + persona_id: None, + reply_to: None, + delivery_delay: 1, + }); + let email_id = sim.append_message(MessageDraft { + channel: MessageChannel::Email, + from: MessageEndpoint::Player, + to: MessageEndpoint::Person(1), + payload: MessagePayload::Note { + label: "email check".into(), + }, + summary: "email check".into(), + origin: MessageOrigin::Player, + persona_id: None, + reply_to: None, + delivery_delay: 1, + }); + + let projected = sim.recent_message_lines_for_person(1, 5); + let delivery_tick = sim.tick + 1; + assert!( + projected.iter().any(|line| { + line.contains("phone check") && line.contains(&format!("next read t{delivery_tick}")) + }), + "Phone projects the delivery tick as its off-site read window: {projected:?}" + ); + assert!( + projected.iter().any(|line| { + line.contains("email check") && !line.contains(&format!("next read t{delivery_tick}")) + }), + "Email still waits for Dana's next work block: {projected:?}" + ); + + run(&mut sim, 2); + assert_eq!(sim.person_room_at_tick(1, sim.tick), None); + assert_eq!( + sim.messages + .iter() + .find(|m| m.id == phone_id) + .unwrap() + .status, + MessageStatus::Read, + "Phone reads without borrowing Email's scheduled-room gate" + ); + assert_eq!( + sim.messages + .iter() + .find(|m| m.id == email_id) + .unwrap() + .status, + MessageStatus::Delivered, + "Email remains delivered but unread while Dana is off-site" + ); + + while sim.tick < (10 * Sim::DAY_TICKS / 24) + 2 { + sim.advance(); + } + assert_eq!( + sim.messages + .iter() + .find(|m| m.id == email_id) + .unwrap() + .status, + MessageStatus::Read, + "Email reads after Dana reaches her scheduled desk block" + ); +} #[test] fn player_messages_land_at_read_time_and_roundtrip() { diff --git a/wiki/log/2026-07-17-phone-offsite-read.md b/wiki/log/2026-07-17-phone-offsite-read.md new file mode 100644 index 00000000..d4a01068 --- /dev/null +++ b/wiki/log/2026-07-17-phone-offsite-read.md @@ -0,0 +1,35 @@ +# Phone does not require workplace presence + +``` +Type: log +``` + +Autonomy tick 69 took the oldest queued finding from the messages audit and +re-verified it against current main. The binding channel table said Phone +could be read at any location including off-site, while +`Sim::read_condition_at` grouped Phone with Email and required the recipient +to occupy a scheduled workplace room. A person leaving work therefore became +unable to answer a phone until their next shift. + +The shared delivery path now keeps the channels distinct. Email still waits +for the recipient's next authored work block. Phone treats an existing person +as reachable regardless of room. The spec makes the B1 boundary explicit: +there is no separate sleep or silence state yet, and a later availability +model must be authored per person rather than inferred from workplace +presence. + +`phone_reads_off_site_while_email_waits_for_a_work_block` sends both channels +to off-site Dana at 02:00. It pins the distinct projected next-read windows, +the Phone transition to Read while she remains off-site, the Email transition +remaining Delivered, and the later Email read after her scheduled desk block. +The existing player-message round-trip test continues to pin Email effects at +read time. No frontend-specific timer or state was added; PEOPLE consumes the +same exact scheduler projection repaired here. + +The independent Financial-channel table contradiction remains in the findings +queue. This tick acts on one violation only. + +Defense: `wiki/mechanics/messages.md` defines channel-specific read conditions +and explicitly permits Phone reads off-site. Splitting Phone from Email at the +one shared read-condition boundary restores that contract without a cast +special case or a second clock. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 80d4df4d..9cbc3dc1 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -21,6 +21,11 @@ add or amend a session log, then re-run the generator. - Intent: Harvest the persistent 2026-07-15 finding in `wiki/vision/simulation-laws.md`: the law required every player-visible art or text stand-in to be recorded under `wiki/art/`, but no art page actually owned that register. - Log: [wiki/log/2026-07-17-placeholder-registry.md](2026-07-17-placeholder-registry.md) +## 2026-07-17 - Phone does not require workplace presence + +- Intent: (see session log) +- Log: [wiki/log/2026-07-17-phone-offsite-read.md](2026-07-17-phone-offsite-read.md) + ## 2026-07-17 - PEOPLE owns the message thread - Intent: (see session log) diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index 351fae2f..59c0e01a 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -49,6 +49,12 @@ defines where and when a message can be read: | In person | co-location | both parties in the same room | | Filing | the institutional channel | the receiving role's next sampling cadence | +At B1, **awake means reachable**: there is no separate per-person sleep or +phone-silence state. A Phone message therefore reads at its scheduled Read +event wherever the recipient is, including off-site; it must not borrow +Email's workplace-presence gate. A later sleep/availability model must be +authored per person rather than inferred from whether they are at work. + **Delivery is on the recipient's clock.** A message sent to Dana at 02:00 sits unread until her next desk block; her reply comes back on a per-person response distribution [TUNE]. No special cases: "Dana @@ -136,8 +142,10 @@ private message from the authored schedule. 1. Channels exist with read conditions per the table; a message to an off-shift recipient is delivered but unread until their next - qualifying block; effects (disposition, obligation, deceive rolls) - apply at read time, not send time (test with Dana at 02:00). + qualifying block when its channel requires one; effects (disposition, + obligation, deceive rolls) apply at read time, not send time. At 02:00, + Dana reads a delivered Phone message while off-site, but a delivered Email + waits for her next desk block. 2. Replies return on a per-person response distribution riding their schedule; save/load round-trips in-flight messages. 3. Authored traffic exists for all five cast members per their specs; @@ -164,6 +172,8 @@ private authored pattern. `sim::tests::communications::player_messages_land_at_r pins both message directions and proves unrelated person-to-external traffic cannot leak into that thread. `schedule::tests::next_tick_for_projects_one_matching_payload_without_draining` pins the read-only scheduler projection used by the explanation. +`sim::tests::communications::phone_reads_off_site_while_email_waits_for_a_work_block` +pins the distinct channel gates and their projected read windows. ## Implementation notes diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 8196fd93..fda8111c 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -55,7 +55,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/compute.md` | 2026-07-12 | finding | [log](../log/2026-07-12-compute-graduation.md) | | retired Operations runtime identifiers | 2026-07-11 | finding | [log](../log/2026-07-11-retired-runtime-identifier-gate.md) | | `wiki/mechanics/reach.md` + `building.md` | 2026-07-15 | finding | re-verified the queued reverse-endpoint duplicate: declaration compared canonical stored endpoints to the raw request tuple; canonicalized proposal identity at the comparison boundary and pinned reverse-order rejection plus cancellation/reproposal — [log](../log/2026-07-15-canonical-build-intent-endpoints.md) | -| `wiki/mechanics/messages.md` | 2026-07-17 | finding | delivery, typed payload, authored traffic, capture, filing, and processing paths verified; repaired the missing shared PEOPLE thread/learned-traffic surface, made direct-message direction explicit, derived next-read timing from the exact scheduler event, and excluded uncaptured private traffic — [log](../log/2026-07-17-people-message-thread.md) | +| `wiki/mechanics/messages.md` | 2026-07-17 | finding | re-verified the queued Phone violation; Phone now reads while the recipient is off-site instead of borrowing Email's workplace gate, with exact status/read-window regression coverage. The independent Financial-channel table contradiction remains queued — [log](../log/2026-07-17-phone-offsite-read.md) | | `wiki/mechanics/sim-mechanics.md` | 2026-07-11 | finding | [log](../log/2026-07-11-tick-sim-no-dockets.md) | | `wiki/mechanics/detection.md` | 2026-07-11 | finding | [log](../log/2026-07-11-tick-detection-filings-messages.md) | | `wiki/engineering/env.md` | 2026-07-15 | finding | re-verified the queued DIGITAL false failure: the wrapper's four-kind dialect copy omitted newer DIGITAL scenarios; the frontend now reports its staged RenderMode and the wrapper gates fog from that one truth, with fake-binary fixtures for DIGITAL, REAL, and malformed evidence — [log](../log/2026-07-15-bevy-shot-dialect-proof.md) | @@ -76,5 +76,4 @@ Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. -- 2026-07-17 · violation · `wiki/mechanics/messages.md` · Phone promises off-site reading while `read_condition_at` currently requires a scheduled room for both Phone and Email; split and pin the distinct channel gate. - 2026-07-17 · contradiction · `wiki/mechanics/messages.md` · Runtime carries a fifth `Financial` message channel with unconditional read behavior, but the binding channel table names only Email, Phone, InPerson, and Filing.