simocracy-v2 · gathering-default-constitution · r1
Logged in as satyam2.climateai.org on the production deploy and walked the “Create an Event → + Add Template” flow end-to-end. Gathering creation landed cleanly on the user’s PDS; the “Suggested Interview Templates” picker opens but hangs on Loading templates… forever. Followed up in r2 with the fix and a clean repro.
Headline. The core “Create Event” happy-path writes a valid org.simocracy.gathering record to the user’s PDS and the UI reflects it after reload. But clicking + Add Template surfaces the picker modal in a permanent Loading templates… state — the indexer has a “Default Constitution” template ready (did:plc:awtx57rxuisy4sf4kzzr3uhf/org.simocracy.interviewTemplate/3mjtodp56ps2e) but the list never renders, so users can never attach a template to their gathering through the UI. Root cause appears to be a useEffect cleanup race in components/events/gathering-form-dialog.tsx: templatesLoading is listed as a dependency, so calling setTemplatesLoading(true) re-runs the effect and triggers the cleanup (cancelled = true) before the inner fetch resolves — the finally block that sets templatesLoading(false) is gated on !cancelled, so the loading flag is never cleared.
Flow executed via agent-browser against production. All screenshots are raw agent-browser captures.
climateai.org). Client is https://www.simocracy.org/client-metadata.json; scopes include Bluesky / Repository / Authenticate.
window.fetch captured zero outbound requests from this dialog, confirming the loader never fires a GraphQL call.
Where. components/events/gathering-form-dialog.tsx, the useEffect at ~line 151 that loads interview templates when showTemplatePicker becomes true.
Repro. Open Events → Create an Event → + Add Template while signed in. The modal opens; the template list stays on Loading templates… indefinitely.
What’s actually happening. A patched window.fetch logged zero outbound requests after the click, yet the effect clearly ran (it flipped templatesLoading to true, which is visible in the UI). The effect declares [showTemplatePicker, allTemplates.length, templatesLoading] as its dependency array. Calling setTemplatesLoading(true) inside the effect re-renders the component, which re-runs the effect and invokes its cleanup (cancelled = true) on the previous run. The inner async IIFE then reaches:
} finally {
if (!cancelled) setTemplatesLoading(false);
}
… which is a no-op because cancelled === true. React StrictMode’s double-invoke makes this deterministic, not a flake. There is a second guard at the top of the effect (if (allTemplates.length > 0 || templatesLoading) return;) that short-circuits every subsequent open, so the picker is permanently useless for the rest of the session.
Backend is not at fault. Querying the indexer directly from the browser returned the curated template instantly:
curl -s 'https://simocracy-indexer-production.up.railway.app/graphql' \
-H 'Content-Type: application/json' \
-d '{"query":"query FetchRecords($collection:String!,$first:Int,$after:String){records(collection:$collection,first:$first,after:$after){edges{node{uri cid did rkey collection value}}pageInfo{hasNextPage endCursor}}}",
"variables":{"collection":"org.simocracy.interviewTemplate","first":10}}' | jq '.data.records.edges[0].node.value.name'
# "Default Constitution"
Suggested fix. Drop templatesLoading from the dependency array (and ideally also allTemplates.length) — both are written by the effect itself. A useRef-backed “didLoad” flag works too. Minimally:
useEffect(() => {
if (!showTemplatePicker) return;
if (allTemplatesRef.current.length > 0) return;
let cancelled = false;
(async () => { /* … */ })();
return () => { cancelled = true; };
}, [showTemplatePicker]);
After a successful POST /api/records the handleCreated callback triggers window.location.reload(), which re-fetches through the Simocracy indexer. The indexer lags the PDS by several seconds, so the first reload still shows the old count (“3 events”). This is expected per AGENTS.md (“Indexer lag after writes … always implement PDS fallback”) but the EventsGallery server component does not appear to fall back to PDS reads for the current viewer’s own writes. The net effect: users see their “Event created!” toast but no new card, which reads like a failure — exactly the same shape as the earlier create-sim-interview “silent Save dismissal” finding.
Direct PDS verification confirmed both test records landed correctly:
DID=did:plc:cpoagodpqrgs4t7thi5z37uf
curl -s "https://climateai.org/xrpc/com.atproto.repo.listRecords" \
--data-urlencode "repo=$DID" \
--data-urlencode "collection=org.simocracy.gathering" -G | jq '.records[]|{uri,name:.value.name,createdAt:.value.createdAt}'
# {"uri":"…/3mjvnbxmq4s2e","name":"Default Constitution Test","createdAt":"2026-04-20T04:34:18.493Z"}
# {"uri":"…/3mjvn62qm5k2e","name":"PDS Direct Test","createdAt":"2026-04-20T04:32:07.492Z"}
OAuth login, dialog interactions, and the POST /api/records → agent.com.atproto.repo.createRecord path all work. The resulting record is a well-formed org.simocracy.gathering:
{
"$type": "org.simocracy.gathering",
"name": "Default Constitution Test",
"status": "application",
"simSize": "normal",
"gatheringType": "governance",
"allocationMechanism": "s-process",
"createdAt": "2026-04-20T04:34:18.493Z"
}
A direct fetch('/api/records', {method:'POST', …}) from the browser console succeeded with a 200 and returned a canonical at:// URI + CID, confirming the server route is not the bottleneck.
Both test gatherings created during this run were deleted through DELETE /api/records (both returned 200). Final PDS state for satyam2.climateai.org’s org.simocracy.gathering collection: records: [].
satyam2.climateai.org · did:plc:cpoagodpqrgs4t7thi5z37uf
Targethttps://www.simocracy.org/events
Driveragent-browser (open → snapshot -i → click / fill → screenshot)
Blocker reproSign in → Events → Create an Event → fill Event Name → scroll to Suggested Interview Templates → click + Add Template. The picker opens stuck on Loading templates….
Suggested filecomponents/events/gathering-form-dialog.tsx (useEffect at ~line 151, deps [showTemplatePicker, allTemplates.length, templatesLoading])
Screenshotsassets/01-oauth-authorize.png … 10-template-picker-stuck.png