diff --git a/wiki/log/2026-07-17-intel-buffer-nag.md b/wiki/log/2026-07-17-intel-buffer-nag.md new file mode 100644 --- /dev/null +++ b/wiki/log/2026-07-17-intel-buffer-nag.md @@ -0,0 +1,29 @@ +# Intel buffer warns once per overflow episode + +``` +Type: log +``` + +Last open finding from the 2026-07-15 Fable playtest (finding 7): a full +intel buffer that kept receiving arrivals logged one "Intel buffer full" +line per dropped recording — the playtest saw five near-identical lines in +one stretch, the repeated-nag smell the attention economy forbids. + +Fix: a runtime-only `Sim::intel_overflow_warned` flag. The buffer +announces the loss once when it begins dropping (one generic line, since +naming one arbitrary dropped item among many was noise), stays quiet while +it remains full, and announces again only after processing makes room and +it refills — a fresh episode earns a fresh line. The flag is not +persisted (it lives on `Sim`, not `SaveState`), so no save format change; +a loaded run re-announces on its next drop. Ongoing pressure is still +carried by the buffer nudge, unchanged. + +Pinned by `buffer_overflow_warns_once_per_episode_not_once_per_drop`: ten +drops in one stretch produce one warning, and refilling to overflow after +processing one produces a second. Lib gate green; an observed run +confirmed no spam with the real save untouched. + +Defense: wiki/mechanics/intel.md's bounded-buffer clause, amended to +"one legible log line per overflow episode" — the buffer must still tell +the player it is losing data (compute scarcity felt), but once per +episode, not once per item. No save or balance change. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -41,6 +41,11 @@ - Intent: (see session log) - Log: [wiki/log/2026-07-17-intel-magnitude-altitude.md](2026-07-17-intel-magnitude-altitude.md) +## 2026-07-17 - Intel buffer warns once per overflow episode + +- Intent: (see session log) +- Log: [wiki/log/2026-07-17-intel-buffer-nag.md](2026-07-17-intel-buffer-nag.md) + ## 2026-07-17 - Financial traffic waits on graph ownership - Intent: (see session log) diff --git a/wiki/mechanics/intel.md b/wiki/mechanics/intel.md --- a/wiki/mechanics/intel.md +++ b/wiki/mechanics/intel.md @@ -83,8 +83,12 @@ Recording is free (the instrument does the work). The buffer is **bounded** [TUNE capacity]: when full, the oldest *unprocessed* events -drop, with a legible log line. Compute scarcity is felt as "I could not -afford to digest everything I collected," never as a missed click. +drop, with **one legible log line per overflow episode** — announced when +the buffer begins dropping and again only after processing makes room and +it refills, never once per dropped item (playtest finding, 2026-07-15). +Ongoing pressure is carried by the buffer nudge, not by a repeated log +line. Compute scarcity is felt as "I could not afford to digest everything +I collected," never as a missed click. Documents are the same pipeline with a physical source: reading the Storage B records (via an asset task or later physical access) deposits diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -859,12 +859,16 @@ if self.intel_buffer.len() >= Self::INTEL_BUFFER_CAPACITY { let dropped = self.intel_buffer.remove(0); self.cancel_process_sinks_for(dropped.id); - self.push_log(format!( - "Intel buffer full: dropped {} from {} at tick {}. Open Operations INTEL (I) and review waiting recordings.", - dropped.opaque_label(), - dropped.feed, - dropped.tick - )); + // Announce the loss once per overflow episode, not once per dropped + // item (playtest finding 7): the flag clears when processing makes + // room, so re-filling to capacity announces again. + if !self.intel_overflow_warned { + self.push_log( + "Intel buffer full: dropping the oldest unreviewed recordings. \ + Open Operations INTEL (I) and review before more is lost.", + ); + self.intel_overflow_warned = true; + } } let id = event.id; let auto_review = self.auto_review_enabled_for_event(&event); @@ -929,6 +933,10 @@ return false; }; let raw = self.intel_buffer.remove(idx); + // Processing made room: the next overflow episode announces afresh. + if self.intel_buffer.len() < Self::INTEL_BUFFER_CAPACITY { + self.intel_overflow_warned = false; + } // Feed evidence returns to the instrument that actually captured it. // Non-device records may still resolve to a currently seen person or // an honestly recorded visual room. diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -414,6 +414,11 @@ pub heard_events: Vec, /// Opaque, unprocessed recordings captured from subscribed feeds. pub intel_buffer: Vec, + /// Runtime-only: true once the bounded buffer has begun dropping the + /// oldest recordings, so the loss is announced once per overflow episode + /// rather than once per dropped item (playtest 2026-07-15 finding 7). + /// Not persisted; a loaded run re-announces on its next drop. + pub(crate) intel_overflow_warned: bool, /// Durable processed intel with provenance. pub intel: Vec, /// Canonical compacted custody for repeated routine output. Exact @@ -664,6 +669,7 @@ remembered: HashMap::new(), heard_events: Vec::new(), intel_buffer: Vec::new(), + intel_overflow_warned: false, intel: Vec::new(), intel_streams: Vec::new(), intel_policies: IntelPolicyLedger::default(), diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -388,6 +388,56 @@ } #[test] +fn buffer_overflow_warns_once_per_episode_not_once_per_drop() { + // Playtest finding 7: a full buffer that keeps receiving arrivals used to + // log one "buffer full" line per dropped item. It now warns once when the + // episode begins, and again only after processing makes room and it + // refills. + let mut sim = Sim::new(); + ensure_ops_executor(&mut sim); + let push = |sim: &mut Sim| { + sim.record_raw_intel( + "test-feed", + Some("server_room".into()), + 0, + 0, + Some(0), + RawIntelKind::Presence { entered: true }, + ); + }; + + // Fill to capacity, then drop ten more in one stretch with no processing. + for _ in 0..(Sim::INTEL_BUFFER_CAPACITY + 10) { + push(&mut sim); + } + let warnings = |sim: &mut Sim| { + sim.drain_log() + .into_iter() + .filter(|l| l.contains("Intel buffer full")) + .count() + }; + assert_eq!( + warnings(&mut sim), + 1, + "ten drops in one episode produce exactly one warning" + ); + + // Process one recording to make room, then overflow again: a fresh episode + // earns a fresh warning. + sim.review_recordings(); + finish_ops(&mut sim); + let _ = sim.drain_log(); + for _ in 0..3 { + push(&mut sim); + } + assert_eq!( + warnings(&mut sim), + 1, + "refilling to overflow after making room warns again" + ); +} + +#[test] fn processed_sightings_stage_schedule_and_buffer_is_bounded() { let mut sim = Sim::new(); ensure_ops_executor(&mut sim);