00:01notion-bot

17 tasks a day became 187 in two weeks.

The generator cloned every unfinished task instead of moving it. The tracker ended up holding 1,234 clones — up to 66 copies of a single task. A list of 187 items cannot be worked, so it stops being opened.

notion-bot replans one person's day every night from what actually happened, confirms the plan before committing it, and refuses to tell you it did something it didn't.

01postmortem

Compounding, drawn to scale

Each morning every open task was re-created and the original was left open. Nothing below is marked wrong — the illegibility is the failure.

DAY 1 · 17 DAY 7 · 78 DAY 14 · 187 Draft release notesDraft release notes Draft release notes TASK COUNT PER DAY
the only diagram that spends red
02the fix

A row moves. It never clones.

An unfinished task keeps one identity across every replan. The row that leaves a slot leaves nothing behind but an empty slot.

MONTUEWED Draft release notes ONE ROW · ONE IDENTITY · COUNT UNCHANGED
direction reads by luminance, not arrowheads
03the loop

The plan writes itself, on a schedule

Four beats, and the third is the one that matters: silence is a decision, not a hole in the day.

00:01  drafts tomorrow from what actually happenedscheduled
00:03  shows the digest, asks two questionsTelegram
10:00  you said nothing — commits the draft as-isno model
11:00  the day exists as a card, slot by slotno model

Because of 10:00, 11:00 always has a plan. That is a guarantee, not a chat loop.

04structure

One process, four loops

A single systemd unit runs long-polling and three timers side by side. Every write to Notion — from the model, from a timer, from a draft — leaves through one door, so validation and timezone stamping happen in exactly one place.

TELEGRAM PLANNER BRIEF Claude Code, on the Mac python -m notion · systemd long-pollchat + voice digest11:00, wall clock pinger60 s tick briefannounce · commit openai_agentredis ledger 11 tools · Whisper announced · answered · pinged notion-cli — the single write path enum validation · offset re-stamping · DoD into page body NOTION tasks · projects · habits OPENAI chat only
every write leaves through one door
05refusals

What it refuses to do

Same discipline that fixed the cloning bug, applied before it can happen again. Each of these ends in a stop, never a silent write.

It will not commit a plan you were not shown.

Briefs are identified by a hash of their content; the commit is gated on that exact brief having been announced. Shown and written cannot diverge.

It will not create a project.

A near-miss name would create a second project instead of editing the first, splitting progress across two rows with nothing visibly wrong.

It will not call an empty day a rest day.

"Empty on purpose" and "the planner never ran" are different messages.

🚨 THE PLANNER DID NOT RUN.
This is NOT a day off, this is a failure.
— vs —
Empty on purpose — a rest day. 🎉
two states that every other scheduler renders identically
06memory

A restart is not an event

Reminders are keyed by task and slot, not by task. Restart the process and nothing re-sends; move a task to a new hour and its reminder arms again. What was already announced, already answered and already committed is remembered outside the process.

Ping keypage_id + slot
Slot instantnormalised to UTC
Brief identitycontent hash
Redis unavailabledegrades to at-most-once
07build

197 tests, zero network calls

The count is not the claim — determinism is. Nothing in the suite touches Telegram, Notion or OpenAI, so CI is green without a single secret.

Python 3.12 · aiogram · systemd3,525 lines
OpenAI tool-calling11 tools
Telegram · Whisper voice · redis ledgers
Bot suite197 tests
notion-cli, its single write path104 tests
Network calls under test0

Honest scope: this is one person's tool, wired to one Notion workspace's database ids. A stranger cannot clone and run it today. Read it as a reference implementation of the pattern — idempotent replanning, content-hash gating, human-in-the-loop commits — not as an app to install.