Skip to content

WIP: Multiplayer#799

Draft
SawyerHood wants to merge 8 commits into
mainfrom
bb/multiplayer
Draft

WIP: Multiplayer#799
SawyerHood wants to merge 8 commits into
mainfrom
bb/multiplayer

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

Backup PR for the multiplayer worktree before migrating the primary bb server to Bee.

This preserves the current implementation commits and the orchestration workflow files. It is intentionally opened as a draft and has not been rebased or prepared for merge.

SawyerHood and others added 8 commits July 18, 2026 08:57
… attribution columns

Shared contracts and schema for multiplayer bb, landed serially so parallel
workstreams build against fixed interfaces:

- @bb/domain claimed-identity: x-bb-claimed-identity handshake header schema,
  handle normalization, and boundary encode/decode. Identity is claimed-only;
  admission gateways (connect gate / network) never assert identity.
- @bb/domain change-kinds: typing client message plus thread-presence and
  presence-summary broadcasts (strict + lenient pairs).
- @bb/db: collaborators table (handle-keyed) and nullable attribution columns
  events.actor_handle, threads.created_by_handle,
  queued_thread_messages.actor_handle, pending_interactions.resolved_by_handle.
  Plain text columns without FKs so the migration stays ADD COLUMN only.
- @bb/connect-db: server_member table (gate admission list, owner-managed).
- Server: accept the typing message as a no-op until presence lands; migrate
  drift manifest and rewind/replay tests updated for migration 0079.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…umbing

WS1 (connect cloud): the gate admits invited members alongside the owner —
non-owner sessions are checked against server_member for server-kind labels
(desktop credentials stay owner-only), and admission fails closed unless a
debounced member-admitted audit_log row lands, since that log is the system's
only verified access record. Owner-session member management API on the
worker: GET/POST /api/servers/:serverId/members and DELETE .../members/:userId
(403 non-owner, 404 unknown handle, 409 duplicate, 400 owner-handle), with
member-added/member-removed audit rows.

WS2 (local server): every /api/v1 request and /ws upgrade resolves a claimed
actor from x-bb-claimed-identity, falling back to a startup local-operator
identity (OS username/hostname); collaborators are upserted through a new
@bb/db data module with a 60s same-content debounce; sockets carry their actor
via ws/socket-actors (registered at open, released on close) for the upcoming
presence work. The server still never authorizes by identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS3 (attribution): the human request path records its actor everywhere —
events.actor_handle on sends (queued messages carry actor_handle and preserve
it through every drain path), threads.created_by_handle on human creates,
the acting handle on manual-stop interruption events, and
pending_interactions.resolved_by_handle on approval resolution. Actors come
only from the request-scoped identity, never from request bodies;
agent/thread/plugin-origin writes stay NULL. Turn commands gain an optional
speaker {handle, displayName}, populated only when the thread has two or more
distinct human authors (targeted countDistinct query), and the host daemon
prefixes provider input with "[from @handle] " — single-human threads produce
byte-identical provider input. HOST_DAEMON_PROTOCOL_VERSION 58 -> 59.

WS4 (presence): ephemeral presence derived from thread-detail subscriptions —
viewers deduped by handle with per-socket refcounts, typing with a 6s TTL via
the ws typing message, strict-schema thread-presence broadcasts to detail
subscribers and partial presence-summary patches to thread-list subscribers
(empty array removes an entry; no-op rebroadcasts suppressed), plus a
GET /api/v1/presence snapshot route. Nothing persisted.

Includes the regenerated @bb/plugin-sdk bundled types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ws identity fallback

The attribution column landed write-only in wave 2; this threads it through
every read surface so the UI can render authorship: StoredEventRow ->
EventMeta -> EventProjectionUserMessage -> TimelineUserConversationRow, plus
the raw events endpoint's ThreadEventRow wrapper. Client-side schemas default
actorHandle to null when parsing responses from pre-multiplayer servers.

Also: /ws accepts the claimed identity via an `identity` query parameter
carrying the same encoded payload, because browsers cannot set custom headers
on WebSocket upgrades.

Includes regenerated plugin-sdk bundled types and templates, and test-fixture
updates for the now-required meta field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… management

WS5 (app): claimed-identity store (localStorage, remote-context-only prompt +
settings section; desktop stays the silent local operator), identity attached
at the single fetch choke point and as ?identity= on the ws URL, presence
store fed by thread-presence/presence-summary broadcasts and reseeded from
GET /api/v1/presence on (re)connect, viewer avatars in the thread header,
typing indicator + throttled typing emit at the composer, sidebar presence
dots with click-to-follow, and author chips on user messages that appear only
when a thread has 2+ distinct human authors. Optimistic user rows stamp the
sender's own handle.

WS6 (surfaces): tunnel-client strips inbound x-bb-via-tunnel and stamps it on
every re-issued request/upgrade, so member management can be owner-console-
only; connect member endpoints also accept the server's own tunnel credential
as owner-equivalent; local GET/POST/DELETE /api/v1/members proxy (rejects
tunnel-origin requests, surfaces enrollment/handle/duplicate errors); SDK
members + presence areas and a claimedIdentity client option; bb members
list/add/remove CLI; guide chapter, bb-cli skill, and docs updated; bb-app
public SDK facade gains the new areas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
High findings:
- Member management is now blocked at the connect gate for all forwarded
  visitor traffic (HTTP and ws-upgrade paths), so tunnels opened by
  pre-marker clients can no longer reach /api/v1/members; the local
  x-bb-via-tunnel check remains as defense in depth.
- Membership mutations commit atomically with their audit rows (D1 batch,
  transaction fallback for the sqlite test double); a failed audit write
  leaves membership unchanged.
- The speaker gate counts only attributed client/turn/requested events, so a
  manual stop by a second person no longer flips a single-author thread out
  of byte-identical provider input (covered by events_thread_type_sequence_idx,
  no new index).

Medium findings:
- Provider resolve, plugin respond, and cancel persist the first resolver's
  handle in the same conditional status transition; racing duplicates cannot
  overwrite it.
- Typing TTLs are per socket, derived per handle, so one device stopping no
  longer clears a person's typing from their other device.
- The app rebinds the live websocket when the claimed identity is saved,
  edited, or cleared (same socket, reconnect + resubscribe + reseed).
- The presence store guards its reconnect snapshot with a generation counter
  so in-flight snapshots cannot clobber newer realtime updates or removals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… one

beginSnapshot now issues a monotonically increasing snapshot id and
applySnapshot drops any response that is no longer the most recently begun
request, so overlapping (re)connect snapshots — e.g. during rapid reconnects
or an identity rebind — can never overwrite the state a newer snapshot
seeded. Per-thread realtime guards are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant