adventure: tell the story of a run, not just how it ended

Pete only ever heard that an expedition happened once it was over — a zone
cleared, a retreat, a death. The run itself was narrated into one Matrix DM
and thrown away. The map on the adventurer page has always shown where
somebody is; this shows what happened there.

Beats arrive on their own channel, append-only and idempotent on
(run_id, seq). They are the one thing gogobee pushes that is history rather
than state, so they accumulate instead of replacing — and they stay off the
dispatch queue so a chatty run can never spend the retry budget a death
dispatch depends on.

The run header is derived from the beats rather than pushed: a run whose
start beat never arrived still gets a readable, unattributed log instead of
being dropped for want of a name.

An unknown beat kind renders as its own noun rather than 400ing. That is the
same lesson the dispatch ingest learned the hard way, and the regression test
covers the class, not the case.

Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
This commit is contained in:
prosolis
2026-07-24 16:33:48 -07:00
parent 7051e8ffff
commit 8c3f2b0d07
10 changed files with 1213 additions and 5 deletions
+9
View File
@@ -152,6 +152,15 @@ func RunMaintenance() {
exec("prune old daily_visitors",
`DELETE FROM daily_visitors WHERE day < ?`, unixDay()-30)
// Finished expedition logs. Kept for longer than the page shows them (the
// adventurer page hides a run six hours after it ends) because the dispatch
// that announced the run outlives the run, and a dead link from a story to
// its own log is worse than a log nobody reads. A run still walking is never
// pruned however old it looks — see PruneRuns for why.
if err := PruneRuns(nowUnix() - int64(14*86400)); err != nil {
slog.Error("db exec failed", "op", "prune finished runs", "err", err)
}
exec("wal checkpoint", "PRAGMA wal_checkpoint(TRUNCATE)")
exec("optimize", "PRAGMA optimize")
}