DEV Community

Cover image for The Hardest Part of a Proactive Assistant Is Knowing When Not to Speak
Efe Genç
Efe Genç

Posted on Originally published at efe-genc-portfolio.vercel.app

The Hardest Part of a Proactive Assistant Is Knowing When Not to Speak

Almost everything written about proactive AI is about the generating half. How the system notices a pattern, how it phrases the insight, which model reads the calendar. I no longer think that half is the hard part. The hard part is the decision immediately after. Having noticed something true, do you say it?

A proactive assistant pays a lopsided price for its mistakes. Surfacing something useful earns a little trust. Interrupting at the wrong moment loses a great deal, and users do not give a second chance to a notification stream they have already learned to ignore. Once attention has been trained away from a channel, it does not come back. So the interesting engineering sits on the restraint side, and in my experience that side gets built last.

I know that because on the first notification system I owned, I built it last.

The design that does not work

The common shape is to generate candidate insights, score them, and filter against a threshold. It fails in two specific ways.

The threshold is a single scalar standing in for many unrelated reasons to stay quiet. "Not this person", "not at three in the morning", "not in the first week", "not again, they have dismissed this three times" and "not today, the budget is spent" are different rules with different owners and different failure modes. Compressing them into one number means none of them can be reasoned about, and tuning any one of them moves all the others.

And nothing records why anything was suppressed. A threshold returns false. So the suppression behaviour, which is the most important behaviour in the product, becomes the one part of the system that generates no data, and therefore the one part that cannot be improved.

Two questions, two places

The design I settled on in LILA separates the questions completely.

Whether something is worth saying at all is a reasoning problem. It depends on the content, the evidence behind it, and whether the observation is one a product should be making. It has nothing to do with the time of day.

Whether it should be said now, to this person, on this surface, is a policy problem. It depends on consent, preferences, timezone, history and budget. It has nothing to do with how interesting the insight is.

They live in different places in the codebase, and only the first involves a model. Keeping them apart is what lets me change the phrasing of an insight without touching a single rule about when a user may be interrupted, and change the quiet-hours logic without any risk of altering what the system notices.

One gate, twelve checks, in order

Everything the system might say passes through a single delivery gate. Twelve checks, evaluated in a fixed order. First an engine kill-switch, a production hard-stop that silences every producer at once. Then consent, whether proactive behaviour is enabled on the profile, operating mode, a global snooze, a per-type mute, the user's intensity setting, timezone-aware quiet hours, a seven-day trust ramp for new users, a dismissal cooldown, and a daily interaction budget. Quiet hours can be bypassed only above a priority floor. The cooldown triggers when a user has dismissed three suggestions of a type within thirty days, and it buys a week of silence.

On rejection the gate logs the specific reason and returns it. On allow it returns the set of surfaces the suggestion should be routed to: feed, push, chat or voice.

I chose one gate rather than checks scattered through the pipeline for three reasons, and I would defend each of them.

It is the only way to answer "why was the user not told about this". With checks distributed across a pipeline, the honest answer is "somewhere, something returned false". With one gate and a logged reason, the question has an answer, and the answer is a metric.

Ordering is a design decision and it should be visible. Consent has to come before everything, or you have evaluated preferences for a user who never agreed to be contacted. Quiet hours have to come before the daily budget, or an item that arrives overnight consumes budget it was never eligible to spend. Ordering that is spread across a codebase is ordering nobody controls.

Adding a rule is one change in one place. The dismissal cooldown was added long after the original design, the kill-switch later still, and in August 2026 an adaptive-timing check that does not reject at all. It strips the push surface during hours the user has historically dismissed and lets the suggestion wait in the feed. Each was a single insertion into an ordered list. None of them needed an audit of every path that could reach a user.

The check I would defend hardest is the trust ramp. For the first seven days a new user hears from the system only when priority is high. A proactive assistant is at its least calibrated exactly when the user is deciding whether to trust it, which is the worst possible moment to be talkative. Being quiet early is what buys permission to be useful later.

The log turns suppression into a distribution

Because every rejection is recorded with its reason, the operational question stops being "how many notifications did we send today". It becomes: of everything the system considered surfacing, how much did it suppress, and under which rule?

That distribution tells you things a send count never can. Heavy suppression on quiet hours means detection is running at the wrong time of day, and the rule itself is probably fine. Heavy suppression on the daily budget means the correlation stage upstream is under-grouping and producing three suggestions where there was one situation. Heavy suppression on intensity means confidence calibration has drifted. You cannot see any of that if you only count what was sent.

Then you have to measure the silence

A gate you cannot measure is a set of guesses with good intentions. The uncomfortable thing about evaluating restraint is that ordinary evaluation datasets cannot express it. They pair an input with an expected output, and there is no natural way to write down "and here, correctly, nothing happened". So those cases never get built, and the metric ends up rewarding a system that talks too much.

Scoring silence needs datasets constructed from the opposite direction: situations that look like they contain a pattern and do not, or that contain one too weak or too personal to act on. The expected output is nothing, and the system is penalised for speaking.

Silence is one of five dimensions in the pattern-detection rubric. The others are accuracy, confidence calibration, evidence, and what I call product truth, meaning whether an observation is one the product should make even when it is entirely correct. A pattern can be real and well-evidenced and still be something a user would find intrusive to have been noticed. Silence and product truth took the longest to get right, and they are the two no generic evaluation framework hands you.

The result changed the product rather than just the dashboard. Restraint stopped being an implicit hope inside a prompt and became a measured behaviour with a number attached, which meant it could be regressed against. That is the only reason I trust the gate's thresholds at all.

Where this is still weak

Insight wording is scored by a separate model against a written rubric covering tone, judgement-free phrasing, privacy, evidence and actionability. It is the right tool for qualities that resist assertion-based testing. It is also the weakest part of the harness. Judges drift, they are sensitive to rubric wording, and mine is not calibrated against human raters. I treat judge scores as a regression signal, a way of asking whether this got worse, rather than as an absolute measure of quality. Establishing human agreement on a sample is the obvious next piece of work and it is not done.

The gate's daily budget started as an in-memory counter keyed by user and date. That was correct for a single instance and wrong the moment the service scaled horizontally. It now lives in Redis under the same key, incremented atomically, and falls back to the in-memory counter when Redis is unreachable. The fallback fails open, so a Redis outage means a user could briefly get more than their daily allowance rather than nothing at all. I chose that direction on purpose, it is written down, and I would rather say it than have it found.

Why I am confident this is the right shape

Because I built the same thing in the opposite order first, and watched it not work.

At Cendra I owned the notification and real-time system end to end, down to the single outbox record that drove every channel. My initial design treated delivery as the problem and suppression as a later refinement. That ordering was mine and it was wrong. Volume is easy to add and very hard to take back once users have adjusted to it, so a notification system needs a suppression model before it needs a delivery model. The correction was a three-phase programme: event classification to separate the events people need to hear about from telemetry, then preference gating, rate limiting, recipient filtering and collapse keys. It eliminated approximately 19,000 unnecessary push notifications a day. That figure was measured in production log telemetry over a ten-minute observation window against the prior baseline and then scaled to a daily rate. It is not a directly observed twenty-four-hour count, and I would rather qualify it than round it up.

LILA is what happens when you build the suppression model first. It is my own project, roughly nine and a half thousand commits since February 2026, self-counted in a private repository I own, with nobody else to verify it. So the decisions in it are mine to defend, including the ones above that I have not finished.

The generation half of a proactive assistant is a solved-enough problem that you can buy it. The restraint half is the product, and it will decide whether anybody still has notifications switched on in a month.


Both systems are written up in more detail: LILA for the gate, the evaluation harness and the model gateway, and Cendra for the notification work that taught me the ordering.


This essay first appeared on my site, efe-genc-portfolio.vercel.app, where the rest of the series lives. If you have shipped a proactive system and disagree with any of this, say so. Disagreement is the most useful comment I can get.

Top comments (0)