Status: Implemented and verified
Replace whole-document V1/V2 detection with one config-domain compatibility pipeline. Supported V1 fields, native V2 fields, and practical mixtures of both should load without an unrelated legacy key changing how the rest of the document is decoded.
Normalize recognized fields independently into the encoded side of the V2 Config.Info schema, then perform one final complete-document V2 decode:
JSON/JSONC encoded input
-> parse and retain source-property presence
-> validate each recognized field or collection entry
-> migrate supported V1 candidates to V2 encoded values
-> decode and re-encode native V2 candidates
-> merge with native V2 precedence
-> decode Config.Info once
-> log redacted diagnostics
There is no whole-document version classification and no independent whole-document V1 and V2 decode.
The encoded boundary matters because schemas such as warming durations transform strings into runtime values. Decoded values must not be fed back into the encoded side of Config.Info.
| Situation | Result |
|---|---|
| Supported V1-only field | Migrate it to its canonical V2 destination. |
| Native V2 field | Preserve it after schema decode and encode. |
| Disjoint V1 and V2 map entries | Preserve both. |
| Same canonical scalar, map entry, or nested leaf | Valid native V2 wins regardless of JSON key order. |
| Malformed native value with valid legacy fallback | Skip native value, log it, and retain legacy value. |
| Malformed collection entry | Skip only the explicitly supported recovery unit. |
| Unsupported accepted V1 setting | Omit it and log a redacted warning. |
| Unknown field | Continue ignoring it for forward compatibility. |
Valid supported V1 syntax does not warn merely because it is legacy.
| Destination | Lowest to highest precedence |
|---|---|
snapshots |
snapshot < snapshots |
share |
autoshare < share |
references[name] |
reference[name] < references[name] |
agents[name] |
agent[name] < mode[name] < agents[name] |
commands[name] |
command[name] < commands[name] |
providers[name] |
provider[name] < providers[name] |
permissions |
tools rules < permission rules < native permissions |
plugins |
migrated plugin items < native plugins items |
media |
attachment < media |
experimental.policies |
enabled-provider policies < disabled-provider policies < native policies |
mcp.servers[name] |
direct legacy server < native servers[name] |
mcp.timeout.* |
experimental.mcp_timeout < native timeout leaf |
compaction.keep.tokens |
preserve_recent_tokens < keep.tokens |
compaction.buffer |
reserved < buffer |
Ordered rules and plugin directives retain both forms, with migrated V1 entries first and native V2 entries last.
paths followed by valid urls.mcp are V1 servers.mcp.servers are native V2 servers.experimental.mcp_timeout.type and enabled discriminators preserve legacy servers that happen to be named servers or timeout.preserve_recent_tokens becomes keep.tokens.reserved becomes buffer.tail_turns and prune remain unsupported and produce warnings.subagent_depth is shared.enabled_providers keeps deny-all behavior.Named commands, agents, providers, MCP servers, formatters, language servers, and references recover independently. Plugin, permission, skill, instruction, provider-ID, and policy arrays recover by item. Top-level legacy permissions recover by action/resource rule. Complex interiors of one agent, provider, command, or MCP server remain atomic rather than being recursively salvaged.
Every decoder preserves propertyOrder: "original" because V1 permission precedence depends on user order. Excess properties remain ignored except for the explicit unsupported inventory.
Provider ID compatibility remains a config migration concern only. Existing V1 agent, command, provider, and provider-policy adapters continue using the migration helper's retired-ID mapping.
The shared top-level model field remains exact because its string and object forms are valid native V2 syntax and provider declarations may come from a different config layer. It is never reinterpreted based on unrelated legacy fields.
This change does not add runtime provider aliases or modify provider policy evaluation, catalog state, model resolution, Sessions, plugins, Server behavior, or generation.
Diagnostics contain only source, JSON path, category, and action. They never include raw values because config may contain credentials after substitution.
Malformed JSON, empty content, and valid non-object roots reject one document with a source-aware warning. Malformed recognized fields and entries are skipped at their recovery boundary while unrelated valid configuration continues loading.
ConfigNormalize.normalize module under packages/core/src/config/.packages/core/src/v1/config/migrate.ts.ConfigMigrateV1.isV1 in packages/core/src/config.ts with normalization and one final V2 decode.OPENCODE_CONFIG_CONTENT, and well-known virtual config.The implementation must establish:
Run from packages/core:
bun test test/config
bun typecheck
Run from packages/www after documentation changes:
bun typecheck
bun validate
bun run build