A proactive message is content the user never asked for, and the cost of failure is asymmetric: send one too few and nobody notices; send one too many and it’s an intrusion.
Table of Contents
Autarkia has a proactive-messaging path: when the user hasn’t said anything, the scheduler decides whether a role should speak first. It works on day one. The hard part is week two, when the user starts finding it annoying.
I went back through the incident records afterwards. Over a hundred involved proactive care, and most were messages sent when they shouldn’t have been; failures to send were rare.
The most obvious one was six meal reminders inside half an hour one morning. The model hadn’t forgotten anything: there were several triggers, each judging and sending independently, none aware that another had just fired.
Another took me days to admit. The user said at eight in the evening, on the phone client, that they had already eaten, and at four in the morning the topic came round again. One backend, and only one client behaved that way, because the two sides resolved which end was last active differently. I assumed switching to smart client selection would cure it, only to find the rule in the source reading: return desktop if desktop is connected, with a comment saying prefer desktop. Written that way the other end would never receive anything, so switching would only have made it worse.
01Backoff killed by one min()
A twenty-minute cooldown was configured and in practice it woke after seven minutes. Sleep time was written as min(dynamic interval, model-suggested wait), the shorter one winning, and the cooldown was ignored entirely. Tuning had nothing to do with it: as long as the shorter value wins, the backoff might as well not exist. Around the same time there was a concurrency net allowing ten seconds, far less than human patience.
02The truth for silence polluted by itself
Computing how long the user had been quiet started from the user’s silence duration and was then overwritten by the timestamp of the assistant’s last message. So fifteen minutes after the character said go to sleep, the system classified it as a long silence and generated a wake-up message built from the eight a.m. template. Silence can have one source of truth and one only: the user’s last message. Overwrite it with the character’s own send time and the character loses track of how long the user has been quiet.
03Four switches overwriting each other
Sleep, busy, do-not-disturb, manual delay: four states, each with its own check, each able to override the others. A manual delay was once cut short by the dynamic interval, and a reminder that came due during sleep was processed anyway and pushed out at three in the morning. By the fourth stacked condition the code was a pile of ifs fighting each other, and it was time for a state machine: an explicit exit from every state, and exactly one module permitted to change it.
One more thing happens when nobody is watching: the messages converge on a template by themselves. Report what I am doing, mention I thought of you, ask what you are doing. Three parts, every time.
One on its own is fine. Five in a row and it feels completely fake.
My reading is that the model assumes every message has to say something, while real people constantly send messages that say almost nothing.
The rules I ended up with: backoff has to be monotonic, and any min() in interval arithmetic deserves a pause. Silence has one source of truth and must not be polluted by your own send times. Use a state machine rather than stacked thresholds, with only one place allowed to change state. Deduplicate semantically, because string similarity kills every short message, including the one good night that mattered. And the default answer is don’t send: if you cannot name a specific reason tied to this moment, don’t send.
Reminders and care messages need two separate strategies. A reminder is worth its punctuality and a care message is worth its restraint; give them one shared backoff policy and both suffer.
Send one too few and the user barely notices; send one too many and it’s an intrusion. So whenever a default is a judgement call, I set it toward not sending.