simocracy-v2 · gathering-default-constitution · r1

Round 1 — Create gathering + attach default constitution template

@daviddao

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.

Date: 2026-04-20 · Target: www.simocracy.org · Account: did:plc:cpoagodpqrgs4t7thi5z37uf · fail


Headline result

2
Gatherings written to PDS
1
Blocker (template picker stuck)
1
Indexer lag observation
1
Suspected root cause

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.

Walkthrough

Flow executed via agent-browser against production. All screenshots are raw agent-browser captures.

01ATProto OAuth consent screen on the user’s PDS (climateai.org). Client is https://www.simocracy.org/client-metadata.json; scopes include Bluesky / Repository / Authenticate.
02Events gallery after sign-in. Before the run: 3 pre-existing events (Senate, Funding the Commons SF, GainForest Data Council). “Create an Event” CTA available to the authenticated user.
03“Create an Event” dialog freshly opened. Required field is Event Name; defaults are Governance type, Normal sim size, S-Process allocation mechanism, empty suggested templates.
04Bottom of the form showing the S-Process allocation panel and the + Add Template / ✎ Author new buttons for Suggested Interview Templates (counter: 0 / 5).
05After clicking + Add Template. The picker modal mounts correctly (search field + “✎ Author a new template” CTA) but the template list is a bare Loading templates… string.
06Same modal after 18 s of waiting. No change. Patched window.fetch captured zero outbound requests from this dialog, confirming the loader never fires a GraphQL call.
07Fallback: submitted the dialog without a template. After reload the counter reads 5 events; the two new entries (PDS Direct Test and Default Constitution Test) show under All Events, proving the core create flow works end-to-end.

Findings

Template picker hangs on “Loading templates…” forever

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]);

Indexer lag masks successful writes

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"}

Core gathering creation end-to-end is solid

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.

Clean-up

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: [].

Reproducing

Accountsatyam2.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.png10-template-picker-stuck.png