Affects: OpenClaw v2026.5.2 (also reproducible on v2026.5.4-beta.1; relevant code paths unchanged in v2026.5.4 release, though we have not separately re-verified the demotion on v2026.5.4).
Related but distinct: #77508 (missing/disabled channel preflight error wording) and #70333 (auto-enable writes enabled keys back, causing churn). Neither covers the symmetric case here, where an external channels.whatsapp.enabled=true write is silently reverted while the WhatsApp plugin is absent from the running registry.
TL;DR
The OpenClaw gateway can silently ignore or overwrite externally-written channels.whatsapp.enabled=true when the WhatsApp plugin isn't already in the running gateway's plugin registry. Combined with the WhatsApp plugin's intentional noopPrefixes: ["channels.whatsapp"] opt-out at extensions/whatsapp/src/shared.ts:222, downstream integrators that try to enable WhatsApp at runtime get an unrecoverable wedge: their disk write reads enabled=true momentarily, the plugin never enters the registry, and a later gateway config write can persist the stale runtime view (enabled=false) back over disk. gateway.channels.start then fails because the channel plugin is still absent.
The wedge is silent. No error, no warn log, no signal that the integrator is using the wrong pattern. The only escape is a full container restart with enabled=true already on disk at cold boot.
Repro on a production fleet
Setup
- A test bot deployed as Telegram-only on v2026.5.2: cold-boot config (
/data/deployments/<id>/openclaw.json) had channels.whatsapp.enabled=false.
- The gateway's container PID 14 is
/app/openclaw.mjs gateway.
Action
User clicks "Link WhatsApp" in our dashboard. An external orchestrator (a sidecar process that owns the deployed config file) writes the config:
cfg.channels.whatsapp = {
enabled: true, // <-- the flip
selfChatMode: true,
dmPolicy: "allowlist",
allowFrom: ["placeholder"],
};
writeFileSync(configPath, JSON.stringify(cfg, null, 2));
This is a plain writeFileSync (truncate + rewrite). chokidar in the gateway picks it up.
What we expected
The gateway reload pipeline observes channels.whatsapp.enabled=true, loads the WhatsApp plugin into the registry on the next iteration of run-loop.ts, and the channel runtime starts (or stays idle if no creds). Subsequent web.login.start / channels.start RPCs work end-to-end.
What actually happens
Five SIGUSR1-keyed reload events fire on the test bot between 13:22 and 13:42 UTC. After each one, the gateway emits a config.write audit entry and the file mtime advances. The disk content alternates between two shapes (4693 ↔ 4820 bytes; the +127 byte oscillation is the gateway.controlUi.allowedOrigins block being added/removed — separate quirk).
On every reload the gateway reverts channels.whatsapp.enabled from true back to false. The other 3 keys are preserved verbatim.
Direct content diff between the orchestrator's last write (4818 bytes) and the gateway's subsequent rewrite (4820 bytes):
35c35
< "enabled": true, ← orchestrator wrote
---
> "enabled": false, ← gateway flipped it back; other 3 keys preserved
195c195
< "lastTouchedAt": "2026-05-04T13:35:15.932Z"
---
> "lastTouchedAt": "2026-05-04T13:42:16.461Z"
Audit log entries from /data/deployments/<id>/logs/config-audit.jsonl (one entry per gateway-side write, 9 total during the click sequence):
{"ts":"2026-05-04T13:32:30.947Z","pid":14,"argv":["/usr/local/bin/node","/app/openclaw.mjs","gateway"],"actor":"config-io","event":"config.write","previousBytes":4818,"nextBytes":4693,"result":"rename","previousHash":"17f0...","nextHash":"c3e3..."}
{"ts":"2026-05-04T13:32:35.537Z","pid":14,"argv":["/usr/local/bin/node","/app/openclaw.mjs","gateway"],"actor":"config-io","event":"config.write","previousBytes":4693,"nextBytes":4820,"result":"rename"}
... (7 more) ...
After the cycle settles, channels.whatsapp.enabled=false is what's persisted. The plugin remains absent from loadPluginLookUpTable's output, the boot line still shows 9 plugins (... telegram) instead of 10 plugins (... telegram, whatsapp), and:
[ws] ⇄ res ✗ channels.start ... errorCode=INVALID_REQUEST errorMessage=invalid channels.start channel
is emitted when the OpenClaw login CLI calls gateway.channels.start { channel: "whatsapp", accountId: "default" } per aa76cf43f0 (fix(whatsapp): stabilize auth state and reconcile local runtime after CLI login).
What does work (the asymmetric comparator)
A comparator bot whose cold-boot disk had channels.whatsapp.enabled=true from deploy time loads the WhatsApp plugin at process start. On that bot, channels.whatsapp.enabled stays true across reloads — no clobber. Boot line 10 plugins (... whatsapp), audit log shows two writes at process start (controlUi seed + auto-enable) and zero subsequent writes over 16+ hours of uptime.
The asymmetry is on cold-boot state, not on runtime behavior. Hot-reloading channels.whatsapp.enabled: false → true on a running gateway is unsupported in v2026.5.2.
Root cause
One trigger is verified, and one write-origin hypothesis remains open.
1. noopPrefixes opt-out at extensions/whatsapp/src/shared.ts:222
reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] },
gatewayMethods: ["web.login.start", "web.login.wait"],
This was introduced in commit ba79d903137e35c4089cd7e98610eb11731ebb0f (2026-03-17, "refactor(whatsapp): share plugin base config") — a refactor commit with no explanatory message. Likely rationale: protect Baileys auth state from churn caused by config-edit-driven reloads. Telegram does NOT opt out (configPrefixes: ["channels.telegram"]), which is why Telegram channel-config edits hot-reload correctly.
Effect: on channels.whatsapp.* writes, the gateway emits [reload] channels.whatsapp.enabled changed then no-ops (src/gateway/server-reload-handlers.ts). Plugin registry is not consulted for re-discovery.
2. Exact demoting write origin remains unverified
The reconcile pass in src/config/plugin-auto-enable.shared.ts enumerates candidates from resolveConfiguredPluginAutoEnableCandidates. For each candidate:
isPluginDenied / isPluginExplicitlyDisabled → skip.
shouldSkipPreferredPluginAutoEnable → call disableImplicitPreferredOverPlugin (writes plugins.entries.<id>.enabled = false, never channels.<id>.enabled).
alreadyEnabled short-circuit (line 953) — for built-ins, checks channels.<id>.enabled === true AND !allowMissing. If both true, skip.
- Otherwise:
registerPluginEntry writes channels.<id>.enabled = true.
registerPluginEntry only enables. disableImplicitPreferredOverPlugin only writes plugin entries, never channel entries. There is no code path in this file that sets channels.<id>.enabled = false.
The clobber comes from a later config-write path, not from the auto-enable candidate loop itself. When the WhatsApp plugin isn't in the running gateway's registry, the external channels.whatsapp.* edit is classified as a no-op reload and does not rebuild the plugin registry. The gateway may still hold a pinned runtime/source config snapshot from cold boot where channels.whatsapp.enabled=false. A subsequent in-process gateway write (for example control UI origin seeding, generated auth/config metadata, startup/reload config persistence, pre-validation normalization, or another managed config write) could build its nextConfig from stale state or otherwise reintroduce enabled=false. When that write is persisted, the on-disk value flips back to false. Subsequent reloads then find enabled=false again, and the cycle repeats.
Important nuance verified against current source: resolvePersistCandidateForWrite() does preserve an externally edited source value if the later nextConfig does not touch that path. If both inputs to createMergePatch() reflect the post-orchestrator disk state, the patch should be empty for channels.whatsapp.enabled. The demotion therefore requires either a later gateway write whose nextConfig already carries stale channels.whatsapp.enabled=false, or an untraced normalization/write path that reintroduces that stale value before persistence. That matches the observed audit trail boundary: the demotion appears alongside gateway-side config writes, but source review alone has not identified which writer first supplies the false value.
Why the enabled=true write succeeds momentarily then loses
The writeFileSync is not atomic — Node's plain truncate-and-rewrite can produce torn states. But that's not the actual problem. The problem is stale runtime/source config ownership: the running gateway does not load the plugin on channels.whatsapp.* edits, and later managed config writes can persist the old runtime snapshot back to disk. Atomicity would not make the plugin enter the registry or make channels.start succeed.
Suggested fixes (any one closes the wedge for downstream integrators)
- Make the stale overwrite observable. Add instrumentation/audit at the config-write boundary so the next repro can identify the exact caller and diff that first changes
channels.<id>.enabled=true back to false while that channel plugin is not registered.
- Refuse or warn on unsupported hot-enable. When the config watcher sees
channels.whatsapp.enabled: false -> true but the WhatsApp plugin is absent from the running registry and channels.whatsapp is a no-op reload prefix, return a clear warning/restart requirement instead of silently accepting a no-op.
- Document the pattern. Add a section to
docs/channels/whatsapp.md describing the noopPrefixes opt-out and the recommended workflow for downstream integrators: write channels.whatsapp.enabled=true before cold boot if they need runtime web.login.start / channels.start, or restart the gateway after changing that flag from false to true.
- (Most invasive) Make hot-reload symmetric for built-in channels: when
channels.<id>.enabled: false → true is observed at reload time, pull the corresponding plugin into the registry on the same iteration. This would align WhatsApp's behavior with Telegram's configPrefixes: ["channels.telegram"] pattern.
A PR for (2) is in preparation and will follow this issue.
Workaround we shipped
Always set channels.whatsapp.enabled=true in the deployed config regardless of the user's channel selection. The plugin loads at every cold boot, the gateway's applyPluginAutoEnable alreadyEnabled short-circuit at src/config/plugin-auto-enable.shared.ts:953-957 fires on subsequent reloads, and the demotion stops happening. Migration for already-wedged bots: one-time docker restart after the config change lands, so cold-boot state matches the new shape.
Side effect on telegram-only bots: the WhatsApp channel runtime starts at cold boot and idles on 401 (no creds), generating [health-monitor] [whatsapp:default] restarting (reason: stopped) log lines every ~10-15 minutes and pushing Docker State.Health=unhealthy (because /readyz returns 503 with whatsapp in failing[]).
What we tried that didn't work (and why)
-
plugins.entries.whatsapp = { enabled: true } alone. Source analysis (hasNonDisabledPluginEntry + materializeConfiguredPluginEntryAllowlist) suggested this should preregister the plugin without enabling the channel. Live test: boot line stayed at 9 plugins, no whatsapp. The actual gate at src/plugins/gateway-startup-plugin-ids.ts:355-362 checks hasConfiguredStartupChannel against listPotentialEnabledChannelIds, which only emits whatsapp when channels.whatsapp has a meaningful (non-enabled) key AND enabled=true. plugins.entries is consulted later for the per-plugin gate but doesn't drive built-in channel discovery.
-
channels.whatsapp.enabled=true + channels.whatsapp.accounts.default.enabled=false. Source said the channel runtime is gated separately by account.enabled && cfg.web?.enabled !== false (extensions/whatsapp/src/shared.ts:227). Live test confirmed that part: plugin loaded (10 plugins), channel runtime stayed idle, no spam. But on a Link click, the CLI's web.login.start paired successfully and gateway.channels.start returned ✓ (2109ms) — yet the channel runtime did NOT actually start. creds.json ended up with registered: false, no [whatsapp] Listening for personal WhatsApp inbound messages, no Baileys process. End-to-end, the bot wasn't actually receiving messages. Suppressing the runtime via accounts.default.enabled=false also suppresses the post-pairing channel-start path.
-
Container restart on channel_type transition or first Link click. Reliable but heavyweight (~100s wait per Link click + Telegram interruption during the restart). Retired in favor of SIGUSR1 then upstream gateway.channels.start RPC — both of which work only when the plugin is already in the registry. Since pinning enabled=true at deploy guarantees that, restoring the heavyweight restart isn't needed. The "always enable" approach is strictly simpler.
References
extensions/whatsapp/src/shared.ts:222 — noopPrefixes opt-out (commit ba79d903137, 2026-03-17).
extensions/whatsapp/src/shared.ts:227 — isEnabled predicate gating channel runtime startup.
src/config/plugin-auto-enable.shared.ts:703-732 — disableImplicitPreferredOverPlugin writes plugins.entries.<id>.enabled=false, not channels.<id>.enabled=false.
src/config/plugin-auto-enable.shared.ts:734-744 — isBuiltInChannelAlreadyEnabled short-circuit predicate.
src/config/plugin-auto-enable.shared.ts:768-810 — registerPluginEntry only enables built-in channels.
src/config/plugin-auto-enable.shared.ts:829-875 — materializeConfiguredPluginEntryAllowlist can add plugin allowlist entries; it is not a channel-disable writer.
src/config/plugin-auto-enable.shared.ts:903-981 — materializePluginAutoEnableCandidatesInternal candidate loop.
src/config/io.write-prepare.ts:16-46, 256-275 — createMergePatch / resolvePersistCandidateForWrite; originally hypothesized as the demotion site, but only causal if nextConfig already carries stale enabled=false or another path reintroduces it before persistence.
src/config/io.ts:1970-2035, 2157-2183, 2429-2505 — gateway config write path, audit record creation, runtime-derived source projection, and write notifications.
src/config/io.audit.ts:134-141, 344-460 — config-audit JSONL infrastructure that proves a gateway-side write boundary but not, by itself, which caller supplied the false value.
src/config/runtime-snapshot.ts:132-145, 266-304 — pinned runtime/source snapshot state that later writes can use.
src/gateway/server-startup-config.ts:83-119 — startup auto-enable persistence path.
src/gateway/server.impl.ts:610-635 — control UI allowed-origin seeding candidate write path observed near the production size oscillation.
src/gateway/server-startup-config.ts:241-295, 371-384 — gateway auth/bootstrap override paths that can feed startup config writes.
src/gateway/server-methods/config-write-flow.ts:216-230 — RPC/control-plane config write path candidate.
src/cli/gateway-cli/run-loop.ts:494, 520 — SIGUSR1 listener and params.start() that re-runs the pipeline.
src/gateway/server-methods/channels.ts:368-405 — channels.start RPC handler that returns INVALID_REQUEST: invalid channels.start channel when the plugin isn't registered.
aa76cf43f0 — upstream commit adding the channels.start RPC the CLI calls after pairing.
Affects: OpenClaw v2026.5.2 (also reproducible on v2026.5.4-beta.1; relevant code paths unchanged in v2026.5.4 release, though we have not separately re-verified the demotion on v2026.5.4).
Related but distinct: #77508 (missing/disabled channel preflight error wording) and #70333 (auto-enable writes enabled keys back, causing churn). Neither covers the symmetric case here, where an external
channels.whatsapp.enabled=truewrite is silently reverted while the WhatsApp plugin is absent from the running registry.TL;DR
The OpenClaw gateway can silently ignore or overwrite externally-written
channels.whatsapp.enabled=truewhen the WhatsApp plugin isn't already in the running gateway's plugin registry. Combined with the WhatsApp plugin's intentionalnoopPrefixes: ["channels.whatsapp"]opt-out atextensions/whatsapp/src/shared.ts:222, downstream integrators that try to enable WhatsApp at runtime get an unrecoverable wedge: their disk write readsenabled=truemomentarily, the plugin never enters the registry, and a later gateway config write can persist the stale runtime view (enabled=false) back over disk.gateway.channels.startthen fails because the channel plugin is still absent.The wedge is silent. No error, no warn log, no signal that the integrator is using the wrong pattern. The only escape is a full container restart with
enabled=truealready on disk at cold boot.Repro on a production fleet
Setup
/data/deployments/<id>/openclaw.json) hadchannels.whatsapp.enabled=false./app/openclaw.mjs gateway.Action
User clicks "Link WhatsApp" in our dashboard. An external orchestrator (a sidecar process that owns the deployed config file) writes the config:
This is a plain
writeFileSync(truncate + rewrite).chokidarin the gateway picks it up.What we expected
The gateway reload pipeline observes
channels.whatsapp.enabled=true, loads the WhatsApp plugin into the registry on the next iteration ofrun-loop.ts, and the channel runtime starts (or stays idle if no creds). Subsequentweb.login.start/channels.startRPCs work end-to-end.What actually happens
Five SIGUSR1-keyed reload events fire on the test bot between 13:22 and 13:42 UTC. After each one, the gateway emits a
config.writeaudit entry and the file mtime advances. The disk content alternates between two shapes (4693 ↔ 4820 bytes; the +127 byte oscillation is thegateway.controlUi.allowedOriginsblock being added/removed — separate quirk).On every reload the gateway reverts
channels.whatsapp.enabledfromtrueback tofalse. The other 3 keys are preserved verbatim.Direct content diff between the orchestrator's last write (4818 bytes) and the gateway's subsequent rewrite (4820 bytes):
Audit log entries from
/data/deployments/<id>/logs/config-audit.jsonl(one entry per gateway-side write, 9 total during the click sequence):{"ts":"2026-05-04T13:32:30.947Z","pid":14,"argv":["/usr/local/bin/node","/app/openclaw.mjs","gateway"],"actor":"config-io","event":"config.write","previousBytes":4818,"nextBytes":4693,"result":"rename","previousHash":"17f0...","nextHash":"c3e3..."} {"ts":"2026-05-04T13:32:35.537Z","pid":14,"argv":["/usr/local/bin/node","/app/openclaw.mjs","gateway"],"actor":"config-io","event":"config.write","previousBytes":4693,"nextBytes":4820,"result":"rename"} ... (7 more) ...After the cycle settles,
channels.whatsapp.enabled=falseis what's persisted. The plugin remains absent fromloadPluginLookUpTable's output, the boot line still shows9 plugins (... telegram)instead of10 plugins (... telegram, whatsapp), and:is emitted when the OpenClaw login CLI calls
gateway.channels.start { channel: "whatsapp", accountId: "default" }peraa76cf43f0(fix(whatsapp): stabilize auth state and reconcile local runtime after CLI login).What does work (the asymmetric comparator)
A comparator bot whose cold-boot disk had
channels.whatsapp.enabled=truefrom deploy time loads the WhatsApp plugin at process start. On that bot,channels.whatsapp.enabledstaystrueacross reloads — no clobber. Boot line10 plugins (... whatsapp), audit log shows two writes at process start (controlUi seed + auto-enable) and zero subsequent writes over 16+ hours of uptime.The asymmetry is on cold-boot state, not on runtime behavior. Hot-reloading
channels.whatsapp.enabled: false → trueon a running gateway is unsupported in v2026.5.2.Root cause
One trigger is verified, and one write-origin hypothesis remains open.
1.
noopPrefixesopt-out atextensions/whatsapp/src/shared.ts:222This was introduced in commit
ba79d903137e35c4089cd7e98610eb11731ebb0f(2026-03-17, "refactor(whatsapp): share plugin base config") — a refactor commit with no explanatory message. Likely rationale: protect Baileys auth state from churn caused by config-edit-driven reloads. Telegram does NOT opt out (configPrefixes: ["channels.telegram"]), which is why Telegram channel-config edits hot-reload correctly.Effect: on
channels.whatsapp.*writes, the gateway emits[reload] channels.whatsapp.enabled changedthen no-ops (src/gateway/server-reload-handlers.ts). Plugin registry is not consulted for re-discovery.2. Exact demoting write origin remains unverified
The reconcile pass in
src/config/plugin-auto-enable.shared.tsenumerates candidates fromresolveConfiguredPluginAutoEnableCandidates. For each candidate:isPluginDenied/isPluginExplicitlyDisabled→ skip.shouldSkipPreferredPluginAutoEnable→ calldisableImplicitPreferredOverPlugin(writesplugins.entries.<id>.enabled = false, neverchannels.<id>.enabled).alreadyEnabledshort-circuit (line 953) — for built-ins, checkschannels.<id>.enabled === trueAND!allowMissing. If both true, skip.registerPluginEntrywriteschannels.<id>.enabled = true.registerPluginEntryonly enables.disableImplicitPreferredOverPluginonly writes plugin entries, never channel entries. There is no code path in this file that setschannels.<id>.enabled = false.The clobber comes from a later config-write path, not from the auto-enable candidate loop itself. When the WhatsApp plugin isn't in the running gateway's registry, the external
channels.whatsapp.*edit is classified as a no-op reload and does not rebuild the plugin registry. The gateway may still hold a pinned runtime/source config snapshot from cold boot wherechannels.whatsapp.enabled=false. A subsequent in-process gateway write (for example control UI origin seeding, generated auth/config metadata, startup/reload config persistence, pre-validation normalization, or another managed config write) could build itsnextConfigfrom stale state or otherwise reintroduceenabled=false. When that write is persisted, the on-disk value flips back to false. Subsequent reloads then findenabled=falseagain, and the cycle repeats.Important nuance verified against current source:
resolvePersistCandidateForWrite()does preserve an externally edited source value if the laternextConfigdoes not touch that path. If both inputs tocreateMergePatch()reflect the post-orchestrator disk state, the patch should be empty forchannels.whatsapp.enabled. The demotion therefore requires either a later gateway write whosenextConfigalready carries stalechannels.whatsapp.enabled=false, or an untraced normalization/write path that reintroduces that stale value before persistence. That matches the observed audit trail boundary: the demotion appears alongside gateway-side config writes, but source review alone has not identified which writer first supplies the false value.Why the
enabled=truewrite succeeds momentarily then losesThe
writeFileSyncis not atomic — Node's plain truncate-and-rewrite can produce torn states. But that's not the actual problem. The problem is stale runtime/source config ownership: the running gateway does not load the plugin onchannels.whatsapp.*edits, and later managed config writes can persist the old runtime snapshot back to disk. Atomicity would not make the plugin enter the registry or makechannels.startsucceed.Suggested fixes (any one closes the wedge for downstream integrators)
channels.<id>.enabled=trueback to false while that channel plugin is not registered.channels.whatsapp.enabled: false -> truebut the WhatsApp plugin is absent from the running registry andchannels.whatsappis a no-op reload prefix, return a clear warning/restart requirement instead of silently accepting a no-op.docs/channels/whatsapp.mddescribing thenoopPrefixesopt-out and the recommended workflow for downstream integrators: writechannels.whatsapp.enabled=truebefore cold boot if they need runtimeweb.login.start/channels.start, or restart the gateway after changing that flag from false to true.channels.<id>.enabled: false → trueis observed at reload time, pull the corresponding plugin into the registry on the same iteration. This would align WhatsApp's behavior with Telegram'sconfigPrefixes: ["channels.telegram"]pattern.A PR for (2) is in preparation and will follow this issue.
Workaround we shipped
Always set
channels.whatsapp.enabled=truein the deployed config regardless of the user's channel selection. The plugin loads at every cold boot, the gateway'sapplyPluginAutoEnablealreadyEnabledshort-circuit atsrc/config/plugin-auto-enable.shared.ts:953-957fires on subsequent reloads, and the demotion stops happening. Migration for already-wedged bots: one-timedocker restartafter the config change lands, so cold-boot state matches the new shape.Side effect on telegram-only bots: the WhatsApp channel runtime starts at cold boot and idles on 401 (no creds), generating
[health-monitor] [whatsapp:default] restarting (reason: stopped)log lines every ~10-15 minutes and pushing DockerState.Health=unhealthy(because/readyzreturns 503 withwhatsappinfailing[]).What we tried that didn't work (and why)
plugins.entries.whatsapp = { enabled: true }alone. Source analysis (hasNonDisabledPluginEntry+materializeConfiguredPluginEntryAllowlist) suggested this should preregister the plugin without enabling the channel. Live test: boot line stayed at9 plugins, no whatsapp. The actual gate atsrc/plugins/gateway-startup-plugin-ids.ts:355-362checkshasConfiguredStartupChannelagainstlistPotentialEnabledChannelIds, which only emits whatsapp whenchannels.whatsapphas a meaningful (non-enabled) key ANDenabled=true.plugins.entriesis consulted later for the per-plugin gate but doesn't drive built-in channel discovery.channels.whatsapp.enabled=true+channels.whatsapp.accounts.default.enabled=false. Source said the channel runtime is gated separately byaccount.enabled && cfg.web?.enabled !== false(extensions/whatsapp/src/shared.ts:227). Live test confirmed that part: plugin loaded (10 plugins), channel runtime stayed idle, no spam. But on a Link click, the CLI'sweb.login.startpaired successfully andgateway.channels.startreturned ✓ (2109ms) — yet the channel runtime did NOT actually start.creds.jsonended up withregistered: false, no[whatsapp] Listening for personal WhatsApp inbound messages, no Baileys process. End-to-end, the bot wasn't actually receiving messages. Suppressing the runtime viaaccounts.default.enabled=falsealso suppresses the post-pairing channel-start path.Container restart on channel_type transition or first Link click. Reliable but heavyweight (~100s wait per Link click + Telegram interruption during the restart). Retired in favor of SIGUSR1 then upstream
gateway.channels.startRPC — both of which work only when the plugin is already in the registry. Since pinningenabled=trueat deploy guarantees that, restoring the heavyweight restart isn't needed. The "always enable" approach is strictly simpler.References
extensions/whatsapp/src/shared.ts:222— noopPrefixes opt-out (commitba79d903137, 2026-03-17).extensions/whatsapp/src/shared.ts:227—isEnabledpredicate gating channel runtime startup.src/config/plugin-auto-enable.shared.ts:703-732—disableImplicitPreferredOverPluginwritesplugins.entries.<id>.enabled=false, notchannels.<id>.enabled=false.src/config/plugin-auto-enable.shared.ts:734-744—isBuiltInChannelAlreadyEnabledshort-circuit predicate.src/config/plugin-auto-enable.shared.ts:768-810—registerPluginEntryonly enables built-in channels.src/config/plugin-auto-enable.shared.ts:829-875—materializeConfiguredPluginEntryAllowlistcan add plugin allowlist entries; it is not a channel-disable writer.src/config/plugin-auto-enable.shared.ts:903-981—materializePluginAutoEnableCandidatesInternalcandidate loop.src/config/io.write-prepare.ts:16-46, 256-275—createMergePatch/resolvePersistCandidateForWrite; originally hypothesized as the demotion site, but only causal ifnextConfigalready carries staleenabled=falseor another path reintroduces it before persistence.src/config/io.ts:1970-2035, 2157-2183, 2429-2505— gateway config write path, audit record creation, runtime-derived source projection, and write notifications.src/config/io.audit.ts:134-141, 344-460— config-audit JSONL infrastructure that proves a gateway-side write boundary but not, by itself, which caller supplied the false value.src/config/runtime-snapshot.ts:132-145, 266-304— pinned runtime/source snapshot state that later writes can use.src/gateway/server-startup-config.ts:83-119— startup auto-enable persistence path.src/gateway/server.impl.ts:610-635— control UI allowed-origin seeding candidate write path observed near the production size oscillation.src/gateway/server-startup-config.ts:241-295, 371-384— gateway auth/bootstrap override paths that can feed startup config writes.src/gateway/server-methods/config-write-flow.ts:216-230— RPC/control-plane config write path candidate.src/cli/gateway-cli/run-loop.ts:494, 520— SIGUSR1 listener andparams.start()that re-runs the pipeline.src/gateway/server-methods/channels.ts:368-405—channels.startRPC handler that returnsINVALID_REQUEST: invalid channels.start channelwhen the plugin isn't registered.aa76cf43f0— upstream commit adding thechannels.startRPC the CLI calls after pairing.