# Shared wind meeting — local Avatar Room scene candidate

This is a reviewable scene layer for the existing Avatar Room, plus a loopback preview. It does not create an app identity, replace the Avatar Room, change live screens, select a source world, read private room captures, or connect remote Nests.

`meeting-scene.mjs` exports `createMeetingScene(existingThreeGroup, {meetingFrameId})`. The host supplies two independent clouds and their placements in a common meeting frame. Each cloud retains its source Nest, frame, stream ID, point identities, provenance and intrinsic edges. The stream adapter separately binds stable participant and selected-volume identities. Proximity creates temporary cross-cloud relations; it never welds participants or changes their intrinsic connectivity.

The preview starts frozen in an **authored demonstration embrace**. Each figure has 13,104 points and 25,682 intrinsic graph edges, including explicit anatomical associations. These are sampled illustrative bodies, not recovered human surfaces, trained models, measured people, Latios/Latias character assets, or an inferred manifold. Their graph is a demonstrable neighborhood structure; no persistent homology, reconstruction or physical contact inference has run.

## Try it

Open <http://127.0.0.1:18983/> while the loopback server is running. Drag to orbit, scroll to approach, enter the scene viewpoint, or reveal graph connections. Scrub Apart → Embrace or play the authored six-second motion; Freeze stops scene time while the view remains movable. Separation moves the two independent volumes closer or through each other.

Both clouds are neutral. Gold is an optional **proximity display cue**, not an intrinsic color of Knightian wind. The proximity radius is adjustable. Proximity calculations run in a Web Worker and have a finite candidate budget; exhausted budgets show an error instead of a fabricated count. While motion runs, proximity waits for freeze rather than displaying old contact as current.

## Local cloud input

Each Load cloud button accepts a single JSON cloud, under 16 MB. The importer requires units and provenance; it does not infer registration or silently fit one person onto another. Coordinates should be in metres with Y up, in the cloud's own frame. The preview places A at `[0,0,-separation/2]`, yaw zero; B at `[0,0,separation/2]`, yaw π. This is manual demonstration placement, not a measured inter-Nest registration.

```json
{
  "id": "presence-A/take-1",
  "nestId": "nest-A",
  "frameId": "nest-A/body-frame-1",
  "units": "m",
  "positions": [0, 1, 0, 0.02, 1, 0, 0.02, 1.02, 0],
  "edges": [0, 1, 1, 2],
  "pointIds": ["p0", "p1", "p2"],
  "provenance": {"kind": "observed", "source": "your source reference"}
}
```

`positions` are flattened XYZ; `edges` are flattened endpoint index pairs. Empty edges mean isolated points, not inferred connectivity. A replacement must have a distinct stream ID; two simultaneous participants must have distinct IDs. Provenance is a supplied label, not independently verified truth. Imports remain in browser memory. Save this scene explicitly downloads a local JSON snapshot with the two clouds and placements; this snapshot is an integration artifact, not a directly reloadable single-cloud file. Nothing is automatically uploaded or retained by a service.

## Host integration and remaining work

Use the existing host scene/clock and pass `put(cloud, {position:[x,y,z], yaw})`. Call `inspect({compute: clockIsFrozen})` for current worker status and measurements. Call `dispose()` when leaving the child scene. Proposed placement remains pending the user's choice; no navigation or live host was changed.

The current presence-fusion endpoint is a room-wide anonymous hull. It is **not** a scoped cross-Nest participant stream, so this candidate has no adapter that exposes it as one. The September 21 continuation adds a transport-independent lifecycle adapter, described below. Live integration still requires independently identified permitted presence volumes, actual clock mapping and source-to-meeting registration, an authenticated scoped transport, and host integration. Body-wind motion alone supplies joints/rotations; it does not supply a dense body surface. A surface or occupancy representation is still needed per participant.

Reused libraries: existing Three.js and OrbitControls from `ec-workspace/tools/embertide-mesh/guest-room/web/vendor`; original license headers retained. No private geometry was copied.

## Verification

Run `node --test meeting-core.test.mjs meeting-scene.test.mjs meeting-streams.test.mjs` in this directory. All 32 checks passed September 21: 13 geometry, five scene, 14 stream-lifecycle checks. Scene tests use real Three.js geometry with a stub worker and stream tests use a controlled host clock. These checks do not establish physical alignment, live two-Nest operation, actual transport latency or device FPS.

Actual in-app browser interaction was inspected on September 20; its coverage is recorded in `../../outputs/wind-meeting-2026-09-20/verification.json`. The separate `verify-preview.cjs` headless harness failed to launch Chrome and its assertions never passed. No screenshot files from that failed harness are claimed. The September 21 continuation changed the reusable scene API, not the preview UI.

## Stream lifecycle adapter — September 21

`meeting-streams.mjs` exports `createMeetingStreams(scene, options)`. This is a local integration candidate, not an adopted Mesh wire protocol. Its input must already be authenticated and limited to the selected participant volume. It does not extract a person from a room hull, crop geometry, verify a grant signature, measure registration, estimate clock offsets, open a connection or start a sensor.

The trusted host admits a stream after its own permission checks. Received packets can update that admitted stream, but cannot create an admission or extend its grant. Each binding pins a participant, source Nest, selected volume, stream epoch, source frame, meeting frame, clock mapping and placement evidence. `manual` and `registered` are host-supplied evidence labels, not independent validation. The current renderer supports translation and Y-axis yaw in metres; arbitrary 6-DoF registration needs a future renderer extension.

```js
import {createMeetingScene} from './meeting-scene.mjs';
import {createMeetingStreams} from './meeting-streams.mjs';

const scene = createMeetingScene(existingThreeGroup, {meetingFrameId: 'meeting/demo'});
const streams = createMeetingStreams(scene); // host milliseconds: performance.now()
const t = performance.now();
streams.admit({ // trusted host call, never run directly from a received packet
  streamId: 'demo-A/epoch-1', participantId: 'demo-person-A',
  nestId: 'demo-nest-A', volumeId: 'demo-selected-body',
  sourceFrameId: 'demo-nest-A/body', meetingFrameId: 'meeting/demo',
  grantRef: 'synthetic-example-only', expiresAtMs: t + 10_000,
  clock: {id: 'demo-clock-1', sourceOriginMs: 0, localOriginMs: t, uncertaintyMs: 0},
  pose: {position: [0, 0, 0], yaw: 0},
  registration: {kind: 'manual', source: 'authored fixture placement'}
});
streams.accept({
  streamId: 'demo-A/epoch-1', sequence: 0, clockId: 'demo-clock-1',
  capturedAtMs: 0, validForMs: 500,
  cloud: {
    id: 'demo-A/epoch-1', nestId: 'demo-nest-A', frameId: 'demo-nest-A/body', units: 'm',
    positions: [0, 1, 0, 0.02, 1, 0], edges: [0, 1], pointIds: ['p', 'q'],
    provenance: {kind: 'synthetic', source: 'two-point contract example'}
  }
});
// In the host's render callback, before rendering:
streams.sweep();
// For a host revocation:
streams.revoke('demo-A/epoch-1');
// When leaving, dispose streams first, then the scene layer:
streams.dispose();
scene.dispose();
```

Sequence numbers strictly increase; capture timestamps are nondecreasing within one source clock epoch. Expiration is the earliest of grant expiry, mapped capture time minus uncertainty plus packet validity, and that same earliest capture time plus `maxAgeMs` (default 1,000 ms). Arrival time cannot refresh old geometry. A definitely future timestamp is rejected; an uncertainty interval touching the present is allowed with the conservative earlier expiration.

Scene time and stream validity use separate clocks. Expiration and revocation remove geometry and proximity even while the authored scene is frozen. The host controls presentation of incoming motion; this adapter applies accepted samples immediately. Timers are a fallback; call `sweep()` before every host render, particularly after suspension, because browser timers can be throttled while hidden. This is not a hard real-time expiry guarantee during a suspended process.

Expiry of either a packet or a grant, and revocation, are terminal for that stream ID. A transmission gap therefore needs explicit host readmission under a fresh stream ID; it cannot silently resurrect a presence. Changes to topology, source frame, clock epoch or registration also require a fresh stream. At most two streams are active by default, and 256 total admissions/tombstones are retained per adapter lifetime. Dispose and create a new meeting session after that limit. Disposal removes adapter-owned geometry and cancels timers; it leaves unrelated static clouds alone.

The existing preview still uses authored static fixtures and reports zero live streams. The adapter is exercised against the renderer in tests; it is not connected to a live transport or installed app. Continuation evidence and pending work: `../../outputs/wind-stream-lifecycle-2026-09-21/README.md` and `../../outputs/claude-continuation-2026-09-21/README.md`.
