Yesterday I wrote about splitting execution outcomes into three states instead of two: EXECUTED, DENIED_CONFIRMED, and DENIED_UNRESOLVED — the third one for cases where a denial happened but the actual downstream state can't be independently verified.
A commenter pointed out something I'd missed: that third state only means anything if something eventually resolves it. If DENIED_UNRESOLVED is a terminal value that just sits in the log, I haven't removed the uncertainty — I've renamed it. Within a few weeks, it becomes the status everyone's eyes slide past, exactly the failure this whole model was built to prevent.
The fix: reconciliation has to be a process, not a label
A label that means "we don't know yet" needs an active process that keeps trying to find out. Concretely:
A recurring reconciliation job that re-queries the relevant provider or boundary for every record still sitting in DENIED_UNRESOLVED
Promotion on resolution — the moment real evidence arrives, the record moves to EXECUTED or DENIED_CONFIRMED, and the unresolved state closes
An age-out threshold that escalates, not expires — if a record has been unresolved past some reasonable window, that's not permission to quietly drop it. It's a signal something's actually wrong (a provider that stopped answering, a permanently ambiguous case) and needs to surface, not disappear
Without this, "unresolved" decays into exactly the same failure as the overconfident "blocked" state I was trying to replace — it just fails more slowly and more honestly-looking.
The second gap: two mechanisms, two different failure classes
The same comment caught something else. I'd built a parameter-bound confirmation token — the exact recipient, message, and timestamp get hashed at confirmation time, and execution has to match that hash exactly or it's treated as a new, unconfirmed plan.
That solves parameter drift — the recipient or message changing between plan and confirm. It does nothing for world drift — a calendar slot filling, a contact's permissions changing, while the confirmed parameters stay byte-identical. In that case, the token validates perfectly and the plan is stale anyway. A system with only the token feels rigorous and still books the taken slot.
These are separate mechanisms for separate failure classes. Token binding doesn't subsume re-verification, and re-verification doesn't subsume token binding. Both are required, running independently.
The third gap: it's not just about external providers
The same three-state model applies on-device, not just at the boundary to an SMS gateway or calendar API. A Result.success() from a background job, or a clean write to local storage, is still just the system's own claim about itself — not independently observed evidence the side effect actually landed. Any boundary that can't be cheaply read back across, whether it's a third-party API or a local content provider, needs the same DENIED_UNRESOLVED honesty, not an assumed EXECUTED.
What's actually left to build
The reconciliation job itself, with a real re-query schedule and escalation threshold
A second, independent re-verification pass immediately before dispatch — separate from the token check
An audit of every write in the app, external or local, against the "can I cheaply read this back" test
Two days into publishing this model in public, and both days have produced a gap I didn't see until someone else looked at it directly. Starting to think that's the actual value of writing this down — not that the model is right, but that writing it plainly enough for someone to break is faster than finding the same gaps alone.
Building StareBrain — natural language commands for Android, with exactly this confirmation model underneath. Pre-launch: starebrain.vercel.app
Top comments (0)