From 45032a43d498af3321de52a8921b0d9256d629dd Mon Sep 17 00:00:00 2001 From: RobVanProd Date: Tue, 11 Aug 2026 20:41:27 -0400 Subject: [PATCH] Boot with demo mode off so the character can actually sleep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IntentEngine::demoEnabled_ defaulted to true and nothing in any release path turned it off, so every shipped image injects a random mode change and a synthetic event every 2.5-6 seconds. Two costs. The character looks random rather than responsive: mode flips between Idle, Attend, Think and React for no reason, and the body light follows. And because every injected event counts as stimulus, sleep pressure can never accumulate, so the documented drowsiness ladder -- heavy lids at fatigue 0.45, yawning at 0.62, asleep at 0.80 after roughly 8.5 idle minutes -- cannot run at all. The reference robot was awake for 5.9 continuous days, and its mode was observed cycling Idle → Attending → Thinking → Listening every few seconds with nobody in the room. docs/BRIDGE_AI_HANDOFF.md already warns to send `demo off` before drawing any conclusion about behaviour, and notes that a large amount of apparently random character behaviour turned out to be this. The native suite says the same thing structurally: every test covering sleep, idle life, or lingering attention calls setDemoEnabled(false) first, so the behaviour under test was never the shipped default. Boot from STACKCHAN_DEMO_ENABLED_AT_BOOT, defaulting to 0. The serial `demo on` command still enables it for a bench demonstration, and a build can override the default if it wants demo behaviour. New coverage asserts the boot state is off, that nothing injects a mode change across thirty idle seconds, and that the engine reaches sleep without any test first having to disable demo mode. The existing enable/disable test now turns demo on explicitly rather than relying on the old default. pio test -e native_logic: 317/317. Co-Authored-By: Claude Opus 5 --- src/persona/IntentEngine.cpp | 2 +- src/persona/IntentEngine.hpp | 18 +++++++++++++++++- test/test_native_logic/test_main.cpp | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/persona/IntentEngine.cpp b/src/persona/IntentEngine.cpp index 17cbc772..4f0d7839 100644 --- a/src/persona/IntentEngine.cpp +++ b/src/persona/IntentEngine.cpp @@ -28,7 +28,7 @@ void IntentEngine::begin() { activeSpeechUntilMs_ = 0; soundOrientUntilMs_ = 0; lastEventAtMs_ = lastUpdateMs_; - demoEnabled_ = true; + demoEnabled_ = STACKCHAN_DEMO_ENABLED_AT_BOOT != 0; reducedMotion_ = false; soundAzimuthNorm_ = 0.0f; lastEventStrength_ = 0.0f; diff --git a/src/persona/IntentEngine.hpp b/src/persona/IntentEngine.hpp index a78429f8..bc00ea7e 100644 --- a/src/persona/IntentEngine.hpp +++ b/src/persona/IntentEngine.hpp @@ -9,6 +9,22 @@ #include "persona/SpeechPlanner.hpp" #include "persona/StateMatrix.hpp" +// Demo mode injects a random mode change and a synthetic event every 2.5-6 s. +// That is useful on a bench when showing the face off, and actively harmful the +// rest of the time: it makes the character look random rather than responsive, +// and because every injected event counts as stimulus the robot can never +// accumulate enough drowsiness to fall asleep. The character design has him +// getting heavy-lidded at fatigue 0.45, yawning at 0.62 and asleep at 0.80 after +// roughly 8.5 idle minutes, and none of that can happen while demo mode runs. +// +// Every native test covering sleep, idle life, or character behaviour calls +// setDemoEnabled(false) first for exactly this reason, so the behaviour under +// test was never the shipped default. Boot with it off; `demo on` over serial +// still turns it on for a bench demonstration. +#ifndef STACKCHAN_DEMO_ENABLED_AT_BOOT +#define STACKCHAN_DEMO_ENABLED_AT_BOOT 0 +#endif + namespace stackchan { class IntentEngine { @@ -74,7 +90,7 @@ class IntentEngine { uint32_t activeSpeechUntilMs_ = 0; uint32_t soundOrientUntilMs_ = 0; uint32_t lastEventAtMs_ = 0; - bool demoEnabled_ = true; + bool demoEnabled_ = STACKCHAN_DEMO_ENABLED_AT_BOOT != 0; bool reducedMotion_ = false; float soundAzimuthNorm_ = 0.0f; float lastEventStrength_ = 0.0f; diff --git a/test/test_native_logic/test_main.cpp b/test/test_native_logic/test_main.cpp index ac2160d7..00a329d2 100644 --- a/test/test_native_logic/test_main.cpp +++ b/test/test_native_logic/test_main.cpp @@ -912,6 +912,7 @@ void test_intent_engine_emits_deduped_speech_cue_on_external_event() { void test_intent_engine_demo_can_be_disabled_and_resumed() { IntentEngine engine; engine.begin(); + engine.setDemoEnabled(true, 0); TEST_ASSERT_TRUE(engine.isDemoEnabled()); engine.setDemoEnabled(false, 0); @@ -1280,6 +1281,32 @@ CharacterMode runUntilAsleep(IntentEngine& engine, uint32_t limitMs, uint32_t* a return mode; } +// Demo mode injects a synthetic event every 2.5-6 s, and every injected event is +// stimulus, so with it on the robot can never accumulate sleep pressure. Shipping +// it enabled meant the documented drowsiness ladder could not run on a real +// robot: one observed device stayed awake for 5.9 days. +void test_intent_engine_boots_with_demo_off_so_the_character_can_sleep() { + IntentEngine engine; + engine.begin(); + TEST_ASSERT_FALSE(engine.isDemoEnabled()); + + // Left alone, nothing injects a mode change. With demo mode on this loop sees + // Think/Attend/React appear within the first few seconds. + for (uint32_t nowMs = 1000; nowMs <= 30000; nowMs += 1000) { + const RobotFrame frame = engine.update(nowMs); + TEST_ASSERT_EQUAL(static_cast(CharacterMode::Idle), static_cast(frame.mode)); + } + + // And drowsiness accumulates instead of being reset by injected stimulus, so + // he reaches sleep the way the character design describes. Every other sleep + // test has to call setDemoEnabled(false) first to get here; this one must not + // need to, because that is now the boot state. + uint32_t asleepAtMs = 0; + runUntilAsleep(engine, 900000u, &asleepAtMs); + TEST_ASSERT_TRUE(engine.isAsleep()); +} + + } // namespace void test_sleep_pressure_builds_only_when_left_alone() { @@ -9418,6 +9445,7 @@ int main() { RUN_TEST(test_robot_frame_carries_character_mode_for_renderer); RUN_TEST(test_robot_frame_carries_speech_cue_for_output_adapters); RUN_TEST(test_intent_engine_emits_deduped_speech_cue_on_external_event); + RUN_TEST(test_intent_engine_boots_with_demo_off_so_the_character_can_sleep); RUN_TEST(test_intent_engine_demo_can_be_disabled_and_resumed); RUN_TEST(test_idle_life_breathing_moves_face_and_body_together); RUN_TEST(test_persona_behavior_codegen_exposes_idle_life_tuning);