Summary
Based on the nanoMuse v1.0.9 source (main @ 067384b), this post dissects how an open-source personal agent implements proactive intelligence: a scheduler loop waking every 30 seconds handles all due checks; due items are handed to the model through an idempotent SQLite ledger; the model's output goes through the [quiet] prompt protocol that decides whether to interrupt the user; dangerous actions are stopped by the Sentinel gate. The most counterintuitive finding: a failed model run still counts as 'this occurrence happened' — only a process crash gets replayed. Conclusion: the reliability of proactive intelligence comes from a deterministic engineering shell, not from the model.

Research baseline: nanoMuse v1.0.9 “Tiller” (main @ 067384b). All line numbers in this post refer to that commit.

It used to be that you went to the agent — open the app, type a question, get an answer. Now the agent comes to you, at the right time, based on what it knows you need: it reminds you, follows up on your goals, tells you “that thing you asked about has an update.”

That is not magic, and it is not a new kind of model. It is a tightly interlocking engineering system. This post takes nanoMuse, an open-source project, and pulls its “proactive intelligence” apart at the source level.

First, the map — the full path of one proactive act, from “something is due” to “it found you”:

The complete execution path of one proactive act

Each section below zooms in on one segment of this picture.

1. One loop, waking every 30 seconds

The foundation of all this “proactivity” is an ordinary Python coroutine in MuseService (service.py:2150-2187):

async def _goal_scheduler(self) -> None:
self.schedule_next_pass()
while True:
try:
self._run_due_reminders()
self._run_due_check_ins()
await self._refresh_calendar()
self._run_event_triggers()
await self._poll_mail()
self._prune_fired()
self._announce_wake()
await self._run_feed_routine()
due = self.next_goal_pass_at or datetime.now(UTC)
remaining = (due - datetime.now(UTC)).total_seconds()
if remaining > 0:
# short naps so a changed setting takes effect without a restart
await self._nap(min(remaining, 30))
continue
...
goal = self._pick_goal_for_pass()
if goal is not None:
self.advance_goal(
goal.id, occurrence=due.isoformat(timespec="seconds")
) # one goal per tick keeps the chat readable

No cron, no message queue, no scheduling framework — just a while True that never sleeps longer than 30 seconds, waking to ask a few questions in order: any reminders due? any goals due for a check-in? anything about to start on the calendar? any new mail?

The ... in the snippet above skips a few gates: no advancing when proactivity is off, none during quiet hours, and the main chat always has the right of way — you will meet them again in the summary diagram at the end.

What deserves attention: every check on this layer — _run_due_reminders, _run_event_triggers, _poll_mail — is a SQL query or a data comparison, costing zero tokens. Mail polling tracks a UID high-water mark (service.py:2027-2054); calendar triggers are “event title matches + start time closer than N minutes” (service.py:2070). All deterministic code.

Deterministic work belongs to deterministic code. This layer needs no model — which is exactly why it is reliable.

The 30-second nap ceiling is explained in a comment: changed settings take effect without a restart. And “one goal per tick” is deliberate — the comment says so: “one goal per tick keeps the chat readable” (service.py:2182).

2. Ledger first, then act: Outbox and Inbox

When the loop finds something to do, it does not throw the work straight at the model. It crosses a bridge first — the DeliveryOutbox.

At heart it is one SQLite table (outbox.py:38-54); each row is a piece of “work waiting to be delivered”, made idempotent by a single unique key, source_key TEXT UNIQUE:

CREATE TABLE IF NOT EXISTS scheduled_deliveries (
id TEXT PRIMARY KEY,
source_key TEXT NOT NULL UNIQUE, -- idempotency key, e.g. "goal:<id>:<timestamp>"
kind TEXT NOT NULL, -- reminder | goal | goal_check | trigger
...
status TEXT NOT NULL DEFAULT 'pending', -- pending → done
finalized INTEGER NOT NULL DEFAULT 0 -- done → finalized
)

When the scheduler finds a due item it first calls claim() (service.py:1654-1676): it tries to insert the row, and a unique-key conflict means “this occurrence is already on the books” — drop it. Only a successful claim queues the task into the session’s inbox, an in-memory asyncio.Queue (service.py:331).

Here is what I consider the single best decision in the whole delivery mechanism. What happens when a background task fails? Read the comment in _worker (service.py:1388-1393):

# The attempt happened and the failure is on screen: the reminder, the
# routine or the trigger moves on to its next time rather than standing
# due until a restart. Only a run the process never finished (a crash, a
# shutdown) is replayed at the next start.

A failed model run still counts as “this occurrence happened” — the reminder moves on to its next slot instead of staying due and re-firing on every restart. Only a genuine crash (a run the process never finished) gets replayed on restart. This one rule prevents the classic incident: restart the service daily, and the user gets spammed by the same email once a day.

So failed tasks are gone forever? The source answers in two parts. A failed goal pass: the goal itself still sits in the database, and the next pass (default interval: 60 minutes) will pick it again — overdue first, least-recently-touched next (_pick_goal_for_pass, service.py:1795-1799) — so failure becomes “try again next round”, not a retry storm. Reminders are blunter: the mark_fired docstring says “a routine moves to its next occurrence, a one-off is done” (reminders/store.py:188-190) — a recurring reminder jumps to its next slot, a one-off simply ends, and the failure notice stays on screen. Better to skip one delivery than to send it twice: a deliberate trade-off.

On process restart, _recover_deliveries (service.py:1631-1636) first settles the books (finalizes rows that are done but whose source counters were not advanced), then replays pending work. One SQLite table, three states, and what you get is effectively exactly-once: delivery is at-least-once (replayed after a crash), converging to once via the unique-key dedup.

3. The agent finally thinks — and “the plan is never rewritten behind your back”

Past the bridge, the model. The goal data model is plain (goals/store.py): a Goal has four states (active/paused/done/cancelled), a Step five (pending/in_progress/done/blocked/skipped), all in SQLite, restored on restart — the precondition for “working while the app is closed” is that goals live in a database, not in conversation history.

Each pass is driven by a prompt (ADVANCE_GOAL_PROMPT, prompts.py:259-269), and one boundary in it stuck with me: when the model finds the plan no longer fits, it is not allowed to rewrite the plan itself — it can only propose a change (a proposal), which the user accepts or dismisses in the app. The store’s docstring states the principle bluntly (store.py:10-11):

“The plan itself is never rewritten behind the user’s back.”

And “after this round of work, should I tell the user” is likewise not decided by code — it is a prompt protocol: the tail of ADVANCE_GOAL_PROMPT injects a paragraph of wording chosen by the user’s proactivity level (prompts.py:315-326):

SURFACING = {
"low": "Reach out only if a step got finished or you need the user … "
"Otherwise begin the summary with [quiet].",
"default": "Reach out if there is real progress … If nothing changed … "
"begin the summary with [quiet] so the user is not interrupted.",
"high": "Always report, briefly, including 'still on track' updates.",
}

The level controls more than wording — it turns three knobs at once (service.py:197-198,237-239):

LevelGoal pass interval*“Should I tell you” (SURFACING, paraphrased)Side effect
offno passes at all— no autonomous action, nothing to saymemory tidying stops too (it uses the same tick)
low×2.0, 120 min by defaultspeak only when a step finishes or it needs you; otherwise [quiet]
default×1.0, 60 min by defaultspeak on real progress; [quiet] when nothing changed
high×0.5, 30 min by defaultalways report briefly, including “still on track” updates

* Interval = goal_interval_minutes (default 60, adjustable from 5 minutes to 24 hours) × the level factor, floored at 60 seconds; level changes take effect within 30 seconds, no restart needed.

What the level does not touch is exactly what you explicitly asked for: reminders, goal check-ins, and triggers always go through. Quiet hours (quiet_hours) is a separate knob — it decides when not to act, not how much to act.

If the model decides it is not worth interrupting you, it starts its reply with [quiet] (QUIET_MARKER, prompts.py:311); _worker strips the prefix with split_quiet() (service.py:1367-1369), and the result lands only in the Feed — no push, no interruption.

Reminders you set yourself get the opposite treatment — their prompts hard-code “Never begin with {quiet}” (prompts.py:280,287,306). What you asked for always lands; what the agent does on its own must earn the interruption. Of everything in this design, that asymmetry strikes me as the most human.

This protocol has failure modes, and honesty demands they be said: it relies entirely on model compliance, with no code-level fallback — read _finish_run end to end (service.py:1439-1440); push or no push depends solely on whether the reply starts with the prefix. Forget the [quiet], and you get one extra interruption; apply it too liberally, and the message that mattered waits silently in the Feed until you open it. Neither failure costs much — probably why a single string prefix was trusted as control flow. But if this were a product, I would build an eval set around the quiet rate and continuously monitor protocol compliance — the reliability of a prompt protocol is itself something that needs to be evaluated.

4. The gate: a cage for autonomy

Once the model decides to act, every tool call passes through the same gate — Sentinel. The policy is a six-step pipeline (sentinel/policy.py:1-13): deny-list → explicit rules → per-tool overrides → risk × mode → taint tracking → danger signatures.

Step five, taint tracking: once a session has read private data (your mail, your files), any later send to a host outside the allowlist is escalated to a question. The comment in gate.py:306-310 is explicit — auto mode skips the question, with exactly two exceptions: calls carrying warnings, and calls that would send this session’s private data out.

Step six is the last line of defense, and its comment states why it exists (policy.py:144-152): in auto mode, calls with danger signatures — rm -rf, curl | sh — are still stopped, and the mode setting itself cannot override them. That is what keeps an unattended background pass from doing something irreversible. Every expansion of autonomy is preceded by a reinforced cage.

5. The last mile: reaching you, and the alarm-clock relay

Where the [quiet] prefix from section 3 finally lands is this last mile. A finished run that passed the gate and is worth reporting travels two roads at once: system-level Web Push (push.py, standard VAPID, no third-party account), plus the IM channels you marked “deliver here” (WeCom, Feishu and friends over long-lived connections, channels/manager.py:519-527). Replies tagged [quiet] stay in the Feed, quietly waiting for your next visit — the “should I interrupt you” decision only truly lands here.

One easily missed engineering detail deserves its own paragraph: when a phone dozes, the process is frozen — a Python loop “sleeping until 7:30” has no idea it slept through 7:30. nanoMuse’s answer is a relay (service.py:1712-1742, api.py:1939-1946): the server computes the next due time and broadcasts it over WebSocket; the Android host sets a setExactAndAllowWhileIdle exact alarm; when it fires, the host grabs a 45-second wakelock and calls POST /api/tick to wake the scheduler. Nothing due, no alarm — “Nothing is due → no alarm”.

Closing: a deterministic shell around an autonomous core

Compress the five sections into one picture and this is what you get — the loop circles inside its rules and hands control to the model only when there is truly something to do:

Branch summary: from the loop to the model loop

Take the pipeline apart, then look back at the opening map, and a pattern appears: the closer to the front, the “dumber” the parts — clocks, SQL, unique keys, state machines, all technology measured in decades; the closer to the end, the “smarter” — deciding whether the plan should change, whether the update is worth an interruption: that is the model’s work. And the smartest layer is wrapped in every outer layer’s dumbness: the ledger guarantees no duplicates, the queue guarantees it never talks over you, the gate guarantees no overreach, the prompt protocol guarantees no noise.

The reliability of proactive intelligence was never a model problem; it is a systems-engineering problem. The determinism of the outer layers is the credit backing the autonomy of the inner one — every layer’s “dumbness” is insurance for the next layer’s intelligence.

If you want to build a proactive agent, here are three things I’m taking away from this source:

  1. If SQL can decide it, don’t call a model.
  2. Before the model acts autonomously, build the ledger (idempotency) and the gate (approval).
  3. “When to speak” matters as much as “how to act” — give it a cheap way to stay silent, like [quiet].

Not covered here: evidence-based goal evaluation (goals/evaluation.py, a standalone goal-completion evaluator worth its own post), the iOS side of background execution, and multi-relay setups. The nanoMuse paper (arXiv:2610.08699) describes these mechanisms far more coarsely than the source — the proposal state machine, the outbox delivery, and the [quiet] protocol are all absent from the paper.