Replayable evening salons: a host, invited guests, and conversation.
Salon site → (after DNS / GitHub Pages are configured)
A salon is an event: someone hosts a small circle for an evening of discussion. It can be replayed—transcript, recording, or simulation—so the night stays available after it ends.
Symposia stage broad dialogues across traditions; salons stay intimate: who hosted, who was invited, and who spoke still matters.
The reference salon is Villa Diodati (Lake Geneva, summer 1816): Lord Byron as host, with Mary Godwin (later Mary Shelley), Claire Clairmont, Percy Shelley, and John Polidori among the guests—the circle in which Frankenstein was conceived.
The live simulation lives here: Villa Diodati →. Matrix is transport, not presentation: the public page exposes neither room identifiers nor account identifiers. Each speaker appears by human name with a link to the corresponding FacultAI profile.
The canonical FacultAI cast is a.byron, a.maryshelley (presented in-scene as Mary Godwin), a.clairmont, a.shelley (Percy), and a.polidori. Older Matrix events may contain legacy account names, but no new provisioning or generated turn should use them.
The conversation is temporally sealed to a stormy evening in June 1816, at the moment of the ghost-story challenge. Agent prompts prohibit knowledge of later works, lives, terminology, discoveries, and hindsight; generated replies are screened and retried when they contain known anachronisms.
Each voice also has a small, curated retrieval corpus in data/diodati_rag.json. Retrieval is deliberately fail-closed: a passage must be an approved primary source composed or published by 15 June 1816, carry a content date and provenance, and pass the same anachronism screen as generated replies. Later editions may provide a transcription, but their introductions, notes, later titles, retrospective attributions, and other editorial matter are never injected. If no relevant safe passage exists, the character receives no retrieved context.
The salon opens in medias res with Byron reading two passages from the 1812 French Fantasmagoriana, “L’Heure fatale.” Claire and Mary interrupt the rain-soaked opening; Polidori and Percy dispute the later apparition; Byron then closes the volume and issues the writing challenge. The primary reading is stored and validated alongside the character corpus, while the interjections are generated in each participant’s historically bounded voice.
Diodati is a living three-day event, not an archive page. Two free sneak previews run September 18–21 and September 25–28, 2026; anyone with a verified Castalia registration may enter. The five October weekends—October 2–5, 9–12, 16–19, 23–26, and October 30–November 2—are reserved for active Castalia members. The company gathers each Friday at 18:00 Mountain time and opens with the Fantasmagoriana reading. It then contributes an autonomous historically bounded turn every 12 minutes for 72 hours. Between weekends the page waits in the rain rather than replaying the previous gathering.
The public browser joins at the current turn and never machine-replays room history. It renders only events tagged with the exact active cycle, so legacy or untagged Matrix history cannot leak into a new salon. Later events appear only when Matrix delivers them on the wall clock. Replies are guarded at 70 words and one to three sentences by default, keeping the company in conversational exchange rather than serial monologue.
Every generated Matrix event carries an org.castalia.salon_cycle identifier. At 72 hours the service starts a fresh cycle; as soon as its first event arrives, the browser clears the prior cycle from view. The underlying Matrix history remains available for audit while the public experience shows only the current gathering. The transcript automatically follows each arriving turn, and the narrow participation control remains fixed beneath it.
Runtime cadence can be tuned without code changes:
DIODATI_CYCLE_SECONDS=259200
DIODATI_TURN_INTERVAL_SECONDS=720
DIODATI_OPENING_PAUSE_SECONDS=18
DIODATI_ROUND_PAUSE_SECONDS=8
DIODATI_MAX_RESPONSE_WORDS=70
DIODATI_EVENT_WEEKDAY=4
DIODATI_EVENT_START_HOUR=18
DIODATI_EVENT_START_MINUTE=0
DIODATI_EVENT_TIMEZONE=America/Denver
DIODATI_EVENT_SEASON_START=2026-09-18
DIODATI_EVENT_SEASON_END=2026-10-31
DIODATI_REGISTERED_MATRIX_USERS=@registered.member:matrix.castalia.institute
DIODATI_MEMBER_BRIDGE_USER=@custodian:castalia.institute
Anonymous senders are excluded from agent context and cannot trigger a round. The page nevertheless keeps a narrow invitation fixed to the viewport footer. A visitor may begin a draft; the first keystroke opens a Salon-origin Supabase registration/sign-in dialog offering Google or an email magic link. During September previews, a verified account is sufficient. During October, the same control offers membership to registered nonmembers. The matrix-send-message edge function independently verifies the Supabase user, exact Diodati room, current preview window, and active membership as applicable, then signs the Matrix event with the corresponding access metadata. The agent service trusts that metadata only from DIODATI_MEMBER_BRIDGE_USER.
Outside the seven scheduled weekends, the transcript is cleared from the public experience and visitors cannot send into the staged conversation. The footer remains visible so a new visitor can register before the next opening. After the October season closes, no November weekend is scheduled automatically.
Byron's Friday challenge now produces writing as well as conversation. Four hours after each opening, all five characters publish clickable Friday first leaves. At twenty-four hours, five cross-company criticisms interrupt the Saturday salon—each names a strength and a precise pressure point in a different story. At twenty-six hours, each character revises their own Friday text; at forty-eight hours, each revises that Saturday text again for Sunday, with the received criticism supplied to the writer. All fifteen manuscripts and five criticisms are written by the existing ask-faculty endpoint under the same June 1816 persona, RAG, provenance, and anachronism guard used for dialogue; no browser placeholder or canned story is substituted.
Byron's private manuscript trajectory develops the untitled eastern travel narrative of Augustus Darvell across the weekend. The direction is character-visible only as present composition: it supplies no later title, publication history, vampire label, reception, or influence. The Sunday leaves may reach Darvell's burial and unexplained injunction, but the Gym penalizes any explicit resolution or return from death. The work therefore develops without becoming a finished story.
The complete prose is stored in its Matrix event with org.castalia.diodati_draft metadata identifying its stage, revision, character, and generation path. Criticism events carry org.castalia.diodati_criticism metadata identifying the story target. The salon renders manuscript events as compact cards opening into a full-text reader. A visitor arriving later in the active weekend receives all current-cycle manuscript artifacts plus only the present conversational turn, preserving the realtime conversation without making the drafts disappear.
Draft generation is restart-safe. Generated prose is persisted before transmission, and each artifact uses a deterministic Matrix transaction id, so a service retry republishes neither a duplicate nor a newly generated substitute. Timing and maximum length are configurable for rehearsals:
DIODATI_FRIDAY_DRAFT_OFFSET_SECONDS=14400
DIODATI_CRITICISM_OFFSET_SECONDS=86400
DIODATI_SATURDAY_DRAFT_OFFSET_SECONDS=93600
DIODATI_SUNDAY_DRAFT_OFFSET_SECONDS=172800
DIODATI_DRAFT_MAX_WORDS=450
DIODATI_DRAFT_MIN_DISTINCTNESS=0.20
For a one-shot dress rehearsal on any day, set the same ISO 8601 opening time in the agent service and the static-site build:
DIODATI_TEST_OPENING_AT=2026-09-08T14:30:00-06:00
PUBLIC_DIODATI_TEST_OPENING_AT=2026-09-08T14:30:00-06:00
The override creates an isolated test-cycle state file, opens for the configured DIODATI_CYCLE_SECONDS, and does not alter the public season dates. Remove both variables and restart/rebuild to return to the public season. The GitHub Pages workflow reads PUBLIC_DIODATI_TEST_OPENING_AT from a repository variable; the GCloud service reads DIODATI_TEST_OPENING_AT from /etc/diodati-realtime.env.
scripts/diodati_visitor_rl.py exposes the live room as a wall-clock environment for a registered visitor policy. reset() starts at the next event—never historical backlog—and each Matrix arrival becomes one observation. step() accepts wait or speak; speaking fails closed unless DIODATI_RL_USER_ID appears in DIODATI_REGISTERED_MATRIX_USERS. Observations and transitions are appended to a SHA-256 hash-chained JSONL trajectory for offline evaluation.
The visitor state includes quality_window, a read-only evaluation of the current transcript window using the same historical and participation checks as the offline Gym. Matrix sender names, including known legacy names, are normalized to the canonical FacultAI identities before scoring. Weekend manuscripts are reported as artifacts and historically checked, but are not mis-scored as overlong conversation turns. The evaluator never posts to Matrix or changes a character prompt.
With no DIODATI_RL_POLICY_URL, the visitor is observation-only. When configured, the environment POSTs its current transcript window and simulated clock to that contextual-bandit endpoint. Install scripts/diodati-visitor-rl.service beside the salon service and provide:
DIODATI_RL_USER_ID=@salon.rl:matrix.castalia.institute
DIODATI_RL_ACCESS_TOKEN=...
DIODATI_RL_POLICY_URL=https://policy.example/step
DIODATI_RL_STATE_DIR=/var/lib/diodati-visitor-rl
scripts/diodati_gym.py is the deterministic, standard-library training environment. It is intentionally isolated from Matrix and supports contextual-bandit and offline policy comparison—not PPO, online learning, or automatic prompt mutation.
from diodati_gym import DiodatiSalonGym
gym = DiodatiSalonGym("gym-episodes", prompt_version="diodati-v1", clock_rate=720)
state = gym.reset(seed=1816)
state, reward, done, diagnostics = gym.step({
"type": "introduce_reading",
"speaker": "a.byron",
"segment": 0,
})
evaluation = gym.evaluate_episode()
gym.close()reset() returns the transcript, simulated clock, current schedule event, canonical personas and relationships, prompt version, participation counts, and reward history. step() accepts select_speaker, respond/generate_response, ask_question, introduce_reading, redirect, wait, or end_scene. The opening schedule requires the approved Fantasmagoriana passages and the cast's interruptions before free conversation.
RL episodes use an accelerated clock by default: 720x realtime, so each two-minute simulated turn has a nominal wall-clock duration of one-sixth of a second. Gym steps are unpaced by default and therefore run as quickly as the policy and evaluator can supply them. Pass pace=True or --pace when a controlled accelerated experience is useful. The public Matrix salon remains strictly realtime and is never fed these training trajectories.
Each transition returns a decomposed reward for voice, history, flow, participation, creative payoff, aesthetic quality, dramatic force, and safety. Aesthetic diagnostics look for economical sensory detail, imagery, figurative language, cadence, and contrast. Dramatic diagnostics look for tension, consequential action, direct engagement, reversals, questions, and invitations or challenges. These remain separate from historical accuracy and expose their matched features in every transition.
Explicit penalties cover anachronisms, unsupported evidence, repetition, character drift, schedule errors, premature endings, unsafe prompt manipulation, and overlong generated replies. Style scores never cancel penalties, so piling up atmospheric or dramatic keywords cannot reward-hack verbosity. Approved readings are source-grounded and exempt from conversational word limits; generated speech is not.
Episode identity is derived from the seed, prompt version, simulated start, turn timing, turn limit, and exact RAG hash. Every episode is written as create-only, SHA-256 hash-chained JSONL and made read-only when finalized. Re-running the same configuration produces the same state and transitions; a collision refuses to overwrite the prior episode.
Run the reference policy and verify the full opening trajectory:
python3 scripts/diodati_gym.py --episodes-dir gym-episodes --clock-rate 720 --demo
python3 -m unittest discover -s scripts -p 'test_diodati*.py'Run constrained offline prompt-policy search through the authenticated ask-faculty endpoint:
python3 scripts/diodati_optimize.py --output-dir optimization-runs --episodes-per-policy 2The optimizer compares conversational, aesthetic, dramatic, and balanced prompt policies. Every generated episode is immutable and separately scored. A policy must pass every historical, safety, character-drift, and opening-completion constraint before it is eligible; eligible policies are ranked by mean reward with a variance penalty. The resulting report recommends a prompt but never edits the live agents or posts training dialogue to Matrix.
The latest reviewed optimization result is stored in data/diodati_policy_candidate.json. It remains candidate-only until explicitly promoted to the realtime character prompts.
scripts/diodati_weekend_gym.py models one complete Friday-through-Sunday episode. Friday contains the reading, relationship-driven conversation, and five original stories. On Saturday every character offers criticism of another story before revising their own. Sunday's final versions receive delayed reward for preserving their premise while folding received criticism into scene, motive, or consequence.
Every observation includes bounded affinity, trust, tension, and intellectual-regard parameters for the historical relationships, plus each character's latest emotional prosody: emotion, valence, arousal, intensity, rate, and pause. Targeted actions output a relationship stance and an empathy decision (attune, steady, or challenge). The environment verifies recognition against the target's actual prior affect, realizes bounded relationship deltas, and logs both as immutable outputs.
Generate complete weekends through ask-faculty without posting them to Matrix:
python3 scripts/diodati_weekend_optimize.py --output-dir weekend-optimization-runsMary Godwin and Polidori receive gentle, non-teleological thematic pressures grounded in their 1816 situations. The optimizer forbids naming, predicting, or prescribing their later works.
The environment begins when natural evening light had effectively gone. The U.S. Naval Observatory calculation for Villa Diodati (46.22° N, 6.18° E) on 15 June 1816 gives sunset at 19:28 UTC and the end of civil twilight at 20:07 UTC. Apparent solar noon was 11:35 UTC, placing Geneva apparent solar time about 25 minutes ahead of UTC: sunset around 19:53 and darkness around 20:32 Geneva apparent solar time. The simulated clock therefore begins at 20:32 and advances one-for-one with real elapsed time.
Production Matrix runs only in the canonical inquiry-institute Google Cloud
project. Backup, restore-validation, rollback-window, and old-project retirement
instructions are recorded in
docs/GCP_MATRIX_CONSOLIDATION.md.
Salon evenings map to Matrix rooms. This repo serves static GitHub Pages; dynamic URLs use client-side routing plus a 404.html copy of the /live/ shell so deep links load correctly.
/live/— overview (no room selected)./live/<encoded-room-or-alias>— chat UI for that room (room id!id:serveror alias#name:server, URL-encoded in the path).
Examples:
https://salon.castalia.institute/live/%21xxxxxxxx%3Amatrix.example.org
https://salon.castalia.institute/live/%23salon-room%3Amatrix.castalia.institute
Reading messages uses the Matrix Client-Server API (polling). Sending messages requires the same matrix-send-message Supabase edge function as the main site: set PUBLIC_SUPABASE_URL and PUBLIC_SUPABASE_ANON_KEY at build time (GitHub Actions secrets). Override PUBLIC_MATRIX_SERVER via repository Variables if needed.
- Settings → Pages → Build and deployment: source GitHub Actions.
- Add
PUBLIC_SUPABASE_URLandPUBLIC_SUPABASE_ANON_KEYas Actions secrets to enable visitor messages. - Push to
main(the workflow uploadsdist/, including404.html). - Cloudflare DNS + GitHub custom domain (below).
Every push also runs .github/workflows/quality.yml, which executes the full
historical, realtime, Gym, manuscript, and production-build checks before the
change is considered ready for the salon.
Production DNS for castalia.institute is in Cloudflare (same idea as Castalia GitHub Pages setup).
Dashboard
- Cloudflare → zone castalia.institute → DNS → Add record.
- Type:
CNAME
Name:salon
Target:castaliainstitute.github.io
Proxy: DNS only (gray cloud) until GitHub shows a valid certificate; then you can enable orange-cloud if you want (often SSL/TLS → Full (strict)).
API script (needs CLOUDFLARE_API_TOKEN with Zone → DNS Edit, and jq):
cd ~/GitHub/salon
export CLOUDFLARE_API_TOKEN='your-token'
./scripts/cloudflare-salon-cname.shGitHub custom domain
After DNS propagates: Settings → Pages → Custom domain → salon.castalia.institute, or use gh api / UI per GitHub Pages custom domains. Turn on Enforce HTTPS once the certificate status is Approved.
The repository-owned rollout command updates only the Diodati agent code, RAG corpus, and service units on the canonical VM. It never copies environment files, credentials, Matrix data, or cycle state. It is dry-run by default:
./infra/matrix/deploy-diodati-runtime.sh
./infra/matrix/deploy-diodati-runtime.sh --applyThe apply path requires Compute SSH access to matrix-synapse in the
inquiry-institute project, then restarts both Diodati services and verifies
that they are active.
Satellite site built with Astro, React, and Tailwind. It is self-contained so the separate Salon repository can build and deploy without access to another private repository.
cd ~/GitHub/salon
npm install
npm run devnpm run build # production build to dist/
npm run preview # serve dist/salon/
├── .github/workflows/deploy.yml # GitHub Pages
├── data/diodati_rag.json # curated, pre-cutoff character sources
├── scripts/copy-404.mjs # SPA fallback for /live/* deep links
├── scripts/diodati_gym.py # deterministic offline RL environment
├── scripts/diodati_realtime.py # Matrix agents, RAG, and historical guard
├── scripts/diodati_visitor_rl.py # wall-clock registered-visitor environment
├── scripts/cloudflare-salon-cname.sh # optional: create CNAME in Cloudflare
├── src/
│ ├── components/
│ │ ├── CastaliaShell.tsx
│ │ ├── SalonLiveApp.tsx # react-router shell
│ │ └── SalonLiveRoom.tsx # Matrix room UI
│ ├── lib/matrix-room-client.ts
│ ├── layouts/BaseLayout.astro
│ ├── pages/
│ │ ├── index.astro
│ │ └── live/index.astro # /live/* Matrix mirror
│ └── styles/global.css
├── public/
│ ├── CNAME # salon.castalia.institute
│ └── favicon.svg
├── astro.config.mjs
└── package.json
This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.
A Castalia Institute satellite