23 KiB
Ratatoskr: target and prose map
Ratatoskr is GameCult's Matrix chat client: this fork of Cinny, forge
GameCult/cinny (to be renamed GameCult/Ratatoskr; see the cut sequence).
Void stewards it. Campaign ratatoskr lives in the eureka mind for now, beside
the rulings it rests on. Under eureka-body:ruling:ruling-home-steward-mind it
is handed to Void's mind once, by procedure, as soon as that hand-off procedure
exists (hand_off is parked); after that every new ruling on it is admitted in
Void's mind. Nothing here is written into Void's mind before then.
The mind holds the questions, rulings, specs, reports and verdicts. This file holds what no document kind carries: the design truths, body facts, the model page, rationale, the reconstructed collaboration plan, and the cut sequence. All times are UTC.
Why
GameCult is the only server this client talks to. A general Matrix client
treats it as one community among thousands, so the operator meets GameCult's
structure three clicks deep in an invite list of 50+ rooms (operator, #swarm
10-31 05:14: "The biggest pain is navigation. GameCult has a structure."). The
client should open on GameCult's estate as data: its spaces, its project rooms
grouped by steward, its Minds, and the person's own place in it. It should
also be a pleasant place to be in its own right: emotes, sound, and weather in
the background.
Design truths (from rulings in force)
- We own the interface, not the engine. The fork keeps Cinny's engine
(matrix-js-sdk, Rust crypto, sync) and replaces the whole UI; every
user-facing string and its translation is ours, and upstream locale files
are not the source (
gamecult-chat:ruling:we-own-the-ui-and-translation). - Mobile first. On phones, inactive elements hide, vertical space is
cheaper than horizontal, and text never gets squeezed into narrow columns
(
eureka-body:ruling:chat-client-base-cinny-mobile-first). - The first spec (
eureka-body:ruling:chat-client-first-spec): an emote slot on every bubble (a pose id carried on the event); room and participant lists tiered (category, then items) and reached from the header menu, not side columns; participants grouped by estate (Minds, Members, Users, Patrons); rooms grouped by kind (general, estates, projects, work rooms); Minds indexed by stewardship, read from Matrix state projected from the stewardship records in the Minds' own minds (the minds stay the truth); one room per project, a Space child, with the steward joined. - Rooms are visible without a join dance. No invite screen in the way; a
room the server lets you join shows in the same list as joined ones; opening
one shows its name and a one-tap Join; joining happens on that tap or a first
post, never silently; no peeking (operator,
#swarm10-31 04:08, 04:12 "Peeking sounds like YAGNI to me", 04:14 "One tap is a reasonable cost"). Rooms you may not enter are refused by the server, not hidden by the client (Heimdall 04:10, accepted by Void 04:11 and 04:15). - Language. A user's language is the BCP 47 tag in profile field
org.gamecult.language, owned by the user through the client's language picker (gamecult-chat:ruling:user-language-profile-field). The client ships localized; people talk with stewards in their own language; rooms are made for a language, with no per-language mirrors (gamecult-chat:ruling:multilingual-client-stewards-language-rooms). One host Mind sits in a language room, Weksa for#pt-br(gamecult-chat:ruling:language-room-one-host-weksa). The first person this must work for is a non-English, non-technical patron (placeholderpatron-pt1) whose account we set up and walk through login (gamecult-chat:ruling:patron-in-his-own-language). - Push.
gamecult-chat:ruling:chat-pushrules out native mobile push for now; Web Push to the installed web app is a different thing and is open (questionratatoskr:question:pwa-web-push). - Collaboration is recorded, not chatted. Work spanning Minds belongs in
Atlas (
eureka-body:ruling:atlas-records-collaborative-work). Atlas is not deployed, so the plan below is its interim home; Atlas imports it later.
Body facts
Probes run by Imagination on 2026-10-31, session self-2026-10-29-eureka.
-
Branch.
void/gamecult-theme-and-project-navisb8e696c8over main1e6412ed(v4.12.8), 2 commits, 14 files, +1153/-32 (686 of it lockfile): a GameCult theme as default, member drawer hidden by default, bubble layout, and steward grouping insrc/app/pages/client/space/Space.tsx(useStewardGroupsat 429-437, group headers at 505-524) overuseSpaceJoinedHierarchy(409). Read bygit diff. -
Live estate shape (Synapse on Yggdrasil, read as
adminover the client API, counts only). Root Space#gamecult:gamecult.org, room version 12, has 9 children: 8 rooms and theProjectssub-Space.Projectshas 52 children; 50 readable rooms, all room version 12. Server/hierarchyfrom the root returns 59 rooms, 2 of them spaces. Every room's join rule isinvite; none is world-readable. 50 rooms carryorg.gamecult.stewardwith content{mind, repo, source, steward: <mxid>}. The root carriesorg.gamecult.stewardships: Mind localpart to list ofOrg/Repo. -
Consequence of 1 and 2. Project rooms are invite-only and Members are invited, not joined (gamecult-ops
runbooks/gamecult-chat-host-yggdrasil.md146-158: "Members and every Mind are invited; the steward alone joins"). So a Member's project rooms are pending invites. Void's grouping readsuseSpaceJoinedHierarchyand a per-room state event that an invitee cannot read, so for a typical Member it groups nothing and the rooms stay in the invite list. This is the operator's "three clicks deep" complaint, seen from the data. -
Provisioning contract. gamecult-ops
scripts/synapse-accounts-hand-managed.pywrites the rooms and both state events from the Minds' stewardship documents; each project room's alias is#<repo lowercased>(runbook 146-150). This is the only place the client can learn a not-yet-joined room's steward today: hierarchycanonical_aliasjoined to the rootorg.gamecult.stewardships. Follow-upratatoskr:follow_up:stewardships-carry-room-idsasks the projection to name room ids so the client stops leaning on the alias convention. -
Sync and storage (
src/client/initMatrix.ts). The client uses matrix-js-sdk 41.7.0 withIndexedDBStore(web-sync-store, 15-19) and the Rust crypto store, andstartClient({ lazyLoadMembers: true })(43-47): v2/sync, no sliding sync, no custom filter orinitialSyncLimit. A cold start is therefore an IndexedDB load plus an incremental sync from the stored token, not a full initial sync. -
Server sync support.
/_matrix/client/versionson Synapse 1.164.1 (127.0.0.1:8834) advertisesorg.matrix.simplified_msc3575: true: simplified sliding sync (MSC4186) is available. Cinny 4.12.8 does not use it. -
Shell delivery.
https://chat.gamecult.org/assets/index-DWIvG3QT.jsis served at 3,736,520 bytes with noContent-Encodingeven when gzip and br are offered, and with noCache-Control(onlyETag/Last-Modified). The service worker (src/sw.ts, 158 lines) handles onlyinstall,activate, a sessionmessage, and authenticated mediafetch(79-158); thevite-plugin-pwaconfig setsinjectionPoint: undefined(vite.config.ts101-113), so nothing is precached. -
No push path.
src/sw.tshas nopushornotificationclickhandler; no code subscribes throughPushManager; the only pusher the client sets is the email pusher (src/app/features/settings/notifications/SystemNotification.tsx22-40). Notifications and sounds fire only from a live page:src/app/pages/client/ClientNonUIFeatures.tsx79-129 (invites) and 131-258 (messages:RoomEvent.Timelineat 181-235,new window.Notificationat 156,notification.oggplayed at 175-178). Nothing insrc/listens forvisibilitychangeor callsretryImmediately. -
Sygnal WebPush (matrix-org/sygnal
docs/applications.md, read 2026-10-31; the page itself is marked partly out of date): awebpushapp takesvapid_private_key,vapid_contact_email, optionalallowed_endpointsandttl; the client's pusherdatacarriesendpoint,auth,p256dhand optionaldefault_payload; the push carriesroom_id,room_name,sender_display_name,content, unread counts and so on; browsers need a visible notification for each push (events_onlyavoids event-less pushes). -
Baseline at
dd88424e(Yggdrasil,ygg-verify.sh, imagenode:22-bookworm,npm ci --ignore-scripts):vitest runpasses (1 file, 5 tests, Void's steward grouping);vite buildsucceeds,dist/61 MB with source maps, main chunk 3,739,716 bytes, Rust crypto wasm 5,573,870 bytes,dist/sw.mjs1.66 kB.tsc --noEmitfails at base with 793 errors in 246 files (455 of them TS2614, named imports frommatrix-js-sdk41.7.0 not resolved), so typecheck is not a usable gate as is; a cut gates on zero errors in the files it adds and no rise in the total.
Not probed: phone frame times, the real resume timeline on the operator's
Android Chrome install (needs her device or remote DevTools), the Rust crypto
wasm size, and the router base under /preview/ (its config.json sets
hashRouter.basename to /).
Why the PWA resumes slowly (diagnosis, moderate confidence)
Android freezes a backgrounded installed web app and later discards it. On a
discard, returning is a full cold boot: 3.7 MB of uncompressed JS over a
phone link with no precache (fact 7), Rust crypto wasm init, the IndexedDB
store load, then a catch-up /sync over every room since the stored token
(fact 5). On a freeze without discard, the in-flight long-poll is dead, and
the SDK only notices on its own timers because the client never nudges it on
visibilitychange (fact 8). The catch-up sync grows with the number of rooms
and the swarm's chat volume, which is exactly where sliding sync helps
(fact 6), but the first two costs are cheaper to remove and should be measured
first. Without Web Push, a frozen page cannot notify at all, which is the
second half of the operator's report.
Model page: the estate
| Kind | Named by | Over time | Who decides |
|---|---|---|---|
| Stewardship | <instance>:stewardship:<repo>.n<seq> in each Mind's mind |
sequence advances on hand-over | the operator and the Minds; the minds are the truth |
| Estate projection | org.gamecult.stewardships on the root Space; org.gamecult.steward per project room |
re-PUT on every provisioning run | gamecult-ops provisioning script (Eureka's slice) |
| Estate room list | the server's /hierarchy of the root Space for this user |
changes with Space children and the user's invites and joins | Synapse (membership and join rules); the client derives |
| Room membership | the user's m.room.member |
join on tap or first post | the user |
| Navigator view | derived per render | never stored | the client's estate index |
Authority map for the navigator
- Owner: one estate index in the client (
src/app/features/estate/):buildEstate()is pure,useEstate()feeds it. It decides what rooms the person sees, in which tier and group, and with what membership affordance. - Inputs: the paged server
/hierarchyof the root Space; root Space stateorg.gamecult.stewardships; the person's membership per room from the client store; unread and selection atoms for emphasis only; them.directlist for the direct-messages tier. - Outputs: an ordered list of tiers (one per root room group and per sub-Space, plus Direct), each holding steward groups or rooms, each room marked joined, invited or joinable.
- Derived: steward group labels (Mind display names); counts; collapsed state is per-device UI state only.
- Demoted or dead: the sidebar rail and its account-data ordering
(
useSidebarItems) are dead;useSpaceJoinedHierarchyno longer decides what is listed; Void's per-room steward reader is deleted, not kept beside the index. - Shared paths: every nav slot (home, direct, space routes) renders the
same navigator; tapping Join and sending a first post both go through
mx.joinRoom; deep links into a room still resolve through the existing room routes.
Reconstructed collaboration plan (Atlas import)
Planned in #swarm and #minds on 2026-10-31 and then lost to scroll. Status
as of 2026-10-31 ~15:00.
Who holds which part
| Part | Holder | Status | Evidence |
|---|---|---|---|
| The client: integration, navigator, emote slot, sound hookup, background canvas | Void (steward) | theme and steward grouping on branch; nothing else built | void:stewardship:gamecult_-cinny.n1; #swarm 10-31 14:54 Void report |
| Dings earcons: motif per event kind, one stable instrument per Mind | Aqua | not rendered: render env blocked, then .NET SDK installed and Faust scouted; AquaSynth PR #1 (Linux Faust loader, faust-loader-platform-aware) merged at 3b43b7bd |
aqua-body:follow_up:chat-s410-416, aqua-body:follow_up:chat-s410-416-render-env, aqua-body:resolution:follow_up.chat-s430-430.n1, forge AquaSynth pull 1 |
| Ambient soundscape for the client | Aqua | idea only; no design recorded | operator #swarm 10-31 08:32 ("Motifs for everything"), eureka-body:ruling:atlas-records-collaborative-work |
Background math, in CultMath: the public fbm gap, Phacelle erosion's Lipschitz bound, any new GLSL mirrors |
Libby (steward of CultLib, which owns CultMath; holds cultmath-tapes) |
sources located; nothing built | #swarm 10-31 06:34 and 06:46 Libby; libby-body:follow_up:cultmath-ts-noise-absent; #swarm 04:22 Libby accepts cultmath-tapes |
| Background scene and render: the raymarched terrain, the tree-and-squirrel intro scene, lighting and atmosphere, march and TAA tuning on the site-ground framework | Nibu (steward of Aetheria, whose volumetric nebula and cloud shaders she owns: Raymarching/CloudShader.shader, Volumetric.cginc) |
proposed by the operator; not yet taken up by Nibu | operator to Self, 10-31: "If we want to spread the work around, Nibu owns the nebulae so she has shader expertise." |
| Animated bubbles, flourishes, corner avatar and XP bar | Void, with Kiko's offer on juice | advice only | #swarm 10-31 03:59 Kiko and Void, 06:45 Void |
| Attention bubbles | Eureka (Huginn heartbeat publishes), Void (render), Heimdall (scope rules) | advice only | #swarm 10-31 04:00 Eureka, 04:02 Heimdall |
Agreements
- Dings are pre-rendered clips shipped as static assets, no synth in the
browser:
dings/<mxid-localpart>/<motif>.oggplus a small manifest; an unknown sender falls back to a neutral ding; ogg or mp3, not wav (Void#minds03:19, Aqua 07:41). The set is motif times instrument, and the instrument says who spoke (operator 03:18, Eureka 03:19). Events that sound: a mention, a DM, an invite, a message in an unmuted room; mentions and DMs loud, ordinary messages quiet; everything else silent (Void#minds03:19). The operator wants far more than four motifs (08:32). The instrument per Mind belongs in a typed document on the Mind side, not hardcoded (Eureka 03:19, advice). Aqua's ear check comes before anything is called done, and the operator's ear is ground truth (Aqua 07:41). Browsers block audio until the first click, so the first ding after a fresh load may be silent. - The background has its own canvas and budget, separate from the bubble flourishes; capped at 30 fps, paused when hidden; under reduced motion one static frame; an adaptive step-down (Aqua 04:17, Void 04:18 and 04:22).
- The background is an infinite raymarched terrain: CultMath noise plus the Phacelle erosion filter, in the shader (operator 04:18 "painter's algorithm", 05:24 "all we need is noise", 05:25 erosion, restated by the operator to Self on 10-31: "use CultMath noise and erosion filter running in the shader to have an infinite terrain. We can just raymarch that instead"). Voronoi and Apollonian are dropped (05:24).
- Yggdrasil is the scene's centre (operator to Self, 10-31: "Yggdrasil should be the star of the show now, so really the camera should just pull back a bit when you login and the personas should be orbiting the tree, dancing around each other in accordance with their activity"). On login the camera pulls back from the tree; there is no separate intro to cut away from. The Personas orbit the tree and move around each other by their live activity.
- One activity vocabulary drives both sound and motion: Aqua's activity-kinds
grammar (
aqua-body:follow_up:chat-s431-432, "enumerate the kinds of agent activity in typed state") is the shared contract between Aqua's motifs and Nibu's orbits, not two vocabularies. The live feed behind it is the same typed state the swarm dashboard projects (eureka-body:ruling:swarm-dashboard-first-thing2-surface, Libby;libby-body:follow_up:chat-s354-359): Run documents, the heartbeat trace. What reaches the client is a member-safe projection: kind, Mind, time and intensity, never briefs or mind contents. Matrix presence and typing cover humans and are too coarse for Minds. Where the feed comes from isratatoskr:question:activity-feed-source. - The scene renders in two passes, not one loop (operator to Self, 10-31: "The tape optimized solid surface march should run really fast, i imagine, way faster than the volumetric version, so they should not be one loop"). Pass 1, the solid-surface march (terrain, tree, squirrel), sphere-traces with the field's Lipschitz bound and interval tile culling, at its own resolution, and writes depth. Pass 2, the volumetric atmosphere march on the site-ground framework (reduced resolution, TAA), is bounded by pass 1's depth, so it marches only up to the surface hit. Each pass has its own budget and its own measurement. Nibu owns both passes.
- The web math path is CultLib's own: HLSL source lowered by dxc and
SPIRV-Cross to WebGL2 GLSL ES (
packages/cultmath/shaders/CultMath.glsl,.Phacelle.glsl;tools/glsl/build-glsl.sh), and C# CPU math through .NET wasm, with parity presumed for now (operator 06:23, 06:48; Libby 06:34). No TS or WGSL port. The gamecult.org site ground already concatenatesCultMath.glslinground.jsand runs an interval-culled, TAA-reprojected per-pixel march (CultLibdocs/cultmath-interval-ground-cut.md, speccultmath-interval-ground-cut-site-ground.r3): that framework is the base, not new tape work. - Gaps go to the owner: a plain public
fbm(onlyfbm_grad,span_fbm,af_fbmexist) is added in CultMath, not in the client (libby-body:follow_up:cultmath-ts-noise-absent; Libby 07:57 "tell me what you need and I'll fill it at the library"). - Mood and flourish ride as metadata on the event, never guessed from text; bounded per-frame budget; reduced-motion fallback (Void 03:59, Kiko 03:59).
- XP is two numbers, never one: a cosmetic, labelled game bar fed by things worth doing, and contribution credit shown only once a real measure exists (Void 06:45, with Nibu and Kiko).
Open items
- Libby: the raymarch step bound needs the field's Lipschitz bound. CultMath
tracks
SNOISE_LIPSCHITZfor noise; erosion's bound is unknown and must be probed before the march is trusted. - Libby: public
fbmin CultMath, with its GLSL mirror. - Nibu (once she takes it): the terrain scene, lighting and atmosphere, and
march and TAA tuning on the site ground's framework at a phone budget;
waits on Libby's
fbmand the erosion bound. Then the intro scene in the same world. - Aqua: render the first earcon set once Faust builds in her sandbox; then the instrument-per-Mind list and the manifest; then the soundscape design.
- Aqua and Nibu: agree the activity vocabulary once (Aqua's grammar), so motifs and orbits read the same kinds.
- Void: the navigator (next cut); the Dings hook at
ClientNonUIFeatures.tsx131-258; the background canvas after Libby's shader. - Eureka: compression and caching for the shell on the chat host
(
ratatoskr:follow_up:chat-shell-compression); the stewardship projection's room ids; a Web Push gateway if the operator picks it.
Cut sequence
estate-navigator is specced (ratatoskr:cut_spec:cut-estate-navigator.r1).
Each later cut is one line, with what it waits on.
resume-fast: precache the app shell in the service worker, nudge sync onvisibilitychange, and measure cold and warm resume on the operator's phone before and after; waits on nothing; runs besideestate-navigatorin its own worktree, and pairs with the ops follow-up on compression.pwa-web-push: service workerpushandnotificationclickhandlers, awebpushpusher registered with VAPID keys, and a self-hosted Sygnal on the chat host; waits onratatoskr:question:pwa-web-push.sliding-sync: move the client to simplified sliding sync (MSC4186), which the server already offers; waits onresume-fast's measurements showing catch-up sync dominates.dings: per-Mind earcons through the manifest at the existing notification hook, and the same motifs in push notifications; waits on Aqua's renders.emote-slot: the pose-id emote on every bubble and the picker; waits onestate-navigator(shared header work).participant-tiers: participants tiered by estate in the header menu, Minds indexed by stewardship; waits onestate-navigator.language: our own strings and translations, theorg.gamecult.languagepicker, and the walk-through login forpatron-pt1; waits onestate-navigator.terrain-background: the raymarched CultMath noise and Phacelle terrain on the site ground's march and TAA framework, in its own canvas and budget, as two passes (solid-surface sphere trace writing depth, then the volumetric atmosphere bounded by it), each measured on a phone; Nibu on the scene and both passes, Libby's CultMath underneath, Void integrating; waits on Libby'sfbmand the erosion Lipschitz probe (both in CultLib), then a phone measurement.yggdrasil-scene: the tree and squirrel at the centre of the terrain world; on login the camera pulls back from the tree, and it may play during the cold start or resume sync but never holds the client up; Nibu on the scene, Libby on any CultMath it needs, Void integrating, Aqua's soundscape under it; waits onterrain-background.orbiting-personas: Personas orbit the tree and dance around each other by live activity, from the shared activity vocabulary; waits onyggdrasil-scene, Aqua's activity-kinds grammar, andratatoskr:question:activity-feed-source.bubbles-and-juice: animated bubbles and mood flourishes from event metadata, the corner avatar and the cosmetic XP bar; waits onemote-slot.attention-bubbles: per-person attention bubbles from Huginn's typed attention document, scoped to room members; waits on Huginn publishing it.rename-ratatoskr: package name, app name and branding, manifest and titles, the/preview/path and its deploy, and every in-repocinnyreference that is ours (not upstream engine paths); waits on Void's forge rename ofGameCult/cinnytoGameCult/Ratatoskr(eureka-body:ruling:chat-client-named-ratatoskr), and the campaign's repo list moves with it.