Summary
Every interaction submission over the WebSocket transport fails. The client sends a
ClientMessage variant the server does not define, so the server rejects the frame at
deserialization — before any game logic runs.
User-visible error (from the reporter's screenshot):
Invalid message: unknown variant `Interaction`, expected one of `ClientHello`, `CreateGame`,
`JoinGame`, `Action`, `PreviewManaPayment`, `Reconnect`, `AbandonGame`, ... `CancelTakeback`
at line 1 column 21
The enumerated list in that message is exactly the current ClientMessage variant set.
Root cause
| Side |
Evidence |
| Client sends it |
client/src/adapter/ws-adapter.ts:764 — this.send({ type: "Interaction", data: { submission } }) in submitInteraction() |
| Draft path too |
client/src/adapter/server-draft-adapter.ts:218 — identical send |
| Server lacks the variant |
crates/server-core/src/protocol.rs:135 — pub enum ClientMessage has 26 variants, none named Interaction |
| Server lacks the handler |
grep submit_interaction crates/server-core/ → zero hits. session.rs:11 imports only bind_interaction_authority; the crate only ever derives viewer_interaction outbound |
Provenance
Introduced by a8766823cd — "fix(client): authorize attachment interaction fan (#6778)",
merged 2026-07-29. git show --stat a8766823cd -- crates/server-core is empty: that PR
wired the engine, WASM, and client halves but never the server half.
Blast radius
- Single-player / P2P: unaffected —
wasm-adapter.ts:815 and p2p-adapter.ts:1697-1698
dispatch to wasm.submitInteraction and never cross the wire.
- WebSocket multiplayer: every interaction submission fails. The attachment fan on any
equipped or enchanted permanent is the most visible surface, which is why it was reported
as an equipment bug. The failure is at the transport layer, so it is not equipment-specific.
- Server drafts: same defect via
server-draft-adapter.ts:218.
Affected releases
git merge-base --is-ancestor a8766823cd <tag>:
| Release |
Date |
Affected |
| v0.41.0 |
2026-07-30 |
clean |
| v0.42.0 |
2026-07-31 |
first affected |
| v0.43.0 |
2026-08-02 |
affected (current) |
git show origin/main:crates/server-core/src/protocol.rs | grep -c "^ Interaction {" → 0.
Still present on main.
Fix
The client envelope is already correct — ClientMessage is #[serde(tag = "type", content = "data")]
(protocol.rs:134), matching the Action / PreviewManaPayment convention. No client change needed.
- Add
Interaction { submission: engine::types::interaction::InteractionSubmission } to ClientMessage.
- Handle it in the session loop alongside
Action, calling
engine::game::interaction::submit_interaction(state, actor, submission). Derive the actor from the
authenticated session, per the convention documented on PreviewManaPayment (protocol.rs:154-155):
"The authenticated session, rather than the client, determines the actor."
Mirror the WASM path at engine-wasm/src/lib.rs:1142-1152; see
crates/engine/tests/integration/interaction_contract.rs for the stale/unauthorized coverage.
- Reply on the channel
ws-adapter.ts:761-768 awaits, and broadcast state as Action does.
Follow-up (separate issue)
Nothing binds the TS message-type string literals to the Rust ClientMessage variants at build
time, and scripts/check-protocol-version.mjs did not catch this. A generated-constants or
parity-test guard would prevent the whole class.
Summary
Every interaction submission over the WebSocket transport fails. The client sends a
ClientMessagevariant the server does not define, so the server rejects the frame atdeserialization — before any game logic runs.
User-visible error (from the reporter's screenshot):
The enumerated list in that message is exactly the current
ClientMessagevariant set.Root cause
client/src/adapter/ws-adapter.ts:764—this.send({ type: "Interaction", data: { submission } })insubmitInteraction()client/src/adapter/server-draft-adapter.ts:218— identical sendcrates/server-core/src/protocol.rs:135—pub enum ClientMessagehas 26 variants, none namedInteractiongrep submit_interaction crates/server-core/→ zero hits.session.rs:11imports onlybind_interaction_authority; the crate only ever derivesviewer_interactionoutboundProvenance
Introduced by
a8766823cd— "fix(client): authorize attachment interaction fan (#6778)",merged 2026-07-29.
git show --stat a8766823cd -- crates/server-coreis empty: that PRwired the engine, WASM, and client halves but never the server half.
Blast radius
wasm-adapter.ts:815andp2p-adapter.ts:1697-1698dispatch to
wasm.submitInteractionand never cross the wire.equipped or enchanted permanent is the most visible surface, which is why it was reported
as an equipment bug. The failure is at the transport layer, so it is not equipment-specific.
server-draft-adapter.ts:218.Affected releases
git merge-base --is-ancestor a8766823cd <tag>:git show origin/main:crates/server-core/src/protocol.rs | grep -c "^ Interaction {"→0.Still present on
main.Fix
The client envelope is already correct —
ClientMessageis#[serde(tag = "type", content = "data")](
protocol.rs:134), matching theAction/PreviewManaPaymentconvention. No client change needed.Interaction { submission: engine::types::interaction::InteractionSubmission }toClientMessage.Action, callingengine::game::interaction::submit_interaction(state, actor, submission). Derive the actor from theauthenticated session, per the convention documented on
PreviewManaPayment(protocol.rs:154-155):"The authenticated session, rather than the client, determines the actor."
Mirror the WASM path at
engine-wasm/src/lib.rs:1142-1152; seecrates/engine/tests/integration/interaction_contract.rsfor the stale/unauthorized coverage.ws-adapter.ts:761-768awaits, and broadcast state asActiondoes.Follow-up (separate issue)
Nothing binds the TS message-type string literals to the Rust
ClientMessagevariants at buildtime, and
scripts/check-protocol-version.mjsdid not catch this. A generated-constants orparity-test guard would prevent the whole class.