docs/reference/config-versioning.md
PicoClaw uses a schema versioning system for config.json to ensure smooth upgrades as the configuration format evolves.
version field to Config structenabled field to ModelConfig — allows disabling individual model entries without removing themenabled is auto-inferred: models with API keys or the reserved local-model name are enabled; others default to disabledmention_only → group_trigger.mention_only, OneBot group_trigger_prefix → group_trigger.prefixesmakeBackup() now uses date-only suffix (e.g., config.json.20260330.bak) and also backs up .security.ymlconfig.json.20260413.bak) in the same directoryWhen you load a config file:
version field from the JSONconfigV0, configV1, etc.)config.json and .security.ymlThe version field in config.json indicates the schema version:
0 or missing: Legacy config (no version field)1: Previous version (will be auto-migrated to V2 on load)2: Current version{
"version": 3,
"agents": {...},
...
}
When making breaking changes to the config schema:
Create a new struct for the new version if the structure changes significantly:
// ConfigV2 represents version 2 config structure
type ConfigV2 struct {
Version int `json:"version"`
Agents AgentsConfig `json:"agents"`
// ... other fields with new structure
}
const CurrentVersion = 2 // Increment this
// loadConfigV3 loads a version 3 config
func loadConfigV3(data []byte) (*Config, error) {
cfg := DefaultConfig()
// Parse to ConfigV3 struct
var v3 ConfigV3
if err := json.Unmarshal(data, &v3); err != nil {
return nil, err
}
// Convert to current Config
cfg.Version = v3.Version
cfg.Agents = v3.Agents
// ... map other fields
return cfg, nil
}
func (c *configV2) Migrate() (*Config, error) {
// Apply V2→V3 structural changes here
migrated := &c.Config
migrated.Version = 3
// Apply structural changes
return migrated, nil
}
func LoadConfig(path string) (*Config, error) {
// ... read file ...
switch versionInfo.Version {
case 0:
cfg, err = loadConfigV0(data)
case 1:
cfg, err = loadConfigV1(data)
case 2:
cfg, err = loadConfig(data)
case 3:
cfg, err = loadConfigV3(data)
default:
return nil, fmt.Errorf("unsupported config version: %d", versionInfo.Version)
}
// ... migrate and validate ...
}
Create a test in config_migration_test.go:
func TestMigrateV2ToV3(t *testing.T) {
// Create a version 2 config
v2Config := Config{
Version: 2,
// ... set up test data
}
// Apply migration
migrated, err := v2Config.Migrate()
if err != nil {
t.Fatalf("Migration failed: %v", err)
}
// Verify version is updated
if migrated.Version != 3 {
t.Errorf("Expected version 3, got %d", migrated.Version)
}
// Verify data is preserved/transformed correctly
// ...
}
config.json and .security.ymldefaults.go in sync with the latest schemaVersion 3 introduces improved type safety and error handling:
val, ok := v.(*Settings)) to prevent panics if Type and Settings are mismatchedGetDecoded() failure for consistency with other channelsWhen you run PicoClaw with a V2 config file:
version field and detects V2config.json.YYYYMMDD.bak (e.g., config.json.20260413.bak)"version": 3No user action required — the migration happens automatically on first load.
Backups are created in the same directory as your config file:
~/.picoclaw/config.json.20260413.bakPICOCLAW_CONFIG, backup is created next to that file.security.yml is also backed up as .security.yml.YYYYMMDD.bak⚠️ Important: Once migrated to V3, the config cannot be safely loaded by older PicoClaw versions that only support V2.
To downgrade:
cp ~/.picoclaw/config.json.20260413.bak ~/.picoclaw/config.json
cp ~/.picoclaw/.security.yml.20260413.bak ~/.picoclaw/.security.yml # if it exists
Alternative: Manually edit config.json and change "version": 3 to "version": 2. This works because V3 changes are primarily code-level safety improvements, not structural schema changes.
Old config (version 2):
{
"version": 3,
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4"
}
]
}
Migration to version 3:
func (c *configV2) Migrate() (*Config, error) {
migrated := &c.Config
migrated.Version = 3
// Add new field with default value if not set
// ...
return migrated, nil
}
New config (version 3):
{
"version": 3,
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"new_option": true
}
]
}
CurrentVersion is incrementedMigrate() is called in LoadConfig()config.json.20260330.bak) to recover original data