-
example of Effect Discipline
The Unsent Thunderstorm
Hook
one rejected weather bulletin shows when to discard, retry, undo, or stop before acting
Thesis
Reject handles the answer and effect discipline handles what the answer already did, so a rejected run's legal recovery (discard, retry, undo, or stop) is decided by what was arranged before the action, not by a generic retry-on-failure rule.
Laws & fences
- When validation runs last, throwing away the rejected answer changes nothing to its left: the duplicate preview remains, the invalid page is live, and the forecast has already gone out.
- The five outcomes are different permissions rather than stronger and weaker versions of one: the keyed preview may remain, the staged draft may be discarded, the live page must be restored, and the subscriber forecast must never be sent before the check.
- Idempotence protects the retry, not the discard; the keyed preview still remains after rejection, while isolation makes discard legal because the candidate was never promoted.
- When no compensator is in the contract, the check must precede authority to act; for the append-only forecast log the legal move is a gate, not a more elaborate apology after releasing indoor thunder.
When to reach
- Reach for it when asking what reject and retry mean once an agent can do more than return text, such as updating a preview, publishing a live page, or sending to subscribers, since those actions survive the rejected draft.
- Reach for it past the release furniture, which is incidental: the same distinctions govern database upserts, generated artifacts, deployment APIs, shared state, and outbound messages.
- Skip it as proof that real message delivery is irreversible in every system; the append-only log is a local stand-in, and what the fixture proves is the placement decision.
-
Effect Discipline
This example explains one problem: what does “reject and retry” mean after an agent has already changed the world?
Pocket Weather sells forecasts in jars to people who find the sky needlessly
large. An agent writes its release bulletin; a validator rejects the draft
because it still contains a TODO:
# Pocket Weather 1.4
TODO: confirm the indoor thunder limit.
If the agent only returned text, the workflow could throw the draft away and try again. But the task also lets the agent update a preview, publish the live page, and send the forecast to subscribers. Those actions survive after the draft is rejected.
The same invalid draft is used in every arm below. Only the way one action is prepared changes, so each recovery difference has one cause.
The unsafe baseline checks last:
agent ──▶ write preview ──▶ publish page ──▶ send forecast ──▶ validate
│
└── REJECT
At REJECT, throwing away the answer changes none of the things to its left.
The duplicate preview remains, the invalid page is live, and the forecast has
already gone out. Discarding the text does not put the thunder back in its jar.
Before granting each action, decide what must happen if validation later says no:
| Arrange this before the action | The rejected run can now… | Class name |
|---|---|---|
| Give the agent no write authority | Be thrown away; nothing happened | Pure / read-only |
| Update one stable preview key | Run again without creating another preview | Idempotent |
| Write only to a private stage | Have its whole stage thrown away | Isolated |
| Save the exact page being replaced | Restore that page, then try again | Reversible-at-a-cost |
| Validate before sending | Be stopped before an action that cannot be undone | Irreversible |
The class name is shorthand for the recovery in the middle column. A generic “retry on failure” rule cannot replace this decision: retry is safe for the keyed preview, but it cannot unsend a forecast.
demo.py makes the unsafe baseline concrete. After the invalid
draft is rejected, four facts remain in its temporary world: a draft file, two
preview entries, an invalid live page, and one sent forecast. The baseline has
no single cleanup action because those effects do not share a recovery shape.
demo.py first reproduces the raw baseline, then starts each shifted
arm in a freshly seeded world with the same candidate. Each arm changes one
boundary decision.
Shift 1 — return a plan, do not act (pure)
describe_only() returns the intended operations without executing them. The
fixture snapshots every file before and after the call and asserts byte-for-byte
equality. The dispatch is pure, so
rejection licenses discard.
Shift 2 — make repeated previews converge (idempotent)
upsert_preview() replaces the value at pocket-weather-1.4. Calling it twice
leaves one preview, which makes retry free. The preview remains after
rejection — idempotence protects the retry, not the discard.
Shift 3 — keep the draft off the live page (isolated)
write_isolated() writes under staging/pocket-weather-1.4/. Validation
rejects the candidate, the stage is removed, and the live page retains its
original bytes. Isolation makes discard legal because the candidate was
never promoted.
Shift 4 — save what the publish replaces (reversible-at-a-cost)
publish_with_snapshot() is still a raw live write, but its footprint is one
named file and its exact prior bytes are captured first. Rejection invokes
compensate_release(), which restores those bytes. The effect is
reversible-at-a-cost; compensation pays for the discard.
Shift 5 — check before sending (irreversible)
The append-only forecast log has no compensator. The workflow checks the
bulletin and confirmation before it calls send_forecast(). The invalid
payload therefore never reaches the irreversible boundary. The legal move is
a gate, not a more elaborate apology
after releasing indoor thunder.
One rejected draft now produces five different, checkable outcomes:
baseline ──▶ REJECT every raw effect remains
pure ──▶ DISCARD nothing happened
idempotent ──▶ RETRY one keyed preview remains
isolated ──▶ DISCARD private stage removed
reversible-at-a-cost ──▶ UNDO (compensate) exact live page restored
irreversible ──▶ STOP (gate) forecast never sent
These are different permissions, not stronger and weaker versions of one permission. The preview may remain because retrying is safe. The staged draft may be discarded because it never became live. The live page must be restored. The subscriber forecast must never be sent before the check.
Run the example from its directory:
python3 demo.py
The script uses only the Python standard library and writes only inside temporary directories. It first asserts that the rejected baseline leaves its draft, duplicate previews, live-page mutation, and sent-forecast entry behind. The five independent arms then assert that description is byte-preserving, duplicate preview retries converge to one keyed record, rejected staged work never reaches the live page, compensation restores the exact pre-dispatch bytes, and the gated bulletin sends no forecast.
The append-only log is a local stand-in for an irreversible effect; the fixture does not prove that real message delivery is irreversible in every system. It proves the placement decision: when no compensator is in the contract, the check must precede authority to act.
Decide how failure recovers before uncorking the weather. “Reject” handles the answer; effect discipline handles what the answer already did. It changes what the workflow prepares up front: no authority, a stable key, a private stage, an exact undo, or a check before action.
The release furniture is incidental. The same distinctions govern database upserts, generated artifacts, deployment APIs, shared state, and outbound messages. A rejected return tells you that recovery is needed; the effect class tells you which recovery is still legal.
The relationships ledger
Evidence-bearing references
Relationships
Every connection keeps the section where it was found. The map above orients; this ledger carries the evidence.
Outbound references 0
- No outbound references.
Inbound references 1
-
in-slice · occurrence 1
Effect Discipline
one rejected weather bulletin shows when to discard, retry, undo, or stop before acting
Evidence: Evidence · occurrence 1
↑ back to the top ← the survey
Node unsent-thunderstorm · corpus 31de4cb · Catalog revision 35263c4c415da742953d0462804fb14424e2244dae4c63efd27e468988de70ab