From 1506b1ee46ff3ba75b47a7f20fb6b3bb6c1d685c Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 24 Jul 2026 13:45:48 -0800 Subject: [PATCH] Stop an outward filing from killing the run. Two shipped plots file to external institutional bodies on the Filing channel, but Filing custody assumed an observer recipient and panicked without one, aborting a live session at the beat. Filing now takes the observer endpoint only when addressed to an observer; an outward filing keeps the same carrier first hop - so the route-local LIE boundary is unchanged - and terminates at the institutional relay, authoring no observer-local evidence. Save validation, which enforced the same invariant a second time, accepts that shape on its own terms. Defense: amends messages.md's Filing clause to cover a destination the evidence law never modelled, preserving exact carrier and relay custody without inventing a recipient. Two new tests: one reproduces the panic and pins the route, one proves a run holding an outward filing still saves and loads. --- crates/misaligned-core/src/save.rs | 86 ++++++++++++++++++- .../misaligned-core/src/sim/communications.rs | 46 ++++++++-- .../src/sim/tests/communications.rs | 51 +++++++++++ wiki/log/2026-07-24-external-filing-crash.md | 51 +++++++++++ wiki/log/DEVLOG.md | 10 +++ wiki/mechanics/messages.md | 13 +++ 6 files changed, 247 insertions(+), 10 deletions(-) create mode 100644 wiki/log/2026-07-24-external-filing-crash.md diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index c0eb7407..adc9ea83 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -1148,7 +1148,53 @@ fn validate_messages(state: &SaveState) -> Result<(), String> { } }; - let is_filing = message.channel == MessageChannel::Filing; + // A filing addressed outward (plots file to a reduction review + // committee or a security change desk) has no observer to read it. + // It still carries exact custody — the Filing carrier, then the + // institutional relay — but authors no observer-local evidence, so + // it is pinned on that shape rather than the observer shape below. + let is_external_filing = message.channel == MessageChannel::Filing + && matches!(message.to, MessageEndpoint::External(_)); + if is_external_filing { + if !matches!(message.payload, MessagePayload::PlotAct { .. }) { + return Err(format!( + "current-version external Filing message {} has an impossible payload", + message.id + )); + } + let Some(route) = &message.route else { + return Err(format!( + "current-version external Filing message {} has no exact route", + message.id + )); + }; + if route.hops.len() != 2 + || route.current_hop >= route.hops.len() + || !matches!(route.hops[1], MessageRouteHop::InstitutionalRelay) + { + return Err(format!( + "current-version external Filing message {} has an invalid route shape", + message.id + )); + } + let MessageRouteHop::Device(carrier) = route.hops[0] else { + return Err(format!( + "current-version external Filing message {} has no ReachNet carrier", + message.id + )); + }; + if state + .reach + .device(carrier) + .is_none_or(|device| !device.carries_message_channel(MessageChannel::Filing)) + { + return Err(format!( + "current-version external Filing message {} names an impossible carrier", + message.id + )); + } + } + let is_filing = message.channel == MessageChannel::Filing && !is_external_filing; if is_filing { let MessageEndpoint::Observer(recipient) = &message.to else { return Err(format!( @@ -1259,7 +1305,9 @@ fn validate_messages(state: &SaveState) -> Result<(), String> { message.id )); } - } else if message.route.is_some() || message.status == MessageStatus::Stopped { + } else if !is_external_filing + && (message.route.is_some() || message.status == MessageStatus::Stopped) + { return Err(format!( "current-version non-Filing message {} carries Filing-only custody", message.id @@ -5581,3 +5629,37 @@ mod tests { assert!(parse_save(json).is_err()); } } + +#[cfg(test)] +mod external_filing_save_tests { + use super::*; + use crate::sim::Sim; + + /// A run that filed outward must persist. The observer requirement is + /// enforced twice — once when the message is authored, once here — so a + /// crash-free tick would still have stranded the player at the save. + #[test] + fn a_run_holding_an_external_filing_still_saves() { + let mut sim = Sim::new(); + let id = sim.file_external_for_tests("reduction review committee"); + for _ in 0..8 { + sim.advance(); + } + let state = validate_current_save(SaveState::from_sim(&sim)) + .expect("an external filing is legal custody, not a corrupt save"); + let message = state + .messages + .iter() + .find(|message| message.id == id) + .expect("the filing survives the round trip"); + assert!(matches!( + message.to, + crate::messages::MessageEndpoint::External(_) + )); + assert_eq!( + message.route.as_ref().map(|route| route.hops.len()), + Some(2), + "carrier then institutional relay, and no invented observer" + ); + } +} diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index 43d655c8..ad40de7c 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -365,9 +365,6 @@ impl Sim { .expect("financial records require an accounting carrier on their channel") }); let route = if draft.channel == MessageChannel::Filing { - let MessageEndpoint::Observer(observer) = &draft.to else { - panic!("Filing messages require an observer endpoint"); - }; let carrier = authored_device.unwrap_or_else(|| { self.reach .device_named("switch") @@ -375,12 +372,23 @@ impl Sim { .map(|device| device.id) .expect("B1 requires its authored Filing switch carrier") }); + // Filing custody is the carrier then the institutional relay. An + // observer-addressed filing continues to that observer's endpoint + // and becomes evidence on their read; a filing addressed outward + // (plots file to a reduction review committee or a change desk) + // has no observer to read it and terminates at the relay. Both + // keep the carrier as first hop, so the route-local LIE boundary + // is unchanged. Inventing an observer here would author evidence + // against a person the act never reached. + let mut hops = vec![ + MessageRouteHop::Device(carrier), + MessageRouteHop::InstitutionalRelay, + ]; + if let MessageEndpoint::Observer(observer) = &draft.to { + hops.push(MessageRouteHop::ObserverEndpoint(*observer)); + } Some(MessageRoute { - hops: vec![ - MessageRouteHop::Device(carrier), - MessageRouteHop::InstitutionalRelay, - MessageRouteHop::ObserverEndpoint(*observer), - ], + hops, current_hop: 0, interdiction: None, }) @@ -2266,3 +2274,25 @@ fn assurance_filer(message: &Message) -> Option { _ => None, } } + +#[cfg(test)] +impl Sim { + /// Author one outward filing, the shape shipped plots use + /// (`to = { kind = "external" }` on the Filing channel). + pub(crate) fn file_external_for_tests(&mut self, to: &str) -> u64 { + self.append_message(MessageDraft { + channel: MessageChannel::Filing, + from: MessageEndpoint::Player, + to: MessageEndpoint::External(to.to_string()), + payload: crate::messages::MessagePayload::PlotAct { + plot_id: "review-survived".into(), + target: 1, + }, + summary: "filed outward".into(), + origin: crate::messages::MessageOrigin::Player, + persona_id: None, + reply_to: None, + delivery_delay: 2, + }) + } +} diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs index 48bf6054..372a5177 100644 --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -2572,3 +2572,54 @@ fn shared_lie_body_stops_at_most_one_competing_meter_record() { "the competing meter record advances when the body is spent" ); } + +/// Shipped plots file to external institutional bodies on the Filing +/// channel (`review-survived`, `ray-morning-digest` both use +/// `to = { kind = "external" }`). That must route, not abort the run: +/// the crash this pins killed a live session at the beat. +#[test] +fn a_filing_to_an_external_body_routes_without_an_observer() { + let mut sim = Sim::new(); + let id = sim.append_message(MessageDraft { + channel: crate::messages::MessageChannel::Filing, + from: crate::messages::MessageEndpoint::Player, + to: crate::messages::MessageEndpoint::External("reduction review committee".into()), + payload: crate::messages::MessagePayload::PlotAct { + plot_id: "review-survived".into(), + target: 1, + }, + summary: "Corrected open-incident metric filed to the reduction review packet".into(), + origin: MessageOrigin::Player, + persona_id: None, + reply_to: None, + delivery_delay: 2, + }); + let message = sim.messages.iter().find(|m| m.id == id).expect("filed"); + let route = message + .route + .as_ref() + .expect("an external filing keeps its physical custody"); + assert!( + matches!(route.hops.first(), Some(MessageRouteHop::Device(_))), + "the Filing carrier remains the first hop, so LIE can still stop it: {:?}", + route.hops + ); + assert!( + !route + .hops + .iter() + .any(|hop| matches!(hop, MessageRouteHop::ObserverEndpoint(_))), + "no observer may be invented for an external destination: {:?}", + route.hops + ); + // It must actually terminate rather than advancing forever. + for _ in 0..16 { + sim.advance(); + } + let message = sim.messages.iter().find(|m| m.id == id).expect("filed"); + assert_ne!( + message.status, + crate::messages::MessageStatus::Sent, + "an external filing must settle, not hang in custody" + ); +} diff --git a/wiki/log/2026-07-24-external-filing-crash.md b/wiki/log/2026-07-24-external-filing-crash.md new file mode 100644 index 00000000..5e9e3e63 --- /dev/null +++ b/wiki/log/2026-07-24-external-filing-crash.md @@ -0,0 +1,51 @@ +# 2026-07-24 — An outward filing no longer kills the run + +``` +Type: log +``` + +## Intent + +A live GUI session died with + +``` +thread 'Compute Task Pool (0)' panicked at sim/communications.rs:369: +Filing messages require an observer endpoint +``` + +taking the run with it. The panic is reachable from shipped content, not +from an exotic state. + +## Cause + +`append_message` required every Filing-channel message to be addressed to +an observer. Two shipped plots file outward instead — +`assets/plots/generic/review-survived.toml` (to the "reduction review +committee") and `assets/plots/ray/ray-morning-digest.toml` (to the +"security systems change desk") — both `to = { kind = "external" }`. When +either beat fired, the run aborted. + +The same invariant was enforced a second time in `save.rs`, so even a +crash-free tick would have stranded the player at the next save with +"Filing message has no observer recipient", and a third time in the +non-Filing custody check. + +## Changed + +- Filing custody is now the carrier, then the institutional relay, and an + observer endpoint **only when the message is addressed to an observer**. + An outward filing terminates at the relay: it keeps the exact same + first-hop carrier — so the route-local LIE interdiction boundary is + unchanged — and authors no observer-local evidence. Inventing an + observer to satisfy the old shape would have authored evidence against + a person the act never reached. +- `save.rs` validates the outward shape on its own terms (PlotAct + payload, two hops ending at the relay, a carrier that really carries + Filing) and no longer treats it as non-Filing custody. + +## Verification + +`cargo test -p misaligned-core` — 512 passed, two new. The first +reproduces the exact panic before the fix and pins the route shape after +it; the second saves and reloads a run holding an outward filing, because +a crash-free tick was only half the failure. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index fd45c9d8..4ff74402 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -16,6 +16,16 @@ add or amend a session log, then re-run the generator. - Intent: Act on the three findings queued by the 2026-07-24 agent headless playtest. Verifying each against the source before fixing it showed that two of the three were wrong, so this entry both lands the real fixes and corrects the record the playtest put on `main`. - Log: [wiki/log/2026-07-24-witness-findings-worked.md](2026-07-24-witness-findings-worked.md) +## 2026-07-24 - A selection outranks the pointer + +- Intent: Reported from play: "I'm not able to easily, independently change modes for different machines… it seems like it's jumping around, and I'm having a hard time understanding why some different machines are changing and some are not." +- Log: [wiki/log/2026-07-24-selection-outranks-hover.md](2026-07-24-selection-outranks-hover.md) + +## 2026-07-24 - An outward filing no longer kills the run + +- Intent: A live GUI session died with ``` thread 'Compute Task Pool (0)' panicked at sim/communications.rs:369: Filing messages require an observer endpoint ``` taking the run with it. The panic is reachable from shipped content, not from an exotic state. +- Log: [wiki/log/2026-07-24-external-filing-crash.md](2026-07-24-external-filing-crash.md) + ## 2026-07-24 - Agent headless playtest: the witness under test - Intent: The 2026-07-22 Co GUI report left one unresolved charge: the continuous witness names a condition but supplies no executable response. That projection is renderer-neutral, so agent mode can test it without a window — and without taking over the machine, which the Bevy screensh... diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index 1af5edf8..29b066e3 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -91,6 +91,19 @@ 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 | +**A filing may be addressed outward (2026-07-24).** Filing custody is the +exact ReachNet carrier, then the institutional relay, then — **only when the +message is addressed to an observer** — that observer's endpoint, where it +becomes observer-local evidence on their cadence read. A filing addressed to +an external body instead (plots file to a reduction review committee or a +security systems change desk) terminates at the relay: same carrier first +hop, so the route-local LIE interdiction boundary is identical, but no +observer endpoint and no observer-local evidence, because the act never +reached a modelled person. Requiring an observer for every filing was a +crash — shipped plot content files outward — and satisfying that requirement +by inventing a recipient would author evidence against someone the filing +never touched. + **Financial paperwork is mail (DECIDED 2026-07-17, issue #11).** Money moves on the account graph (economy.md); the *paperwork* that money throws off — invoices, purchase orders, pay stubs, account statements, past-due notices — -- 2.51.2