Skip to main content

Property Layers: Multi-Level Configuration

Implementation guide for Gas Town's configuration system. Created: 2025-01-06

Overview

Gas Town uses a layered property system for configuration. Properties are looked up through multiple layers, with earlier layers overriding later ones. This enables both local control and global coordination.

The Four Layers

┌─────────────────────────────────────────────────────────────┐
│ 1. WISP LAYER (transient, town-local) │
│ Location: <rig>/.beads-wisp/config/ │
│ Synced: Never │
│ Use: Temporary local overrides │
└─────────────────────────────┬───────────────────────────────┘
│ if missing

┌─────────────────────────────────────────────────────────────┐
│ 2. RIG BEAD LAYER (persistent, synced globally) │
│ Location: <rig>/.beads/ (rig identity bead labels) │
│ Synced: Via git (all clones see it) │
│ Use: Project-wide operational state │
└─────────────────────────────┬───────────────────────────────┘
│ if missing

┌─────────────────────────────────────────────────────────────┐
│ 3. TOWN DEFAULTS │
│ Location: ~/gt/config.json or ~/gt/.beads/ │
│ Synced: N/A (per-town) │
│ Use: Town-wide policies │
└─────────────────────────────┬───────────────────────────────┘
│ if missing

┌─────────────────────────────────────────────────────────────┐
│ 4. SYSTEM DEFAULTS (compiled in) │
│ Use: Fallback when nothing else specified │
└─────────────────────────────────────────────────────────────┘

Lookup Behavior

Override Semantics (Default)

For most properties, the first non-nil value wins:

func GetConfig(key string) interface{} {
if val := wisp.Get(key); val != nil {
if val == Blocked { return nil }
return val
}
if val := rigBead.GetLabel(key); val != nil {
return val
}
if val := townDefaults.Get(key); val != nil {
return val
}
return systemDefaults[key]
}

Stacking Semantics (Integers)

For integer properties, values from wisp and bead layers add to the base:

func GetIntConfig(key string) int {
base := getBaseDefault(key) // Town or system default
beadAdj := rigBead.GetInt(key) // 0 if missing
wispAdj := wisp.GetInt(key) // 0 if missing
return base + beadAdj + wispAdj
}

This enables temporary adjustments without changing the base value.

Blocking Inheritance

You can explicitly block a property from being inherited:

gt rig config set gastown auto_restart --block

This creates a "blocked" marker in the wisp layer. Even if the rig bead or defaults say auto_restart: true, the lookup returns nil.

Rig Identity Beads

Each rig has an identity bead for operational state:

id: gt-rig-gastown
type: rig
name: gastown
repo: [email protected]:steveyegge/gastown.git
prefix: gt

labels:
- status:operational
- priority:normal

These beads sync via git, so all clones of the rig see the same state.

Two-Level Rig Control

Level 1: Park (Local, Ephemeral)

gt rig park gastown      # Stop services, daemon won't restart
gt rig unpark gastown # Allow services to run
  • Stored in wisp layer (.beads-wisp/config/)
  • Only affects this town
  • Disappears on cleanup
  • Use: Local maintenance, debugging

Level 2: Dock (Global, Persistent)

gt rig dock gastown      # Set status:docked label on rig bead
gt rig undock gastown # Remove label
  • Stored on rig identity bead
  • Syncs to all clones via git
  • Permanent until explicitly changed
  • Use: Project-wide maintenance, coordinated downtime

Daemon Behavior

The daemon checks both levels before auto-restarting:

func shouldAutoRestart(rig *Rig) bool {
status := rig.GetConfig("status")
if status == "parked" || status == "docked" {
return false
}
return true
}

Configuration Keys

KeyTypeBehaviorDescription
statusstringOverrideoperational/parked/docked
auto_restartboolOverrideDaemon auto-restart behavior
max_polecatsintOverrideMaximum concurrent polecats
priority_adjustmentintStackScheduling priority modifier
maintenance_windowstringOverrideWhen maintenance allowed
dndboolOverrideDo not disturb mode

Commands

View Configuration

gt rig config show gastown           # Show effective config (all layers)
gt rig config show gastown --layer # Show which layer each value comes from

Set Configuration

# Set in wisp layer (local, ephemeral)
gt rig config set gastown key value

# Set in bead layer (global, permanent)
gt rig config set gastown key value --global

# Block inheritance
gt rig config set gastown key --block

# Clear from wisp layer
gt rig config unset gastown key

Rig Lifecycle

gt rig park gastown          # Local: stop + prevent restart
gt rig unpark gastown # Local: allow restart

gt rig dock gastown # Global: mark as offline
gt rig undock gastown # Global: mark as operational

gt rig status gastown # Show current state

Examples

Temporary Priority Boost

# Base priority: 0 (from defaults)
# Give this rig temporary priority boost for urgent work

gt rig config set gastown priority_adjustment 10

# Effective priority: 0 + 10 = 10
# When done, clear it:

gt rig config unset gastown priority_adjustment

Local Maintenance

# I'm upgrading the local clone, don't restart services
gt rig park gastown

# ... do maintenance ...

gt rig unpark gastown

Project-Wide Maintenance

# Major refactor in progress, all clones should pause
gt rig dock gastown

# Syncs via git - other towns see the rig as docked
bd sync

# When done:
gt rig undock gastown
bd sync

Block Auto-Restart Locally

# Rig bead says auto_restart: true
# But I'm debugging and don't want that here

gt rig config set gastown auto_restart --block

# Now auto_restart returns nil for this town only

Implementation Notes

Wisp Storage

Wisp config stored in .beads-wisp/config/<rig>.json:

{
"rig": "gastown",
"values": {
"status": "parked",
"priority_adjustment": 10
},
"blocked": ["auto_restart"]
}

Rig Bead Labels

Rig operational state stored as labels on the rig identity bead:

bd label add gt-rig-gastown status:docked
bd label remove gt-rig-gastown status:docked

Daemon Integration

The daemon's lifecycle manager checks config before starting services:

func (d *Daemon) maybeStartRigServices(rig string) {
r := d.getRig(rig)

status := r.GetConfig("status")
if status == "parked" || status == "docked" {
log.Info("Rig %s is offline, skipping auto-start", rig)
return
}

d.ensureWitness(rig)
d.ensureRefinery(rig)
}

Operational State Events

Operational state changes are tracked as event beads, providing an immutable audit trail. Labels cache the current state for fast queries.

Event Types

Event TypeDescriptionPayload
patrol.mutedPatrol cycle disabled{reason, until?}
patrol.unmutedPatrol cycle re-enabled{reason?}
agent.startedAgent session began{session_id?}
agent.stoppedAgent session ended{reason, outcome?}
mode.degradedSystem entered degraded mode{reason}
mode.normalSystem returned to normal{}

Creating and Querying Events

# Create operational event
bd create --type=event --event-type=patrol.muted \
--actor=human:overseer --target=agent:deacon \
--payload='{"reason":"fixing convoy deadlock","until":"gt-abc1"}'

# Query recent events for an agent
bd list --type=event --target=agent:deacon --limit=10

# Query current state via labels
bd list --type=role --label=patrol:muted

Labels-as-State Pattern

Events capture the full history. Labels cache the current state:

  • patrol:muted / patrol:active
  • mode:degraded / mode:normal
  • status:idle / status:working

State change flow: create event bead (immutable), then update role bead labels (cache).

# Mute patrol
bd create --type=event --event-type=patrol.muted ...
bd update role-deacon --add-label=patrol:muted --remove-label=patrol:active

Configuration vs State

TypeStorageExample
Static configTOML filesDaemon tick interval
Operational stateBeads (events + labels)Patrol muted
Runtime flagsMarker files.deacon-disabled

Events are the source of truth. Labels are the cache.

For Boot triage and degraded mode details, see Watchdog Chain.

  • ~/gt/docs/hop/PROPERTY-LAYERS.md - Strategic architecture
  • wisp-architecture.md - Wisp system design
  • agent-as-bead.md - Agent identity beads (similar pattern)