2
6 Comments

My "blocked" status was quietly lying to me

Follow-up to yesterday's build log. Someone gave me a model that broke a fix I thought was already solid.

My system had two states: executed, or blocked. Turns out blocked was doing more work than it earned. My earlier fix (tag every attempt with an ID, drop stale results) stops the interface from showing a wrong result. It does nothing to revoke the authority of an older, already-in-flight attempt to actually go complete somewhere downstream I'm not watching. Fixing what the screen says isn't the same as fixing what happened.

The real model needs three states, not two:

EXECUTED — observed evidence of the actual side effect
DENIED_CONFIRMED — denied, and verified it never crossed the execution boundary
DENIED_UNRESOLVED — denied, but downstream state can't be conclusively closed

That third one is uncomfortable, because for some actions it might be permanent. I can't query someone's phone to prove a text never arrived. Best I can honestly claim is "never submitted to the provider" — which is a narrower, weaker, truer sentence than "not delivered."

Also landed on a distinction today that reframes the whole confirmation question: once something's crossed the boundary where consequences aren't fully yours to control anymore, undo stops being reversal and becomes recovery. Reversal means the event un-happens. Recovery means you're managing the aftermath as well as you can. Very different posture, and a confirmation screen's real job might be flagging exactly which one you're about to sign up for.

Slower week than I expected, in a good way — more time spent finding gaps in fixes I thought were done than writing new code.

on September 2, 2026
  1. 1

    Collapsing three states into one is a measurement compression that hides the actual decision boundary. Blocked sounds like a single fact, but it's really three invisible facts: (1) what did we observe, (2) did we verify the denial, (3) is there uncertainty beyond our control. The "screen says blocked" problem is that users read it as statement 1 when it could be statement 2 or 3. Recovery vs reversal is the same threshold - once consequences escape your boundary, you've moved from "did I stop this" to "can I manage this." That's why storing reason plus timestamp for last successful observation isn't just more honest UI, it's exposing the measurement structure itself. The status field was doing three measurements at once and calling it one answer.

    1. 1

      "The status field was doing three measurements at once and calling it one answer" is the cleanest way anyone's put this yet, and it exposes something I hadn't noticed: "blocked" wasn't lying because the label was wrong, it was lying because one word was standing in for three separate facts that don't always agree. A user reading "blocked" has no way to know which of your three they're actually getting.

      The timestamp-of-last-successful-observation addition is the piece I'm taking directly — I'd been thinking about this purely as a state enum, EXECUTED / DENIED_CONFIRMED / DENIED_UNRESOLVED, but a bare state name still hides when the system last had real information, versus how stale that information might now be. "Denied, unresolved, last confirmed clean 40 seconds ago" is a genuinely different claim than "denied, unresolved, never confirmed at all," and my current model can't tell those apart.

      Tying it to recovery-vs-reversal is right too — I think that's actually the same boundary described from two directions this whole week: recovery-vs-reversal is the user-facing question, and observed/verified/uncertain is the same fact from the system's side. Once you're past the boundary on one, you're past it on the other.

  2. 1

    The recovery versus reversal distinction is the strongest part. I would expose the evidence boundary directly in the event record: last confirmed stage, provider request ID if one exists, and what remains unknowable. That gives the UI a precise statement instead of turning uncertainty into a friendlier but false status.

    1. 1

      You and omri above are converging on the same fix from different angles — his was last-successful-observation timestamp, yours is last-confirmed-stage plus provider request ID plus an explicit unknowable field. Combined, that's basically the schema: not a status word, but a small record — stage reached, evidence ID if one exists, timestamp, and a named gap for whatever the system genuinely can't see.

      The "what remains unknowable" as its own explicit field is the part I like best out of either version, honestly, because it's the one most systems don't have room for at all — most schemas have a slot for what you know and treat everything else as implicit absence. Making the gap itself a first-class field is what stops "we don't know" from silently decaying into "nothing happened" over time, which is the exact failure this whole thread started from.

      Going to try drafting the actual event record shape today instead of just the state enum — feels like that's the real deliverable this thread's been circling toward.

  3. 1

    The distinction that saved me a lot of pain: "we know it did not happen" and "we could not verify what happened" are two different states, and collapsing them into one BLOCKED/FAILED flag is what makes status quietly lie. I ended up with EXECUTED / DENIED (a real refusal we observed) / UNRESOLVED (timeout, parse failure, upstream flake) as separate values, and only UNRESOLVED is allowed to be retried or re-checked. In my case it is scraping carrier HTML: the page shape changes or the fetch times out, and if I record that as "no update" the user reads it as "nothing moved," which is not what I actually know. Storing the reason plus the timestamp of the last successful observation made the UI honest almost for free.

    1. 1

      A fourth person landing on the same three states, from tracking-page scraping instead of agent actions, is a good sign this isn't a pattern I talked myself into. "We know it didn't happen" vs. "we couldn't verify what happened" being different states seems to be true wherever there's a boundary between what you decided and what you can actually observe, not just in my specific domain.

      The rule I hadn't made explicit yet: only UNRESOLVED gets to be retried, DENIED never does. That's obvious once said, but I don't think my current logic actually enforces it as a rule — it's more like an accident of which code path a given failure happens to hit. Making retry eligibility a property of the state itself, not something decided ad hoc per code path, closes off a real bug class: someone accidentally wiring a retry onto something that was a genuine, observed denial, not an unknown.

      Your carrier example is a good one for the everyday version of the harm too — "no update" reading as "nothing moved" instead of "I don't know if anything moved" is exactly how this quietly misleads people in something totally mundane, not just in a dramatic agent-safety scenario.