Phase 3: polled sweeps (vitals, house decay, economy)

BridgeSweeps runs three repeating Core-thread timers for the state that has no
EventSink. Cost measured in Phase 1 is why they can run on the main thread: a
full pass of all three is well under a millisecond at the seeded scale.

  - Vitals: online players only. Small and volatile; the sidecar diffs snapshots.
    Offline characters do not move, so they are served on demand as full profiles
    instead, not swept.
  - House decay: emits only on a level transition. A silent baseline on
    ServerStarted records every house's current stage, so a restart does not
    re-announce them. Payload carries from/to, coords, nested ban location,
    region, sign name, owner serial+account, and built/refreshed timestamps, all
    null-guarded.
  - Economy supply: periodic sum of every account's currency as a snapshot. The
    level; AccountGoldChange and the vendor events are the flow.

All three re-arm on `[bridge reload`; `[bridge sweepnow` runs one of each on
demand; `[bridge status` reports sweep counters. Sweeps skip emitting while the
sidecar is disconnected, since their state is perishable and re-emitted next
tick anyway (unlike events, which queue through an outage).

Verified on the seeded world with 8s intervals: baseline recorded 29 houses
silently, a probe bumped one Somewhat->Fairly, and the next sweep emitted exactly
one house.decay and none for the other 28. Economy emitted a supply snapshot per
interval. Vitals emitted nothing, correctly, since all seeded characters are
offline. Evidence in docs/PLAN.md §13.

Adds tools/scaffolding/BridgeSweepProbe.cs (never deployed) to force a decay
transition on demand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 10:59:21 -05:00
parent d5035cb6c6
commit afc97c414e
6 changed files with 389 additions and 4 deletions

View File

@@ -37,7 +37,7 @@ Anything under `overlay/` is authoritative. Do not edit files in the server tree
| 0 — build fix (`Scripts.csproj`) | **done, verified end-to-end** |
| 1 — transport (`BridgeLink`) | **done, acceptance in `docs/PLAN.md` §11** |
| 2 — event streams (`BridgeEvents`) | **done, acceptance in `docs/PLAN.md` §12** |
| 3 — sweeps | not started |
| 3 — sweeps (`BridgeSweeps`) | **done, acceptance in `docs/PLAN.md` §13** |
| 4 — request/response | not started |
| 5 — `[link` account linking | not started |
| 6 — town-crier inbound | not started |
@@ -66,6 +66,7 @@ Runtime script compilation therefore had no effect, silently. `overlay/Scripts/S
| `BridgeLink.cs` | The socket. Link thread owns it; a bounded drop-oldest queue fronts it; a reader thread marshals inbound lines to the Core thread. |
| `BridgeBoot.cs` | Lifecycle, inbound dispatch, `[bridge status\|reload\|ping]`. |
| `BridgeEvents.cs` | EventSink subscriptions (Phase 2). Read-only, player-filtered, never emits secrets. |
| `BridgeSweeps.cs` | Polled streams (Phase 3): vitals, house decay on transition, economy supply. Core-thread timers. |
`Emit()` is called from the Core thread. It enqueues and returns — it never touches the socket, never blocks, never allocates a syscall. **A wedged or absent sidecar cannot stall the shard**, and that is the property everything else depends on.