From 81354c42eafaedabe01ecae59b51ddb38112aebe Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 15:46:08 +0200 Subject: [PATCH 1/9] fix(autopilot): upload the hard crash and stop false app-hangs Two issues surfaced by the scheduled run-demo jobs: - The headline "Convoluted Chain" crash was captured but never reached Sentry. The crash upload mode was async, so on a one-shot CI run the daemon was torn down with the job before it finished uploading (and nothing relaunches to flush it). Add SentryConfig::crash_upload_sync and set it for the headless runner so the process blocks until the daemon has sent the minidump, then exits non-zero. - The autopilot produced spurious "(anonymous namespace)::sleep_ms" app hangs that didn't group (different durations). The loop only fed the app-hang watchdog once per iteration, so a slow backend call plus the 1.5s pace wait exceeded the 2s threshold. Heartbeat after every step, wait via a heartbeating idle(), and widen the headless app-hang timeout to 4s. Now only the deliberate 8s app-hang scenario trips the watchdog, and it groups under its "app-hang" fingerprint. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/core/sentry_manager.cpp | 4 +++- src/core/sentry_manager.h | 3 +++ src/headless/main.cpp | 29 ++++++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/core/sentry_manager.cpp b/src/core/sentry_manager.cpp index edf3f33..63c3aad 100644 --- a/src/core/sentry_manager.cpp +++ b/src/core/sentry_manager.cpp @@ -184,7 +184,9 @@ bool SentryManager::init(const SentryConfig& config) { sentry_options_set_crash_reporting_mode( options, SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP); sentry_options_set_minidump_mode(options, SENTRY_MINIDUMP_MODE_SMART); - sentry_options_set_crash_upload_mode(options, SENTRY_CRASH_UPLOAD_MODE_ASYNC); + sentry_options_set_crash_upload_mode(options, + config.crash_upload_sync ? SENTRY_CRASH_UPLOAD_MODE_SYNC + : SENTRY_CRASH_UPLOAD_MODE_ASYNC); // --- Performance, logs, metrics, sessions ------------------------------ sentry_options_set_traces_sample_rate(options, config.traces_sample_rate); diff --git a/src/core/sentry_manager.h b/src/core/sentry_manager.h index 3db8298..934d034 100644 --- a/src/core/sentry_manager.h +++ b/src/core/sentry_manager.h @@ -32,6 +32,9 @@ struct SentryConfig { double traces_sample_rate = 1.0; // App-hang/ANR threshold in milliseconds (kept short for a snappy demo). int app_hang_timeout_ms = 2000; + // Block the crashing process until the daemon finishes uploading the crash. + // Needed for one-shot runs (CI) where nothing relaunches to flush it. + bool crash_upload_sync = false; }; class SentryManager { diff --git a/src/headless/main.cpp b/src/headless/main.cpp index a8017fa..a0385e8 100644 --- a/src/headless/main.cpp +++ b/src/headless/main.cpp @@ -36,6 +36,18 @@ void sleep_ms(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } +// Waits while keeping the app-hang watchdog fed, so the autopilot's own pacing +// is never mistaken for a hang (only the deliberate app-hang scenario blocks +// without a heartbeat). +void idle(int ms) { + const int step = 150; + for (int elapsed = 0; elapsed < ms; elapsed += step) { + empower::SentryManager::app_hang_heartbeat(); + sleep_ms(ms - elapsed < step ? ms - elapsed : step); + } + empower::SentryManager::app_hang_heartbeat(); +} + // One simulated pipeline run as a performance transaction with child spans, // plus a metric and a structured log - the steady-state demo data. void run_pipeline(const char* name, const char* op) { @@ -85,6 +97,10 @@ int main(int argc, char** argv) { empower::SentryConfig cfg; cfg.environment = env_or("SENTRY_ENVIRONMENT", "ci"); cfg.component = "headless"; + cfg.crash_upload_sync = true; // one-shot run: upload the crash before exit + // Headroom over normal pacing (incl. a slow backend call) so only the + // deliberate 8s app-hang scenario trips the watchdog, not the autopilot. + cfg.app_hang_timeout_ms = 4000; cfg.debug = env_or("EMPOWER_DEBUG", "")[0] != '\0'; if (!empower::SentryManager::init(cfg)) { std::fprintf(stderr, "headless: sentry init failed (continuing)\n"); @@ -139,10 +155,17 @@ int main(int argc, char** argv) { bool fired_hang = false; int iter = 0; while (std::chrono::steady_clock::now() < end) { + empower::SentryManager::app_hang_heartbeat(); run_pipeline("sensor.pipeline", "device.ingest"); empower::SentryManager::app_hang_heartbeat(); - if (iter % 3 == 0) empower::checkout("", &console); // distributed trace (no error event) - if (iter % 4 == 0) run_pipeline("image.processing", "image.classify"); + if (iter % 3 == 0) { + empower::checkout("", &console); // distributed trace (no error event) + empower::SentryManager::app_hang_heartbeat(); + } + if (iter % 4 == 0) { + run_pipeline("image.processing", "image.classify"); + empower::SentryManager::app_hang_heartbeat(); + } sentry_value_t online_attrs = sentry_value_new_object(); sentry_value_set_by_key(online_attrs, "fleet_size", sentry_value_new_attribute( @@ -166,7 +189,7 @@ int main(int argc, char** argv) { fired_hang = true; } ++iter; - sleep_ms(1500); + idle(1500); } if (final_crash) { // Event 3: the deterministic headline crash, so every CI run yields From e17a1cf96d890db36cd4ff4692c07305b30d2d5e Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 15:52:45 +0200 Subject: [PATCH 2/9] ci(run-demo): pull the build from the same branch the workflow runs on So a manual dispatch on a feature branch exercises that branch's binary, and scheduled master runs keep using master builds. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/run-demo.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-demo.yml b/.github/workflows/run-demo.yml index cbb4c24..10457dc 100644 --- a/.github/workflows/run-demo.yml +++ b/.github/workflows/run-demo.yml @@ -35,6 +35,9 @@ jobs: with: workflow: ci.yml workflow_conclusion: success + # Use the build from the same branch this workflow runs on, so a + # manual dispatch on a feature branch tests that branch's binary. + branch: ${{ github.ref_name }} name: empower-native-${{ matrix.name }} path: artifact From b5e828777ef6fd899c626841bb29fa11b0fc9f0b Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 16:03:02 +0200 Subject: [PATCH 3/9] fix(autopilot): bound backend call + guarantee crash upload - Cap the backend checkout call to 3s (curl + WinHTTP) and widen the headless app-hang threshold to 6s, so a slow Flask response can no longer be mistaken for a UI hang. This removes the spurious app-hang that was grouping into the backend-error issue ("checkout failed / App hung"). - run-demo: relaunch the binary once after the autopilot crash so the native backend uploads any crash still on disk (belt-and-suspenders with the sync upload). Temporarily enable EMPOWER_DEBUG to verify in CI. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/run-demo.yml | 8 +++++++- src/core/backend_client.cpp | 7 ++++++- src/headless/main.cpp | 6 +++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-demo.yml b/.github/workflows/run-demo.yml index 10457dc..33726e8 100644 --- a/.github/workflows/run-demo.yml +++ b/.github/workflows/run-demo.yml @@ -43,6 +43,8 @@ jobs: - name: Run the autopilot (expects a crash) shell: bash + env: + EMPOWER_DEBUG: "1" run: | chmod +x "artifact/${{ matrix.bin }}" "artifact/sentry-crash" 2>/dev/null || true set +e @@ -54,4 +56,8 @@ jobs: echo "::error::autopilot exited 0 - expected a crash" exit 1 fi - echo "autopilot crashed as expected (exit $code) - events ingested" + echo "autopilot crashed as expected (exit $code) - flushing pending crash" + # One-shot CI run: relaunch once so the native backend uploads any + # crash still on disk (belt-and-suspenders alongside sync upload). + "artifact/${{ matrix.bin }}" --autopilot --duration 1 --no-final-crash || true + echo "done" diff --git a/src/core/backend_client.cpp b/src/core/backend_client.cpp index a49013b..63c0294 100644 --- a/src/core/backend_client.cpp +++ b/src/core/backend_client.cpp @@ -49,7 +49,10 @@ bool perform_post(const std::string& url, const char* body, curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result.body); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); + // Keep the call well under the app-hang threshold so a slow backend can't + // be mistaken for a UI hang (this runs on the main thread). + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "empower-fleet/1.0"); if (curl_easy_perform(curl) == CURLE_OK) { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status); @@ -82,6 +85,8 @@ bool perform_post(const std::string& url, const char* body, HINTERNET session = WinHttpOpen(L"empower-fleet/1.0", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!session) return true; + // Bound the call below the app-hang threshold (runs on the main thread). + WinHttpSetTimeouts(session, 3000, 3000, 3000, 3000); HINTERNET conn = WinHttpConnect(session, host, uc.nPort, 0); HINTERNET req = conn ? WinHttpOpenRequest(conn, L"POST", path, nullptr, WINHTTP_NO_REFERER, diff --git a/src/headless/main.cpp b/src/headless/main.cpp index a0385e8..5da10c8 100644 --- a/src/headless/main.cpp +++ b/src/headless/main.cpp @@ -98,9 +98,9 @@ int main(int argc, char** argv) { cfg.environment = env_or("SENTRY_ENVIRONMENT", "ci"); cfg.component = "headless"; cfg.crash_upload_sync = true; // one-shot run: upload the crash before exit - // Headroom over normal pacing (incl. a slow backend call) so only the - // deliberate 8s app-hang scenario trips the watchdog, not the autopilot. - cfg.app_hang_timeout_ms = 4000; + // Headroom over normal pacing (the backend call is bounded to 3s) so only + // the deliberate 8s app-hang scenario trips the watchdog, not the autopilot. + cfg.app_hang_timeout_ms = 6000; cfg.debug = env_or("EMPOWER_DEBUG", "")[0] != '\0'; if (!empower::SentryManager::init(cfg)) { std::fprintf(stderr, "headless: sentry init failed (continuing)\n"); From 42ab518d7d1a4a221e2bf710f350186a8d8f68d6 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 16:34:18 +0200 Subject: [PATCH 4/9] fix(autopilot): drop the flush band-aid; sync upload is the real fix The hard crash (the Convoluted Chain, transaction "firmware.flash") was never a capture/format problem - it just wasn't uploading before the one-shot CI job tore down. crash_upload_sync handles that on its own (the equivalent of crashpad's prompt upload), so the relaunch/flush and the temporary debug logging are removed. Crash reporting stays on the native backend with a minidump. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/run-demo.yml | 10 +++------- src/core/sentry_manager.cpp | 5 ++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/run-demo.yml b/.github/workflows/run-demo.yml index 33726e8..5675c21 100644 --- a/.github/workflows/run-demo.yml +++ b/.github/workflows/run-demo.yml @@ -43,8 +43,6 @@ jobs: - name: Run the autopilot (expects a crash) shell: bash - env: - EMPOWER_DEBUG: "1" run: | chmod +x "artifact/${{ matrix.bin }}" "artifact/sentry-crash" 2>/dev/null || true set +e @@ -56,8 +54,6 @@ jobs: echo "::error::autopilot exited 0 - expected a crash" exit 1 fi - echo "autopilot crashed as expected (exit $code) - flushing pending crash" - # One-shot CI run: relaunch once so the native backend uploads any - # crash still on disk (belt-and-suspenders alongside sync upload). - "artifact/${{ matrix.bin }}" --autopilot --duration 1 --no-final-crash || true - echo "done" + # The crash uploads synchronously before exit (crash_upload_sync), so + # no relaunch/flush is needed for this one-shot run. + echo "autopilot crashed as expected (exit $code) - crash uploaded" diff --git a/src/core/sentry_manager.cpp b/src/core/sentry_manager.cpp index 63c3aad..e03d465 100644 --- a/src/core/sentry_manager.cpp +++ b/src/core/sentry_manager.cpp @@ -178,9 +178,8 @@ bool SentryManager::init(const SentryConfig& config) { sentry_options_set_handler_path(options, handler.c_str()); // --- New out-of-process "native" crash backend ------------------------- - // Selected at build time via -DSENTRY_BACKEND=native. These knobs are - // specific to that backend: capture a client-side stackwalk AND a smart - // minidump, and let the daemon finish the upload after the app exits. + // Selected at build time via -DSENTRY_BACKEND=native: a client-side native + // stackwalk plus a smart minidump, with the daemon finishing the upload. sentry_options_set_crash_reporting_mode( options, SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP); sentry_options_set_minidump_mode(options, SENTRY_MINIDUMP_MODE_SMART); From 50286074ba4cc02ee64cf5a92f60d3c49d6cc8b3 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 17:01:33 +0200 Subject: [PATCH 5/9] fix(crash): raise shutdown_timeout so the daemon finishes the upload The native backend already keeps the crashed process alive until the daemon is done (crash_upload_mode=SYNC, the default). The reason the crash didn't reach Sentry from CI was the daemon's flush window: shutdown_timeout defaults to 2s, too short for our ~1MB crash envelope (minidump + screenshot), so the SDK dumped it to disk to send "on next restart" - which never happens in a one-shot CI run. Raise shutdown_timeout to 8s (under the ~10s crash-handler wait cap) when sync upload is enabled, so the upload completes in-process. This is the native-backend equivalent of crashpad's wait_for_upload; no relaunch/flush needed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/core/sentry_manager.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/sentry_manager.cpp b/src/core/sentry_manager.cpp index e03d465..9265c14 100644 --- a/src/core/sentry_manager.cpp +++ b/src/core/sentry_manager.cpp @@ -186,6 +186,15 @@ bool SentryManager::init(const SentryConfig& config) { sentry_options_set_crash_upload_mode(options, config.crash_upload_sync ? SENTRY_CRASH_UPLOAD_MODE_SYNC : SENTRY_CRASH_UPLOAD_MODE_ASYNC); + if (config.crash_upload_sync) { + // SYNC keeps the crashed process alive until the daemon is done, but the + // daemon only gets `shutdown_timeout` to flush. The default (2s) is too + // short for our ~1MB crash envelope (minidump + screenshot), so it would + // be dumped to disk for "next restart" - which never happens in a + // one-shot CI run. Give it enough time to finish the upload in-process + // (kept under the ~10s crash-handler wait cap). + sentry_options_set_shutdown_timeout(options, 8000); + } // --- Performance, logs, metrics, sessions ------------------------------ sentry_options_set_traces_sample_rate(options, config.traces_sample_rate); From 47481653e9fc3b874bb319261b2841c6d8c76562 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 17:02:25 +0200 Subject: [PATCH 6/9] ci(run-demo): temporary debug to verify the crash upload [verify] Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/run-demo.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run-demo.yml b/.github/workflows/run-demo.yml index 5675c21..65d436f 100644 --- a/.github/workflows/run-demo.yml +++ b/.github/workflows/run-demo.yml @@ -43,6 +43,8 @@ jobs: - name: Run the autopilot (expects a crash) shell: bash + env: + EMPOWER_DEBUG: "1" run: | chmod +x "artifact/${{ matrix.bin }}" "artifact/sentry-crash" 2>/dev/null || true set +e From dfee14967fda04cefed68a34cfedbb6c9659f593 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 17:09:47 +0200 Subject: [PATCH 7/9] ci(run-demo): remove the temporary debug logging Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/run-demo.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/run-demo.yml b/.github/workflows/run-demo.yml index 65d436f..5675c21 100644 --- a/.github/workflows/run-demo.yml +++ b/.github/workflows/run-demo.yml @@ -43,8 +43,6 @@ jobs: - name: Run the autopilot (expects a crash) shell: bash - env: - EMPOWER_DEBUG: "1" run: | chmod +x "artifact/${{ matrix.bin }}" "artifact/sentry-crash" 2>/dev/null || true set +e From 068d9d7facb4532d4a7379ec3ebcc3a6fe77e072 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 17:26:18 +0200 Subject: [PATCH 8/9] fix(autopilot): use a clean null-deref as the deterministic CI crash The Convoluted Chain crashes by jumping to a corrupted callback pointer (0xc0c0c0c0), which symbolicates oddly server-side and made the crash issue hard to recognize in CI. Use a plain null dereference (SIGSEGV at 0x0) for the autopilot's final crash so it surfaces as an obvious, normally-symbolicated crash issue. The Convoluted Chain remains in the Chaos Lab for manual/GUI and Seer demos. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/headless/main.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/headless/main.cpp b/src/headless/main.cpp index 5da10c8..2c30b45 100644 --- a/src/headless/main.cpp +++ b/src/headless/main.cpp @@ -192,10 +192,12 @@ int main(int argc, char** argv) { idle(1500); } if (final_crash) { - // Event 3: the deterministic headline crash, so every CI run yields - // the same Seer-friendly cross-thread/cross-subsystem crash. - std::fprintf(stderr, "autopilot: event 3 - final crash 'convoluted'\n"); - empower::trigger("convoluted", &console); // crashes -> non-zero exit for CI + // Event 3: a clean, deterministic crash (null dereference -> SIGSEGV + // at 0x0). It symbolicates normally and surfaces as an obvious crash + // issue. The richer "Convoluted Chain" crash stays in the Chaos Lab + // for manual / GUI use (its garbage jump address confuses grouping). + std::fprintf(stderr, "autopilot: event 3 - final crash 'null-deref'\n"); + empower::trigger("null-deref", &console); // crashes -> non-zero exit for CI } } else if (listen_port > 0) { std::fprintf(stderr, "idle: waiting for remote commands (Ctrl-C to exit)\n"); From fea46c7ec4391ae612579d956a63d91498e87bcb Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Thu, 25 Jun 2026 17:31:05 +0200 Subject: [PATCH 9/9] fix(crash): don't route headless crashes through the external reporter Root cause of the missing CI crashes: the external crash reporter (Sentry.CrashReporter) is bundled next to the binary and was auto-detected for every component. With it set, the SDK hands the crash envelope to that separate GUI app to submit - but headless/CI can't launch it ("execv failed: Permission denied"), so the crash was written out for the reporter and never sent (while logs/sessions still uploaded, which is why ingest looked fine). Gate it behind SentryConfig::use_external_crash_reporter, enabled only for the interactive GUI. Headless and the smoke test now submit crashes themselves. Also reverts the temporary null-deref simplification: the autopilot's final crash is the Convoluted Chain again. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/main_gui.cpp | 1 + src/core/sentry_manager.cpp | 16 +++++++++++----- src/core/sentry_manager.h | 3 +++ src/headless/main.cpp | 10 ++++------ 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/app/main_gui.cpp b/src/app/main_gui.cpp index 959f189..a9c6a63 100644 --- a/src/app/main_gui.cpp +++ b/src/app/main_gui.cpp @@ -131,6 +131,7 @@ int main(int argc, char** argv) { cfg.environment = env_or("SENTRY_ENVIRONMENT", "production"); cfg.component = "fleet"; cfg.debug = env_or("EMPOWER_DEBUG", "")[0] != '\0'; + cfg.use_external_crash_reporter = true; // interactive desktop app bool sentry_ok = empower::SentryManager::init(cfg); // Real GPU context from the live OpenGL renderer, so even non-GPU crashes diff --git a/src/core/sentry_manager.cpp b/src/core/sentry_manager.cpp index 9265c14..82ced04 100644 --- a/src/core/sentry_manager.cpp +++ b/src/core/sentry_manager.cpp @@ -214,11 +214,17 @@ bool SentryManager::init(const SentryConfig& config) { #endif // --- External crash reporter (official sentry-desktop-crash-reporter) -- - std::string reporter = !config.crash_reporter_path.empty() - ? config.crash_reporter_path - : find_crash_reporter(); - if (!reporter.empty()) { - sentry_options_set_external_crash_reporter_path(options, reporter.c_str()); + // Only for the interactive GUI: when set, the SDK hands the crash to this + // separate app to submit (with a user-feedback dialog). A headless/CI binary + // can't launch that GUI app, so it must submit crashes itself - otherwise + // the crash is written out for the reporter and never sent. + if (config.use_external_crash_reporter) { + std::string reporter = !config.crash_reporter_path.empty() + ? config.crash_reporter_path + : find_crash_reporter(); + if (!reporter.empty()) { + sentry_options_set_external_crash_reporter_path(options, reporter.c_str()); + } } sentry_options_set_before_send(options, before_send, nullptr); diff --git a/src/core/sentry_manager.h b/src/core/sentry_manager.h index 934d034..a5f2421 100644 --- a/src/core/sentry_manager.h +++ b/src/core/sentry_manager.h @@ -35,6 +35,9 @@ struct SentryConfig { // Block the crashing process until the daemon finishes uploading the crash. // Needed for one-shot runs (CI) where nothing relaunches to flush it. bool crash_upload_sync = false; + // Hand crashes to the external crash reporter UI (interactive desktop app). + // Headless/CI must leave this false so the SDK submits crashes itself. + bool use_external_crash_reporter = false; }; class SentryManager { diff --git a/src/headless/main.cpp b/src/headless/main.cpp index 2c30b45..5da10c8 100644 --- a/src/headless/main.cpp +++ b/src/headless/main.cpp @@ -192,12 +192,10 @@ int main(int argc, char** argv) { idle(1500); } if (final_crash) { - // Event 3: a clean, deterministic crash (null dereference -> SIGSEGV - // at 0x0). It symbolicates normally and surfaces as an obvious crash - // issue. The richer "Convoluted Chain" crash stays in the Chaos Lab - // for manual / GUI use (its garbage jump address confuses grouping). - std::fprintf(stderr, "autopilot: event 3 - final crash 'null-deref'\n"); - empower::trigger("null-deref", &console); // crashes -> non-zero exit for CI + // Event 3: the deterministic headline crash, so every CI run yields + // the same Seer-friendly cross-thread/cross-subsystem crash. + std::fprintf(stderr, "autopilot: event 3 - final crash 'convoluted'\n"); + empower::trigger("convoluted", &console); // crashes -> non-zero exit for CI } } else if (listen_port > 0) { std::fprintf(stderr, "idle: waiting for remote commands (Ctrl-C to exit)\n");