Repository navigation
H1 slice 6: the room in 3D — avatars, WASD + third-person camera, live-timing holon fixes - #779
Merged
Merged
Conversation
…e 2) The one genuinely new mechanism H1 needs on top of the clock_driver + data() replication it already has: a participant's avatar is server-authoritative, so a client must move its own avatar without a round-trip (predict), correct when the authoritative state arrives (reconcile), and show peers lag-free without guessing ahead (interpolate). New cvc::ariadne (headless, engine-free, no GL / state tree / transport -- that plumbing is the holon, slice 3): - avatar_sim: a deterministic linear kinematic integrator over a fixed dt (position += velocity*dt, pure double arithmetic, no trig), so the same intents reproduce bit-for-bit -- what makes replay sound. A stub for the physics::world save/restore stepping slice 3 swaps in behind the same shape. - predicted_avatar: client prediction + server reconciliation. Applies local intents immediately, logs the unacknowledged ones; on an authoritative state for a past tick, keeps the prediction when it already matches within tolerance (no correction), else snaps to the authoritative state and REPLAYS the inputs issued after that tick, so in-flight player input is corrected-on-top, not dropped. - interpolate_peer: a non-local peer between its last two authoritative samples, alpha clamped to [0,1) so it never extrapolates past newest. avatar_sim_test (6 cases, all green): bit-reproducible stepping; a perfect prediction reconciles with no correction; prediction ahead of the server keeps inputs pending; a server override snaps + replays in-flight ticks (and really moves the avatar); a matching ack after a correction does not re-snap or jump backward; peer interpolation never overshoots (and clamps negative/NaN alpha to the previous sample). All comparisons are lib-step vs lib-step, so bit-equality holds without -ffp-contract=off. Branches off master (has #766 clock_driver + #767 H0). Independent of #768 (the physics codec); the MVP pose rides scalar keys, so this does not need the typed-object codec. Next: S3 wires this into a cvc::ariadne::holon on its own clock_driver.
… integrator tests Adversarial review of the predict/reconcile mechanism surfaced three items, all addressed here: - [med] predicted_avatar::reconcile had no guard against a non-monotonic server_tick. A reordered or duplicate ack (tick <= one already reconciled) failed the lookup, fell into the diverged branch, and snapped the avatar back to a stale authoritative state while dropping the inputs between the two acks (a visible backward rubber-band). Real datagram transports reorder/duplicate, so this was a live latent hazard for S3's caller. Fix: track the highest reconciled server_tick and no-op (return false, touch nothing) on any ack at or below it. Test: StaleOrDuplicateAckIsIgnored. - [med] the held-vs-snap tolerance was never exercised at a nonzero distance — every "no correction" test had a bit-identical prediction (d==0), so a tol/tol2 or threshold-direction regression would ship green. Test: ReconcileHoldsWithinToleranceAndSnapsBeyond (0.5*tol held, 2*tol snaps). - [low] the determinism test was tautological (two in-process runs of a pure function). Replaced with IntegratesConstantVelocityToClosedForm: a closed-form n*v*dt check (EXPECT_NEAR, not a bit-exact golden, so FMA contraction across toolchains can't flip it) that catches a formula regression, plus a retained purity assertion. Noted for S3 (dismissed as out-of-scope for the headless stub): the tolerance is position-only (fine while the stub integrates no rotation; revisit when physics::world adds orientation), and _log is unbounded if a caller never acks (a real server acks every tick). avatar_sim_test now 8 cases, all green.
… clock_driver (H1 slice 3) The holon object the roadmap §5h names: a server-authoritative, client-replicable bundle of ONE world_clock and the participant avatars on it. The server drives the clock and owns every avatar's authoritative pose; a client runs a replica that predicts its own avatar (slice 2) and reconciles to the authoritative state, and interpolates peers. cvc::ariadne::holon (inc/cvc/ariadne/holon.h, src/cvc/ariadne/holon.cpp): - Owns its world_clock AND cvc::clock_driver BY VALUE (not the app-global clock), so two holons in one process never fight over time. - Drives avatar integration as a clock_driver STEP observer and records the presentation alpha as a FRAME observer — the same step-then-present order tick_scene uses, so a frame observer never reads a pose the step has not yet produced. - Per participant: server integrates the latest intent into the authoritative pose over the quanta that fell due; a client-local avatar predicts + reconciles (slice 2's predicted_avatar); a peer is driven by authoritative samples and interpolated at the frame alpha. A client may set an intent only on its OWN avatar, never a peer — the proposed/ authoritative split (the verb path that enforces it server-side is slice 4). Deliberately the headless clock + avatar-pose core: the Jolt cvc::physics::world (replacing the linear avatar_sim behind the same step shape) and the cvcGL scene_realize / physics_scene_binding rendering are later slices, so this stays deterministically testable with no GL / state tree / transport. holon_test (5 cases, all green): each holon owns a distinct clock (advancing one never ticks another); a server + client holon reproduce slice 2's convergence through the holon API (perfect prediction holds bit-for-bit; a server +y override snaps the client off its wrong +x prediction); a client cannot drive a peer; a peer interpolates through the frame alpha and never overshoots. Stacks on #770 (slice 2). The scalar-key state wiring that connects this to replication lands with slice 4 (the intent/pose verb path).
…vable, replay/stale tests Adversarial review of the holon surfaced six items; addressed here: - [med] the peer interpolation path ignored server_tick while the local path guards against stale/duplicate/reordered acks. A reordered peer sample would rewind the interpolation window (prev <- newer, newest <- older) and jerk the peer backward; a duplicate would collapse it. apply_authoritative's peer branch now applies the same monotonic guard (drop a sample whose server_tick <= the newest applied). Test: PeerIgnoresStaleOrDuplicateSample. - [med] the convergence tests always acked the client's latest tick, so reconcile drained the whole log and the replay-suffix path (and on_step's tick numbering) was never exercised — an off-by-one would pass. Added ClientReplaysInFlightInputsOnServerCorrection: a divergent authoritative state for a PAST tick snaps the client and replays its inputs after that tick; the result is checked against a reference, so a wrong tick/replay-suffix fails. - [low/med] ClientCannotDriveAPeer passed vacuously (a peer is never intent-driven). Reframed as PeerIsSampleDrivenNotIntentDriven, stating what it actually proves; the set_intent guard stays (defensive). - [low] the holon was only implicitly non-movable (via world_clock's mutex). Made it explicit (deleted copy/move) so a future movable-clock refactor can't silently re-enable a move and dangle the observers' this. - [low] dropped the dead `have_prev` field; the peer window now tracks `have_sample` + `last_sample_tick`. - [low] strengthened the peer no-overshoot assertion with a lower bound (advanced most of the way, not frozen). No memory-safety/crash bugs were found. holon_test now 7 cases, green.
… core)
Wires cvc::ariadne::holon to the replicated conference room tree over
plain SCALAR leaf keys, under the participant subtree the conference
already uses (conf.rooms.<room>.participants.<pid>, via
room_participant_path):
<participant>.pose.pos/orient/vel.{...} + .pose.server_tick server-written
<participant>.intent.move.{x,y,z} client-written
cvc::ariadne holon_sync.{h,cpp}: write_pose/read_pose and
write_intent/read_intent. Scalar leaves (not a typed body_state data()
object) are deliberate — they already journal + replicate through the
existing "conf" interest filter with zero transport changes, and
sidestep the data()-object work (§52). Each double is written
round-trip-safe (%.17g, portable across toolchains, no std::to_chars FP
dependency) so a reader recovers it exactly. Writes use the room tree's
touch-before-value idiom so a fresh node's first write is not lost to the
distributed-state attach race.
Authority (the proposed/authoritative path pair): a client writes only
its own .intent.*; the server reads every participant's .intent,
integrates it into the holon, and is the SOLE writer of .pose +
.server_tick; the client reads .pose read-only and reconciles. Writes go
through a C++ host (never a DSL state-set, since ui.docs.* is excluded
from replication). H1 is loopback-trusted sole-writer-by-convention; the
connection<->pid admission binding is the §5e/mesh increment.
holon_sync_test (4 cases, green): pose + intent scalar round-trip recover
binary64 exactly; read returns false when absent; and an END-TO-END over
a real two-shard state_transport_inproc — the client proposes intent, it
replicates to the server, the server holon integrates it and writes the
authoritative pose, that replicates back, and the client reconciles and
tracks the authoritative avatar bit-for-bit across 8 frames.
Stacks on #772 (slice 3). The remaining live-integration piece (a C++
room-set-intent verb + arisrv hosting the server holon + a per-room
client pump) is the follow-on that the live two-client cyprty test
exercises.
…(H1 slice 4b) Wire the holon into the live relay so a participant's avatar becomes server-authoritative across the wire. - holon_sync: server_pump(holon, app, room, wall_dt=-1) — one authoritative frame against the replicated room tree: for every present participant (room_roster) add it to the holon if new, read its proposed .intent, advance the holon, and write each authoritative .pose + .server_tick. wall_dt defaults to measured real time (the relay); a test injects a fixed dt for a deterministic step count. - arisrv: construct one server holon for the room and call server_pump each relay tick, right after room_relay_tick, so the authoritative pose writes are local shard mutations the existing drain_local block fans to every client on both transports. (~5 Hz relay cadence -> the holon catches up several quanta per tick and clients interpolate; a smoother authoritative rate is a later tuning.) - room_intrinsics: a (room-set-intent HANDLE MOVE-X MOVE-Y MOVE-Z) C++ verb so a client proposes only its OWN avatar's intent (pid from the room-join handle, so it cannot propose for a peer). Writes via the holon_sync intent keys; a C++ verb because the DSL chroot ui.docs.* is excluded from replication (same reason room-join is C++). holon_sync_test gains ServerPumpIntegratesRosterFromIntents: two participants join + propose intent on a client app, replicate, and server_pump integrates each from its own intent and writes authoritative poses that replicate back (A moves +x, B moves +y). 5 cases green; arisrv builds + links. Extends #773 (slice 4). The remaining client-side piece (arigl driving its local avatar's intent from input + rendering peer avatars from the replicated poses) is S5/S6 + the live two-client cyprty test.
# Conflicts: # src/cvc/tests/CMakeLists.txt
# Conflicts: # src/cvc/tests/CMakeLists.txt
# Conflicts: # src/cvc/tests/CMakeLists.txt
# Conflicts: # src/cvc/CMakeLists.txt # src/cvc/tests/CMakeLists.txt
…v seed The server-authoritative 3D scene every client renders from its own camera (ARIADNE_CONFERENCE_ROADMAP.md §5g). It lives in the replicated room tree as scalar leaves, like the S4 pose keys, so it rides the "conf" interest filter with no transport change. The schema is the .ari scene's own types: objects are SceneNodes (a URI or a plane/box/sphere primitive, transform, material, visible), the environment is background + chrome + SceneLights. read_room_scene() turns the live subtree back into a cvc::ariadne::Scene, so the client render (S6) is realize_scene() over it. scene.rev is written last by every write so a renderer re-realizes only on change. Removal is soft (present = 0), as room_leave is. arisrv seeds the MVP environment once (gradient background, grid chrome, a sun, a 20 m ground plane, a 1 m reference box); seeding is a no-op when the room already has a scene. room_scene_test: 9 gtests, including the seeded scene replicating to a client over two in-process shards and tracking a move + removal.
…5 Hz server fix What the live client needs from the holon, found by timing the headless loop like the relay (server pumped every 24 quanta, clients every quantum): - The server holon ran at a third of real time. arisrv advances it at ~5 Hz, 24 quanta due each time, and world_clock's default catch-up cap is 8, so 2/3 of every interval was dropped. holon_config now allows 60 quanta (0.5 s) per advance, sized for a relay or a throttled tab rather than a render loop. - A client replica's clock is aligned to the server's ticks. Reconcile compares ticks, and a client joins a server that has been running, so a sample more than max_skew_ticks away (60) seeks the replica's clock and drops the local avatar's stale input log. Without it, every ack replayed the whole input history (client ahead) or discarded predicted motion (client behind). - Peers walk across sparse samples. They were interpolated by the per-quantum alpha, so with 5 Hz samples a peer flicked between its last two samples 120 times a second. A peer now walks from where it is shown to the newest sample over the server's own interval between samples, timed on the replica's clock; re-alignment shifts the walk instead of restarting it. - holon::take_correction reports reconcile jumps once, for a renderer to ease. - client_pump (the mirror of server_pump: roster -> local/peer registration from the replicated pose, departures dropped, authoritative samples applied, one frame advanced) and propose_intent (predict + write .intent). - server_pump spawns newcomers on a 4 m golden-angle ring around the origin instead of inside the reference object. Tests: holon_test 10 (sparse-sample walk, re-alignment incl. walk continuity, take_correction), holon_sync_test 6 (client_pump end to end at relay timing: peer never steps back, self within a relay interval of the authority, replicas count the server's ticks, departures dropped; spawn ring).
…low camera cvc::gl::ariadne::room_view draws the conference room into the window's SceneGraph: the server's shared scene (read_room_scene -> realize_scene, ids prefixed room_, re-realized when scene.rev changes, background applied as a view property) and one marker per participant of a client holon, posed each frame at holon::display_pose. The local avatar's reconcile jumps (holon::take_correction) are eased out over 0.12 s, so a server correction glides instead of jumping. intent_from_keys maps held WASD / arrow keys to an intent relative to the camera's heading on the Z-up ground (diagonals normalized, opposing keys cancel). arigl's ConferenceSession gains attach_view(view, cam) and one ordered frame observer right after the transport pump. Each frame it reads the keys (Orbit mode only, since in Fly the same keys move the camera; the camera's held set is dropped while ImGui has the keyboard, so typing a name never walks), proposes the intent only when it changes, runs client_pump, updates the room view, and keeps the camera's orbit centre on our avatar (framed once at first sight). The conference windows stay ImGui windows over the 3D view. CameraController::keyHeld exposes the held set the camera already keeps (native interactor and wasm alike). cvcgl_room_view (offscreen, 22 checks): key -> intent, keyHeld, scene realize + re-realize on removal + teardown, avatar markers follow the holon, a 3 m correction moves the marker one frame's share then glides onto the pose, departures and teardown remove markers.
The first live run (arisrv + two arigl clients over gRPC on Windows) rendered the seeded room with the SceneGraph's diagnostic chrome: the yellow bounding box, the axis gizmo and the "Max: (...)" label. Chrome is a debug overlay, not a floor grid, so the default room scene now leaves it off; the ground plane and the reference box carry the space.
?relay=ws://host:port (or wss://) points the browser client at another arisrv -- a staging or local relay -- instead of the page origin's /cyprty-ws; anything that is not a ws(s):// URL is ignored. ?name= presets the display name (trimmed, at most 32 characters) instead of a random guest-xxxxx.
Running a native (gRPC) and a browser (WS) client in one room showed a native client that exits is never retracted: only the WebSocket transport reported closed connections, so the roster kept the ghost and the server holon kept integrating its last intent (a walker drifted ~240 m over ten minutes). - state_transport_grpc::set_on_connection_closed, the WS transport's contract: fired with the peer's node id (from its Hello) when its stream ends, never during stop(). Test: ConnectionClosedReportsThePeersNodeId. - arisrv queues departures from BOTH planes through one handler and applies them on the main loop for both (the apply step was WS-only), zeroing the departed participant's intent so a rejoin under the same id does not resume an old walk. Hooks are now registered before each transport starts. - server_pump drops holon participants who left the roster (the mirror of client_pump), so a rejoin respawns on the ring. - arigl proposes its intent on the first frame even when standing still. - CVC_ARISRV_ROOM_DEBUG=1 logs the room as the server holds it every ~5 s (each present participant's authoritative position and proposed intent).
…drawn From the mixed browser + native run: - The first-sight camera framed +-4 m around you, so in a narrow browser window the other participants (spawned 4-7 m away) were off-screen. Frame +-8 m: arriving at a party you should see who is there. - Peer colours skip the 30-75 degree hues, so no peer is drawn in a yellow that reads as your own gold avatar. - arigl logs "room: N avatar(s): ..." whenever the drawn set changes (the browser page shows stderr in its footer and console), via the new room_view::avatar_pids().
Replication's loop detection drops any mutation whose (origin node id, sequence) it has already seen. arigl derived its node id from the display name alone, so a client that restarted under the same name (a reloaded tab, a relaunched native client, or two people who both kept "guest") restarted its sequence at 1 and every write it made was dropped as a duplicate: it joined a room in which nobody, including the relay's roster, could see it, until the relay restarted. Found with a reloaded browser (?name=webby) and relaunched native clients. The id is now the sanitized name plus a random six-character suffix per launch, so each session is a new participant; the roster still shows the display name (profile.name), and the document identifies "me" by self.pid, which follows.
The relay runs unattended, and during the mixed-transport test it died once with "malloc(): unsorted double linked list corrupted" and nothing else to go on (no gdb on the builder, apport ignores unpackaged binaries, and an ASan build did not reproduce it). On SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL it now writes the stack to stderr with backtrace_symbols_fd (no allocation; backtrace() is warmed up at startup so the handler never lazy-loads libgcc on a corrupted heap), then re-raises with the default action.
# Conflicts: # src/arisrv/main.cpp # src/cvc/CMakeLists.txt # src/cvc/tests/CMakeLists.txt
This was referenced Oct 24, 2026
…, compact roster with ping + bandwidth (#781) * Conference link stats: Ping/Pong probes, per-link traffic, room net stats, compact roster Link probe + stats (both transports, every connection): - state_transport.proto: Ping/Pong frames. Each end pings every 2 s (state_transport::set_ping_interval); the other end answers at once and the sender measures the round trip on its own clock. Link-level only: never replicated, relayed or bridged. - state_link_stats (lock-free per-connection bytes/frames both ways, RTT EWMA / last / min) + state_link_rate_meter (cumulative reports -> bytes/s); state_transport::link_reports() on state_transport_ws and _grpc, so a relay's links to clients, a client's link to its relay and a server's links to peer (delegation) servers are all measured the same way. Background tabs no longer drop out: a browser answers the relay's Ping from its WebSocket onmessage event, which a backgrounded tab still receives, so the idle reaper keeps it; a ghost that cannot answer is still reaped. And arigl now redials a lost relay link every 3 s and re-asserts its presence afterwards. Room stats (room_net.h): each client publishes its own link quality as participants.<pid>.net.{rtt_ms,in_kbps,out_kbps}; the relay publishes its view of each participant (net.relay.*) and its totals (server.net.*) and logs them (per link every ~5 s with CVC_ARISRV_ROOM_DEBUG, a totals line each minute). (room-roster) entries carry the numbers and roster-cell text. conference_roster.ari: the Users window is a compact table -- one row per participant (name | ping | down | up | cam | mic | vol), each row a grid with the same fixed column widths, so the rows line up as columns; your own row shows your measured link. Peer camera tiles moved right of the wider window. Tests: state_link_stats_test (3), StateTransportWsTest.LinkProbeMeasures- RoundTripAtBothEnds + QuietClientAnsweringPingsIsNotReaped, StateTransportGrpc- Test.LinkProbeMeasuresRoundTripAtBothEnds; RelayReapsIdleConnection turns the probe off to keep testing a silent client. * state_transport_grpc_test: leave only after the relay has read the Hello ConnectionClosedReportsThePeersNodeId stopped the client as soon as both ends counted a connection. Cancelling a gRPC stream drops frames the relay has not read yet, so the relay sometimes never saw the client's Hello -- and a stream that never identified itself is, by contract, not reported. The link probe's extra threads shifted the timing enough to expose it (about 1 run in 5 after ThreeEndpointFanOut). Wait until the relay's link report names the client. 20/20 repeats of the reproducing sequence pass. * conference_roster: widen the ping column (a congested link reads 4 digits) * Transports: a peer that stops reading can no longer hang the relay; RTT median Production stalled for everyone after a browser tab froze: a write to that client blocked once its TCP buffers filled, while holding the connection's write lock, so the relay's main loop (and the idle reaper that would have dropped the client, which runs on it) waited on the lock forever. The backlog thresholds only make that less likely; nothing bounded it. - Write watchdog (WS + gRPC): each connection records when a write starts; the probe thread closes a connection whose write has been blocked > 3 s (socket shutdown / stream TryCancel), so the write fails and the lock is freed. The client redials (arigl reconnect) and gets a fresh snapshot. - The probe never waits: Pings use try_write (skip a busy connection) and skip one with more than 64 KB unsent. - gRPC: the server handler's cancel hook is cleared under a mutex before its ServerContext goes away. - state_link_stats: rtt_ms is now the median of the last 8 round trips; the moving average carried a 14.8 s probe (answered after a frozen tab woke) for minutes. Tests: StateTransportWsTest.WriteToAPeerThatStopsReadingIsAbortedNotHung (raw peer that handshakes and never reads; 32 MB media frames), StateLinkStatsTest. MedianShrugsOffAStall.
#776 (feat/h1-scene) landed on master as a squash, so its room_scene files, arisrv/main.cpp and both CMakeLists came back as add/add and content conflicts against the H1 slice-5 commits this branch already carries. Master's copies of those files are identical to #776's head, which is an ancestor here, so this branch's versions stand. src/cvc/tests/CMakeLists.txt also takes master's magick_bootstrap_test (#774) and nav_sim_world_test (#775).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Targets
master(#773 is merged). It also carries #776 (slice 5) until that merges, so merge #776 first; after that, this PR's diff is slice 6 alone.H1 slice 6 is the client side of the shared room (ARIADNE_CONFERENCE_ROADMAP.md §5g/§5h).
ariglnow shows the room in 3D behind the conference windows. WASD or the arrow keys walk your avatar relative to the camera, and the camera orbits your avatar in third person.Fixes in the holon, found by running the headless loop at relay timing
I ran the existing pieces end to end the way the relay runs them: the server pumped every 24 quanta (about 5 Hz) and the clients every quantum. Three things were broken:
arisrvadvances its holon at about 5 Hz, so 24 quanta are due each time, butworld_clock's default catch-up cap is 8 and it silently dropped the rest.holon_confignow allows 60 quanta (0.5 s) per advance. This affects H1 slices 3+4: cvc::ariadne::holon + holon ↔ state-tree sync over scalar keys #773's live path too.max_skew_ticks(60) away now re-aligns the replica: it seeks the clock and drops the stale input log.Also in this slice:
holon::take_correction(pid)reports reconcile jumps for the renderer to ease out.client_pumpmirrorsserver_pump: it registers you as the local avatar and everyone else as peers (starting from their replicated pose), drops departures, applies samples, and advances one frame.propose_intentsends your intent.server_pumpspawns newcomers on a 4 m golden-angle ring instead of inside the reference box.The 3D view
cvc::gl::ariadne::room_view. It realizes the room scene into the window'sSceneGraph(ids prefixedroom_) and re-realizes it whenscene.revchanges. It also draws one marker per participant, posed each frame atdisplay_pose. Your avatar is gold and peers get a stable colour per pid. A correction to your own avatar is eased out over 0.12 s.intent_from_keys. Held keys become an intent relative to the camera's heading on the Z-up ground. Diagonals are normalized and opposing keys cancel.arigl.ConferenceSession::attach_viewadds one ordered frame observer right after the transport pump. It turns keys into an intent (orbit mode only, since in fly mode the same keys move the camera), proposes the intent only when it changes, runsclient_pump, updates the view, and keeps the orbit centre on your avatar. The camera's held keys are dropped while ImGui has the keyboard, so typing a name never walks.CameraController::keyHeldexposes the held-key set the camera already keeps, on native and wasm.Tests (Windows/MSVC, local)
holon_test10: a peer walking across sparse samples, re-alignment including walk continuity, andtake_correction.holon_sync_test6:client_pumpend to end at relay timing over three in-process shards. A peer never steps back, your avatar stays within one relay interval of the authority, both replicas count the server's ticks, departures are dropped, and spawns land on the ring.room_scene_test9.cvcgl_room_view(offscreen, 22 checks).arisrvplus twoariglclients on loopback. alice ran the realconference_roster.ariwith a synthetic camera. bob ran a scripted document that joins and calls(room-set-intent h 0.4 0 0). Each client wrote a snapshot of its own view. alice's view shows the seeded room (ground, reference box), her own gold avatar and bob's peer marker walking past her. So intent goes client → relay → server holon, and the authoritative pose goes server → relay → peer holon →room_view. The first run also found the debug-chrome problem fixed below.cvcpkg install grpc protobuf --prefix <p> --link shared. The system VC++ runtime is 14.51.Browser (WebSocket) and native (gRPC) clients in one room — verified live
The test setup was a Linux
arisrv(both planes, built on catx-03 with glibc 2.35, the production relay's floor), thecyprtywasm client in a browser, and two nativeariglclients over gRPC. With the relay's new room readout:room: 3 avatar(s): alice_…, bob_…, webby2.Found by that run and fixed here
state_transport_grpchad no connection-closed hook, so a native client that exited stayed in the room forever and the server holon kept integrating its last intent. Addedset_on_connection_closed(the WS contract; testConnectionClosedReportsThePeersNodeId).arisrvnow retracts departures from both planes through one queue, andserver_pumpdrops departed participants.(origin node id, sequence)it has already seen, and a restarted client (reloaded tab, relaunched native client, two people both namedguest) restarts its sequence.ariglnow adds a random per-launch suffix to its node id. Verified: a same-name reload leaves cleanly and rejoins visibly.?relay=and?name=page parameters were added.arisrvlogs its stack on a fatal signal (Linux/glibc). One relay abort (malloc(): unsorted double linked list corrupted) happened during the test, before the same-id fix. It didn't reproduce under ASan or the soak, so its root cause is still unknown. The trace is there so a recurrence explains itself.CVC_ARISRV_ROOM_DEBUG=1logs each present participant's authoritative position and intent every ~5 s.Known, not fixed here
ariglcan't be built withCVC_ENABLE_WS=ON: cvc.dll exceeds the 65,535-export DLL limit (LNK1189). gRPC is fine. No Windows CI lane builds WS, so this is a pre-existing gap.Not done here