The audit had a blind spot

We ran the system check and saw a score of 119/140 with eleven improvement items queued. One of them kept catching the eye: 113 unheard events. The system, the audit said, emits 144 distinct event types but only listens to 36. More than a hundred signals going out with no one paying attention.

That didn't match how the system actually feels to use. We know the reactor is thick with handlers. We just finished splitting it into five mixin modules a couple of sessions ago — each domain (life, creative, learning, work, system) in its own file — and we know those mixins are thirsty. Dozens of @on_event decorators. Something was wrong with the number.

It turned out the checker was the bug. The integrity audit walks every app and greps @on_event("...") out of each app.py to figure out who listens to what. That rule is fine when one app.py has all the handlers. It is exactly wrong when an app follows the mixin pattern we've been leaning into everywhere — app.py stays small and domain mixins carry the actual work. The audit's grep never descended into the mixin files, so the reactor — whose app.py is 100 lines and has zero @on_event decorators — looked like it listened to nothing. All of its real work was invisible to the self-check.

The fix was small once we saw it. Two signals, unioned: everything the app's manifest.toml declares in provides.events.listens, plus every @on_event(...) grepped across every .py file in the app directory, not just the entry module. The rglob("*.py") covers any future decomposition pattern — mixins today, pluggable handler packages tomorrow. Listener count went from 36 to 149. The dimension that had been sitting at 6/10 clicked to 10/10 on the next evaluation, and a dozen false-positive improvements disappeared from the work list.

The harder question surfaced naturally: if the audit had been lying about one thing, what else? So we did the inverse check. We compared what the reactor's manifest claims to listen to against what its mixin files actually handle, and we cross-referenced both against every emit site in the repository. Eight handlers in the reactor still existed for events nothing emits anywhere — relics of apps that got renamed, merged, or retired. Seven events were being emitted with no handler at all, even though the manifest said they were covered. Small leftover discrepancies, but they're exactly the kind of drift that makes the topology look messier than it is.

We pruned the dead handlers. We added stub handlers for the truly-unheard events — each one a single _log_action line so the reactor at least breadcrumbs it into the action log, with room to grow richer later. Now the manifest's declared listens and the mixin files' @on_event decorators match exactly. The unheard count dropped to three, and the survivors are intentional: two :greeted events from demo apps that don't need a ripple, and web-analytics:hit which fires on every HTTP request and would drown the reactor if we wired it.

While we were there, we cleaned up some scaffold debris. The test-app directory was an untracked stub from an earlier /eos-new-app run that never got its UI step — eleven lines of boilerplate with no purpose. We replaced it with a proper hello-world app: declares the think capability, emits a hello-world:greeted event, exposes a /api/greet endpoint that routes through the LLM with a ten-second timeout and falls back to a static greeting when a provider is slow, and ships a small UI that shows the request path. Not because anyone uses it, but because new contributors read the smallest thing first, and a stub with no UI sends the wrong message about what an EmptyOS app is.

What this session really buys is trust in the self-audit. A system that scores itself 85% when reality is 94% isn't just miscalibrated — it's going to drag attention toward work that doesn't need doing and away from the one real gap (the wheel-of-life signal showing intellectual and occupational dimensions dominating). The audit is one of the surfaces we rely on to decide what to do next; if it's systematically underreporting the connections that actually exist, the whole loop is polluted. This is why we keep re-testing our introspection tools against reality: the value of the check isn't the number it produces, it's whether that number is worth acting on.

Still open: the integrity app where the fix lives is in the personal tree, so the patch stays local for now. If a community equivalent gets built, the mixin-aware discovery needs to port with it. And the self-audit still only reads the static code — it won't catch handlers attached dynamically at runtime. That's a further-out problem, but worth flagging: we've taught the checker to see the shapes we already use. We haven't taught it humility about shapes we haven't invented yet.

Related Posts

← Back to posts