Summary
When a channel config has named accounts but no default, the doctor/channels add migration moves top-level account-scoped keys into accounts.default. For channels that declare namedAccountPromotionKeys, this is limited to credentials only — policy keys stay at the channel top level as shared defaults.
Problem: Most channels do NOT declare namedAccountPromotionKeys. The guard in resolveSingleAccountKeysToMove() short-circuits when the value is undefined:
if (
hasNamedAccounts &&
namedAccountPromotionKeys && // undefined for most channels → guard skipped
!namedAccountPromotionKeys.includes(key)
) {
return false;
}
This means ALL movable keys (groupPolicy, allowFrom, defaultTo, credentials, etc.) get swept into accounts.default, stripping shared defaults from existing named accounts.
Channels with the guard
| Channel |
Source |
Status |
| Telegram |
BUNDLED_NAMED_ACCOUNT_PROMOTION_FALLBACKS |
✅ Patched |
| Matrix |
Plugin setup surface (namedAccountPromotionKeys) |
✅ Patched |
Channels WITHOUT the guard (affected)
Slack, Discord, WhatsApp, Signal, IRC, MS Teams, Google Chat, iMessage, Line, Nostr, Mattermost, Synology Chat, BlueBubbles, Tlon, Twitch, Zalo, and all other extensions.
Repro
Before migration — top-level acts as inherited base for all accounts:
channels:
slack:
groupPolicy: "open"
allowFrom: ["U123"]
accounts:
work:
botToken: "xoxb-work"
personal:
botToken: "xoxb-personal"
Both work and personal inherit groupPolicy and allowFrom via the { ...base, ...account } merge in mergeSlackAccountConfig().
After doctor --fix — shared defaults moved into phantom default:
channels:
slack:
accounts:
default:
groupPolicy: "open"
allowFrom: ["U123"]
work:
botToken: "xoxb-work" # lost groupPolicy + allowFrom
personal:
botToken: "xoxb-personal" # lost groupPolicy + allowFrom
work and personal no longer inherit those values.
Expected behavior
Each channel should declare namedAccountPromotionKeys listing only credential/auth keys that are per-account by nature (e.g., botToken, appToken, signingSecret). Policy keys (groupPolicy, allowFrom, defaultTo) should remain at the channel top level as shared defaults.
Alternatively, the fallback when namedAccountPromotionKeys is undefined could default to credentials-only (conservative) rather than move-everything (destructive).
Relevant code
src/channels/plugins/setup-helpers.ts — resolveSingleAccountKeysToMove()
src/commands/doctor/shared/channel-legacy-config-migrate.ts (calls via seedMissingDefaultAccountsFromSingleAccountBase)
BUNDLED_NAMED_ACCOUNT_PROMOTION_FALLBACKS — only has Telegram
BUNDLED_SINGLE_ACCOUNT_PROMOTION_FALLBACKS — only has Telegram
Related PRs
Summary
When a channel config has named accounts but no
default, the doctor/channels addmigration moves top-level account-scoped keys intoaccounts.default. For channels that declarenamedAccountPromotionKeys, this is limited to credentials only — policy keys stay at the channel top level as shared defaults.Problem: Most channels do NOT declare
namedAccountPromotionKeys. The guard inresolveSingleAccountKeysToMove()short-circuits when the value isundefined:This means ALL movable keys (
groupPolicy,allowFrom,defaultTo, credentials, etc.) get swept intoaccounts.default, stripping shared defaults from existing named accounts.Channels with the guard
BUNDLED_NAMED_ACCOUNT_PROMOTION_FALLBACKSnamedAccountPromotionKeys)Channels WITHOUT the guard (affected)
Slack, Discord, WhatsApp, Signal, IRC, MS Teams, Google Chat, iMessage, Line, Nostr, Mattermost, Synology Chat, BlueBubbles, Tlon, Twitch, Zalo, and all other extensions.
Repro
Before migration — top-level acts as inherited base for all accounts:
Both
workandpersonalinheritgroupPolicyandallowFromvia the{ ...base, ...account }merge inmergeSlackAccountConfig().After
doctor --fix— shared defaults moved into phantomdefault:workandpersonalno longer inherit those values.Expected behavior
Each channel should declare
namedAccountPromotionKeyslisting only credential/auth keys that are per-account by nature (e.g.,botToken,appToken,signingSecret). Policy keys (groupPolicy,allowFrom,defaultTo) should remain at the channel top level as shared defaults.Alternatively, the fallback when
namedAccountPromotionKeysisundefinedcould default to credentials-only (conservative) rather than move-everything (destructive).Relevant code
src/channels/plugins/setup-helpers.ts—resolveSingleAccountKeysToMove()src/commands/doctor/shared/channel-legacy-config-migrate.ts(calls viaseedMissingDefaultAccountsFromSingleAccountBase)BUNDLED_NAMED_ACCOUNT_PROMOTION_FALLBACKS— only has TelegramBUNDLED_SINGLE_ACCOUNT_PROMOTION_FALLBACKS— only has TelegramRelated PRs