Skip to main content
This runbook is for mayors and operators. It covers the endpoint architecture introduced in the beads-and-Dolt contract redesign — specifically the case where rigs inherit their Dolt endpoint from a single city-managed server. If you came here because gc sling is dropping work, supervisor.log shows rigStores=0, or your port edits keep reverting, you are in the right place.
Rules of thumb — skip to What NOT to do if you are mid-incident. Come back for the mental model after.

Mental model

A city runs one Dolt SQL server. Every rig is a logical database inside that Dolt. Each rig’s .beads/dolt-server.port is a compatibility mirror that lets raw bd commands reach the city Dolt — it is not the rig’s own server.

Endpoint origins

Two TOML fields, one per scope, describe endpoint ownership:
  • managed_city — The city owns the Dolt lifecycle. gc start launches it; gc stop shuts it down. This is the default for fresh bd-backed cities.
  • city_canonical — The city declares an explicit, externally managed Dolt. GC does not launch or manage the server.
  • inherited_city — The rig reuses its parent city’s endpoint. No rig-local Dolt. This is the default for rigs added to a managed_city city.
  • explicit — The rig pins its own external endpoint. Use for cross-city rigs or rigs pointing at a shared Dolt cluster.
gc.endpoint_origin describes endpoint ownership, not JSONL retention. To keep a city-root .beads/issues.jsonl on disk, leave the city origin as managed_city or city_canonical and change the existing export.auto: false line in that scope’s .beads/config.yaml to export.auto: true. Running bd config set export.auto true from the scope root makes the same change. Gas City preserves that explicit setting and skips stale-JSONL cleanup for the scope. Do not append a second export.auto key instead: bd cannot parse a config.yaml with a duplicate key and ignores the whole file, and Gas City honors only the first occurrence, so the scope stays opted out and the file is still removed. Do not use explicit at city scope; it is a rig-only endpoint origin and fails canonical validation. Every supported bd (1.3.0 or newer) imports a present issues.jsonl only into an empty database, so the retained file is never re-applied over existing Dolt data. The opt-out buys retention, not freshness. Gas City still forces BD_EXPORT_AUTO=false on its own bd calls — gc bd, controllers, dispatch, mail — so those writes do not re-export the file, and the retained issues.jsonl drifts from Dolt with no warning. Refresh it out of band from the scope root with bd export -o .beads/issues.jsonl, and note that bd export omits infrastructure beads and memories unless you pass --include-infra, --include-memories, or --all. Plain bd run in the scope, from a shell or an agent session, does not get the BD_EXPORT_AUTO=false override: it follows export.auto: true, so its writes can re-export the file too. Use gc bd instead if the file should change only when you refresh it. Rolling back to a Gas City build from before this opt-out existed rewrites export.auto back to false and reaps the file once.

What NOT to do

These are the edits mayors try first, all of which self-revert at the next gc start and waste an afternoon.

Do not run bd dolt set port <N> inside an inherited rig

If gc.endpoint_origin: inherited_city, the rig’s canonical port is whatever the city’s managed Dolt is listening on. bd will accept the set port command and update .beads/config.yaml, but on the next gc start the city’s normalization pass (syncConfiguredDoltPortFiles) restores the managed port and rewrites .beads/dolt-server.port back. Your change disappears.

Do not hand-edit .beads/dolt-server.port

Same reason. The file is a compatibility mirror for raw bd commands — it reflects the city-managed port, not an authoritative choice. gc start atomically rewrites it to match the managed port on each startup.

Do not delete the gc.endpoint_origin: inherited_city line

Deleting the line does not switch the rig to explicit. It puts the rig into an unverified state. gc start’s verification pass will re-derive the origin from topology and write the line back.

Do not run bd dolt start inside an inherited rig

Any Dolt server you start this way is orphaned. GC does not track it, will not shut it down on gc stop, and will not publish its port to dependent sessions. If the rig’s .beads/dolt-server.port points at this orphan server, the drift check will flag it on next gc doctor run (see Drift symptoms below).

Sanctioned escape hatches

When you genuinely need to change a rig’s endpoint, use gc rig set-endpoint — it is the single owning operation for endpoint topology, and it refuses footgun configurations up front.

Make this rig run its own Dolt

--self marks the rig as running its own 127.0.0.1:<P> Dolt. While the city is in managed_city mode the command refuses unless --force is given, and emits a one-line WARN explaining the rig now falls outside gc start’s supervision. The rig’s origin flips to explicit; its port file will no longer track the managed city Dolt.

Rejoin the city

Flips the rig back to inherited_city. The next gc start will re-publish the managed city port into .beads/dolt-server.port and the rig’s sessions will talk to the city Dolt again.

Point at an external Dolt

Pins explicit origin to <H>:<P>. Use this for cross-city rigs or rigs pointing at a shared/external Dolt cluster. Add --adopt-unverified if you need to record the endpoint before the target is reachable.

Inspect current state

gc doctor is authoritative for GC-side state: it reports the managed city port, each rig’s resolved endpoint origin, and any drift it detects between the two. The dolt-drift check only registers when the workspace is in bd-backed managed_city topology — if you do not see it, your workspace is not in that mode.

.beads/dolt-server.port priority explainer

When raw bd commands resolve a port, they consult sources in this order (first hit wins):
  1. $BEADS_DOLT_SERVER_PORT environment variable
  2. .beads/dolt-server.port file (the compatibility mirror)
  3. .beads/config.yaml dolt.port key (or the global beads config equivalent)
  4. .beads/metadata.json historical record
GC runtime publishes the managed port into $BEADS_DOLT_SERVER_PORT for sessions it launches, which is why sessions keep working even when the on-disk mirror is transiently out of date. For raw interactive bd invocations the mirror file is what matters, which is why GC aggressively rewrites it during normalization. The authoritative source for this order is DefaultConfig in beads/internal/doltserver/doltserver.go. bd dolt show advertises a slightly different order in its output text; that is a known cosmetic bug tracked separately in the beads repo and does not affect actual resolution.

Drift symptoms

The dolt-drift check (cmd/gc/cmd_doctor_drift.go) is registered automatically when the workspace is in bd-backed managed_city topology and catches three shapes of drift: All three carry FixHint text pointing at the specific gc rig set-endpoint invocation that resolves them. At gc start, the Dolt-port normalization pass also emits a WARN line on stderr for each rig whose .beads/dolt-server.port it is rewriting back to the managed port — that is your early warning that a prior edit or orphan server made the mirror drift.

Storage-mode drift: a scope pointed at the wrong database

.beads/metadata.json names which bead database bd opens. dolt_mode=server reads the managed city’s .beads/dolt; dolt_mode=embedded reads the scope’s own .beads/embeddeddolt/<dolt_database>. Nothing copies rows between them. gc rig add, gc start, gc supervisor run, gc rig set-endpoint and gc beads city use-managed|use-external all canonicalize a managed scope to server mode, because a Dolt directory opened in-process cannot be shared by the controller, every agent’s bd and the dashboard at once. On a workspace you had initialized in embedded mode, that flip re-points the ledger and leaves the old database on disk, unread — and bd does not fail, it answers [] with exit 0. This is not split-store specific: it bites a city with exactly one store just as hard. Three things now tell you:
  1. The flip prints the scope, both modes and the exact directory it is about to stop reading, before it happens.
  2. gc doctor’s bd-split-store check enumerates both databases.
  3. An empty whole-ledger read (gc ready’s city leg, an unfiltered List) from a scope that has not returned a row in that process prints one notice per gc process, naming the unread database. It is a notice, never a refusal, and it spends no bd subprocess of its own — a workspace bd init created and never filed a bead into is the same shape on disk, an idle ledger reads the same way, and separating them would mean either opening the database you were told to preserve or asking bd a second question on the caller’s read deadline. Expect it on any scope that still has two bead databases, including healthy ones; that is the price of a notice that cannot cost a read.
Recovery is gc doctor’s: export from a copy of the unread database, review with bd import --dry-run, import into the active one, and keep both directories until reconciled. Editing dolt_mode back does not hold — the next gc start re-canonicalizes it. While you reconcile, GC_BD_ALLOW_UNREAD_STORE_READ=1 silences the read-time notice without changing any answer.

Recovery recipe — slung beads not reaching agents

If you see gc sling accepting work but agents not processing it, and the supervisor log reports rigStores=0 or assignedWorkBeads=0, that is almost always a symptom of the rig’s Dolt view diverging from the city’s. Work the following sequence:
  1. Stop the city:
  2. Kill any live rig-local Dolt servers, rig by rig. For each rig, check .dolt/sql-server.info for a PID and verify it:
  3. Confirm the rig’s canonical state is inherited_city and verified: each rig’s .beads/config.yaml should contain:
    If any rig says something else and you want it to rejoin, run gc rig set-endpoint <rig> --inherit.
  4. Delete the compatibility mirror in each rig, so it is regenerated fresh on next start:
  5. Start the city:
  6. Verify only one Dolt is running on the managed port:
  7. Test a sling end-to-end and watch the supervisor log for assignedWorkBeads > 0:
If the symptom persists after step 7, the cause is likely not endpoint drift. Open an investigator bead referencing this runbook and attaching the full gc doctor --verbose output plus the supervisor.log slice covering the last sling attempt.

References

Last modified on October 18, 2026