Commit Graph

2 Commits

Author SHA1 Message Date
Indiana
c22b2f9c08 fix: reward guards now survive a reconnect
The three replay guards shipped earlier lived on the in-memory SeanceState,
which made them per-CONNECTION. I flagged that as an open residual at the
time: drop the socket and reconnect — or just open a second one — and the
client got a fresh empty guard and could be paid again for the same spirit.
summon_limiter bounded the rate of that, never the total.

`award_claims` is the durable form: one row per (seeker, presence,
milestone), with a UNIQUE constraint doing the actual enforcement. The claim
is a bare INSERT and losing the race raises IntegrityError, which is caught
and read as "already paid" — a check-then-insert would let two sockets both
read "unclaimed" and both pay. `crossing` is claimed by BOTH roads, so a
spirit crosses once whichever road arrives first.

Measured with the durable claim disabled: 5 reconnects paid 75 extra essence
on the ritual, 60 on a verdict, 140 on the passage, and two simultaneous
sockets paid 30 for one 15-essence ritual.

The four tests that were failing were the tests, not the guard. They compared
raw balances across reconnects, but re-opening a channel IS a summon, and
SUMMON_ESSENCE_TRICKLE is paid per summon by design (inventory.py:42, bounded
by summon_limiter rather than by any once-per-presence rule). The expected
trickle is now stated explicitly so the assertion speaks about the milestone
it is actually testing. Favor has no trickle, so it must not move at all —
asserted separately.

Anti-overshoot covered in both directions: a genuinely fresh presence still
pays in full across a reconnect, a corrected verdict still pays on a second
connection, and `test` stays freely repeatable since it never touches the
ledger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:37:32 +00:00
Indiana
a727858a62 fix: the Passage was an unbounded essence faucet
`passage_start` rewinds the rite to `listen` at any time, and PassagePanel
offers exactly that button after a resisted release. Every replayed layer
re-credited its essence, so one summon funded an endless loop — the 20/60s
limiter caps the rate, never the total. Measured at ~110 essence/minute,
indefinitely.

Each layer now pays the first time it opens for a presence and never again,
cleared only by a genuine summon. Re-walking still reveals; it just doesn't
mint. The frame reports what was ACTUALLY credited, so the UI's running
total can't drift from the ledger.

Three further fixes in the same handlers:
- judgment -> passage double-paid a crossing. The passage -> cross_over
  direction was already guarded; the reverse ran free, favor included.
- both handlers read `state.entity["id"]` AFTER their DB round-trip. The
  HTTP telemetry path drives the same SeanceState and can summon
  concurrently, so a crossing could mark the presence that just arrived.
  Pinned before the awaits.

tests/test_ws_passage.py is new, and covers the gap that let all of this
hide: test_passage.py tests the pure module, and nothing exercised these
handlers over a real connection. Efficacy proven by reverting the fix —
the replay test then reports "minted 280 extra essence".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 01:44:47 +00:00