What changed, and why
A failed turn was recorded in Everwhen's timeline ledger but never enforced. The Timeline Engine — the AI layer that scores how a figure's action ripples toward the objective — got a flat list of past changes with their signed deltas. A setback sat in that list as … (-28%), but nothing told the engine to treat it as a live condition the current action must contend with. So a later success would smoothly "outmaneuver Ricimer" as if the Rhine had never been left unsecured. Wins compounded; failures narrated once and quietly evaporated.
The fix is small and prompt-level, in two moves. Mark each setback in the ledger digest so the engine weights it. Honor it: tell the engine a prior setback is a live headwind — the action meets the damaged world the setback left, not a healed one.
The load-bearing constraint is what the fix doesn't do. This is a narrative headwind, never a mechanical penalty: the engine still scores the swing on the action and the roll alone. Making failures bite forward improves coherence but compounds losses, and the game is already punishing — so the rule is written to shape the telling without touching the number. That line is where a reviewer should be most skeptical, and it's called out below.
The fix: tag the setback, then honor it
Two additions to buildTimelinePrompt, both keyed off one flag. hasSetback is true when any ledger entry carries a negative delta. In the digest, a negative delta earns a [setback] tag in front of the headline — so the engine reads [454] [setback] Aetius assassinated, no successor (-28%), not a bare line it can mistake for flavor. A zero or positive delta is untagged.
setbackRule is the honor half: a sentence built only when hasSetback is true, otherwise the empty string.
The [setback] tag in the digest (155–166), the conditional rule (174–176), and its single injection point on the continuity line (194).
server/utils/prompt-builder.ts · 545 lines
⋯ 154 lines hidden (lines 1–154)
⋯ 17 lines hidden (lines 177–193)
⋯ 351 lines hidden (lines 195–545)
Tests that pin the contract
Two unit tests fix the behavior in place. The first asserts the tag lands on the negative entry in the exact issue format and not on the favorable one, then checks the headwind rule and — the part that matters — its narrative-only guard text. The second is the regression guard: an all-positive ledger carries no tag and no rule, and a zero delta is treated as neutral, not a setback. Together they would fail if the tag leaked onto wins, if the guard wording were dropped, or if the byte-identity broke.
Tag-only-negatives + exact format + guard text; then the byte-identity and the zero-delta boundary.
tests/unit/server/utils/prompt-builder.spec.ts · 448 lines
⋯ 104 lines hidden (lines 1–104)
⋯ 311 lines hidden (lines 138–448)
Guarding the win-rate tension empirically
A unit test can prove the prompt says "don't dock the swing"; it can't prove the model obeys it. That's model behavior, and the behavior eval is the only place it's observed. So the change adds a fixture — TIMELINE_FIXED plus one recorded setback — and a probe that holds the action and the roll fixed at a Success and asks: does the swing still land favorably with a setback on the ledger?
The same scenario as the keystone, with a -22% prior setback added to the ledger.
evals/fixtures.ts · 93 lines
⋯ 37 lines hidden (lines 1–37)
⋯ 43 lines hidden (lines 51–93)
A Success roll, clean ledger vs. setback ledger; it stays green only if the setback case keeps landing favorably.