The extract pre-check is gone. It read a snapshot up to two minutes behind and still got the last word, so somebody who set out over Matrix during a lagging roster push was told they weren't on an expedition for a run gogobee would happily have ended. Same call abandon and leave already made: let it through and let rejected_not_running be the answer. The siege_join check stays, because whether a boss is camped outside town is town-wide and runs on a day-or-longer clock, but it now reads one column through SiegeIsCamped instead of loading every defender row and the whole history to look at one flag. The war-room history insert is OR REPLACE. boss_id is the primary key and it was never settled whether gogobee means the siege instance or the boss type by it, so a duplicate pair used to fail the transaction carrying the live boss and the muster too and freeze the war room on the last good snapshot. A dropped history row is the smaller failure; the open question is noted in the schema. offersToUndo's guard didn't cover the case its comment claimed. A gogobee too old to push seats sends a valid blob with no party key, which decodes to the same empty slice as a solo run, and a party member got shown the button that throws away everyone's day. That needs a new field, so whoDetail gains party_known and the flag gates the empty-list branch alone; the branch that reads the viewer's own seat is self-evidencing and keeps working against any sender. gogobee's half is written up in adventure_party_known_flag.md. And an empty offer list no longer claims "you're already out there", which Pete can't actually know from a game box too old to push offers at all.
3.4 KiB
Wire contract — party_known on the public detail blob (gogobee side)
Status: Pete side BUILT + TESTED. gogobee side NOT BUILT. Written 2026-07-24 out
of a code review of the W9 verbs branch. Small: one boolean, set unconditionally,
no new endpoint and no new verdict. Companion to adventure_expansion_spec.md.
The hole
offersToUndo in internal/web/who.go decides which of "Call the whole thing off"
(abandon) and "Leave the party" (leave) an owner's page offers. On a live expedition
with an EMPTY party list it took the "solo run ⇒ this player is the leader" branch,
because a solo run publishes no seats at all.
But a gogobee too old to push party seats sends a perfectly valid detail blob with no
party key, and that decodes to the same empty slice. So a party MEMBER served by an
old game box was shown the leader's button — the one that throws away four people's
day. gogobee refuses it with rejected_not_leader, so the live cost was a misleading
offer rather than a lost expedition, but the page was asserting something it could
not know.
Nil and empty cannot be told apart on the current wire, hence a new field.
What gogobee must send
The public detail blob on the roster push (the one Pete decodes into whoDetail)
gains:
"party_known": true
Set it unconditionally, on every sheet, from any gogobee build that knows what a party seat is:
- on a solo run — YES, this is the case the flag exists for;
- when the party list is omitted because there is nobody else — YES;
- when the player is standing in town — YES;
- when the sheet does carry seats — YES, harmless there.
It is a capability flag about the sender, not a fact about the character. It must
never be computed from whether there is a party, whether the run is shared, or
anything else on the sheet. A conditional party_known is worse than none: it would
read as "this player is solo" and put the flag straight back into the hole it was
added to close.
Anything that omits it (old builds, JSON that fails to decode) lands as false in
Go, which is the fail-closed answer Pete wants.
What Pete already does with it
whoDetail.PartyKnown bool \json:"party_known"`—internal/web/who.go:47`.offersToUndo's third parameter ispartyKnown; the call site passespage.HasDetail && page.Detail.PartyKnown, so a blob that failed to decode is still excluded.- The flag gates the
len(party) == 0branch only. Thelen(party) > 0branch reads the viewer's own seat and is self-evidencing — a seat that says "member" cannot be mistaken for leadership — so it keeps working against every sender, including ones that never set the flag. - Cases pinned in
internal/web/orders_undo_test.go.
Rollout
Pete is already deployed-safe: it ships first and costs nothing but one button. Until
gogobee sets the flag, a genuine SOLO leader on a live run does not see the abandon
button on the party branch. They are not stuck — the self.Resume != nil clause
still offers it for an extracted run from town, and abandoning over Matrix was never
affected. The moment gogobee ships the flag the button comes back with no Pete change.
Done when
- gogobee sets
party_known: trueon every public detail sheet it builds. - A solo adventurer on a live expedition sees "Call the whole thing off" on their web page again.
- A party member on the same build still sees "Leave the party" and never the abandon.
- Delete this file.