fix: suppress notifier plugin warnings from CLI output#1684
fix: suppress notifier plugin warnings from CLI output#1684i-trytoohard wants to merge 1 commit intomainComposioHQ/agent-orchestrator:mainfrom fix/notifier-log-spam-1681ComposioHQ/agent-orchestrator:fix/notifier-log-spam-1681Copy head branch name to clipboard
Conversation
Notifier plugins emit console.warn() during create() when config is missing (no webhook URL, no token, etc). Since getPluginRegistry() is called by almost every CLI command, these warnings spam the terminal on every invocation — even for commands unrelated to notifications. Add suppressNotifierWarnings option (default: true) to createPluginRegistry(). When enabled, console.warn is temporarily silenced during notifier plugin create() calls. Only ao doctor disables suppression, so warnings appear where they belong — during diagnostics. Fixes #1681 Affected commands (no longer spam): ao status, ao session ls/show/kill/restore/claim-pr/detail/cleanup, ao spawn, ao send, ao start, ao stop, ao review-check, ao report, ao verify, ao plugin list/install
Test Coverage Report
Per-file breakdown
Uncovered lines
|
Greptile SummaryThis PR centralises the
Confidence Score: 4/5Safe to merge; the change correctly solves the warning spam problem and is well-scoped to notifier initialization paths. The core helper monkey-patches a global ( packages/core/src/plugin-registry.ts — the
|
| Filename | Overview |
|---|---|
| packages/core/src/plugin-registry.ts | Adds CreatePluginRegistryOptions interface and withSuppressedWarn helper that monkey-patches global console.warn during each plugin.create() call; suppression is synchronous-only and unconstrained by TypeScript, which can be a hazard if async plugins are introduced or tests run in parallel. |
| packages/cli/src/commands/doctor.ts | Opts out of warning suppression with suppressNotifierWarnings: false so diagnostic output remains visible; minimal, correct change. |
| packages/cli/src/commands/plugin.ts | Removes the local try/finally console.warn suppression hack and delegates to the new centralised option; cleaner and equivalent. |
| packages/core/src/index.ts | Re-exports the new CreatePluginRegistryOptions type; trivial, no issues. |
Sequence Diagram
sequenceDiagram
participant CLI as CLI Command
participant Registry as createPluginRegistry()
participant Suppressor as withSuppressedWarn()
participant Global as console.warn (global)
participant Plugin as notifier plugin.create()
CLI->>Registry: createPluginRegistry({ suppressNotifierWarnings: true/false })
Registry->>Registry: loadFromConfig() → loadBuiltins()
Registry->>Suppressor: withSuppressedWarn(() => plugin.create(config))
alt suppressNotifierWarnings = true
Suppressor->>Global: console.warn = noop
Suppressor->>Plugin: plugin.create(config)
Plugin-->>Suppressor: instance
Suppressor->>Global: console.warn = original (finally)
else suppressNotifierWarnings = false (ao doctor)
Suppressor->>Plugin: plugin.create(config)
Plugin->>Global: console.warn notifier warning
Plugin-->>Suppressor: instance
end
Suppressor-->>Registry: instance
Registry-->>CLI: PluginRegistry
Comments Outside Diff (1)
-
packages/cli/src/commands/verify.ts, line 60 (link)Silent adoption of warning suppression via default
verify.ts(and similarlycreate-session-manager.tsandpackages/web/src/lib/services.ts) callscreatePluginRegistry()with no arguments and will inheritsuppressNotifierWarnings: truefrom the new default. Consider passing{ suppressNotifierWarnings: true }explicitly at these sites so the intent is self-documenting — similar to howplugin.tswas updated — rather than relying on a default that could change.Prompt To Fix With AI
This is a comment left during a code review. Path: packages/cli/src/commands/verify.ts Line: 60 Comment: **Silent adoption of warning suppression via default** `verify.ts` (and similarly `create-session-manager.ts` and `packages/web/src/lib/services.ts`) calls `createPluginRegistry()` with no arguments and will inherit `suppressNotifierWarnings: true` from the new default. Consider passing `{ suppressNotifierWarnings: true }` explicitly at these sites so the intent is self-documenting — similar to how `plugin.ts` was updated — rather than relying on a default that could change. How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
packages/core/src/plugin-registry.ts:414-423
**Global `console.warn` mutation swallows unrelated warnings**
`withSuppressedWarn` replaces `console.warn` on the global `console` object for the duration of each `plugin.create()` call. Any third-party library code that happens to call `console.warn` synchronously inside `create()` — not just the notifier's own misconfiguration messages — will also be silently dropped. In test environments running multiple `createPluginRegistry` instances concurrently (e.g., parallel test workers sharing the same Node.js process), two calls entering `withSuppressedWarn` in an interleaved fashion can leave `console.warn` permanently silenced if the inner call restores a no-op as `original`.
### Issue 2 of 3
packages/core/src/plugin-registry.ts:414
**`withSuppressedWarn` type allows async `fn` but suppression ends synchronously**
The generic `T` is unconstrained, so TypeScript happily accepts an async lambda: `withSuppressedWarn(() => asyncCreate(config))`. In that case the `finally` block restores `console.warn` as soon as the Promise is returned — before the async body runs — making the suppression a no-op for async plugins. A compile-time guard prevents this category of silent breakage.
```suggestion
function withSuppressedWarn<T extends Exclude<unknown, Promise<unknown>>>(fn: () => T): T {
```
### Issue 3 of 3
packages/cli/src/commands/verify.ts:60
**Silent adoption of warning suppression via default**
`verify.ts` (and similarly `create-session-manager.ts` and `packages/web/src/lib/services.ts`) calls `createPluginRegistry()` with no arguments and will inherit `suppressNotifierWarnings: true` from the new default. Consider passing `{ suppressNotifierWarnings: true }` explicitly at these sites so the intent is self-documenting — similar to how `plugin.ts` was updated — rather than relying on a default that could change.
Reviews (1): Last reviewed commit: "fix: suppress notifier plugin warnings f..." | Re-trigger Greptile
| function withSuppressedWarn<T>(fn: () => T): T { | ||
| if (!suppressNotifierWarnings) return fn(); | ||
| const original = console.warn; | ||
| console.warn = () => {}; | ||
| try { | ||
| return fn(); | ||
| } finally { | ||
| console.warn = original; | ||
| } | ||
| } |
There was a problem hiding this comment.
Global
console.warn mutation swallows unrelated warnings
withSuppressedWarn replaces console.warn on the global console object for the duration of each plugin.create() call. Any third-party library code that happens to call console.warn synchronously inside create() — not just the notifier's own misconfiguration messages — will also be silently dropped. In test environments running multiple createPluginRegistry instances concurrently (e.g., parallel test workers sharing the same Node.js process), two calls entering withSuppressedWarn in an interleaved fashion can leave console.warn permanently silenced if the inner call restores a no-op as original.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/plugin-registry.ts
Line: 414-423
Comment:
**Global `console.warn` mutation swallows unrelated warnings**
`withSuppressedWarn` replaces `console.warn` on the global `console` object for the duration of each `plugin.create()` call. Any third-party library code that happens to call `console.warn` synchronously inside `create()` — not just the notifier's own misconfiguration messages — will also be silently dropped. In test environments running multiple `createPluginRegistry` instances concurrently (e.g., parallel test workers sharing the same Node.js process), two calls entering `withSuppressedWarn` in an interleaved fashion can leave `console.warn` permanently silenced if the inner call restores a no-op as `original`.
How can I resolve this? If you propose a fix, please make it concise.| * Used to suppress noisy notifier misconfiguration warnings that | ||
| * should only appear during `ao doctor` diagnostics. | ||
| */ | ||
| function withSuppressedWarn<T>(fn: () => T): T { |
There was a problem hiding this comment.
withSuppressedWarn type allows async fn but suppression ends synchronously
The generic T is unconstrained, so TypeScript happily accepts an async lambda: withSuppressedWarn(() => asyncCreate(config)). In that case the finally block restores console.warn as soon as the Promise is returned — before the async body runs — making the suppression a no-op for async plugins. A compile-time guard prevents this category of silent breakage.
| function withSuppressedWarn<T>(fn: () => T): T { | |
| function withSuppressedWarn<T extends Exclude<unknown, Promise<unknown>>>(fn: () => T): T { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/plugin-registry.ts
Line: 414
Comment:
**`withSuppressedWarn` type allows async `fn` but suppression ends synchronously**
The generic `T` is unconstrained, so TypeScript happily accepts an async lambda: `withSuppressedWarn(() => asyncCreate(config))`. In that case the `finally` block restores `console.warn` as soon as the Promise is returned — before the async body runs — making the suppression a no-op for async plugins. A compile-time guard prevents this category of silent breakage.
```suggestion
function withSuppressedWarn<T extends Exclude<unknown, Promise<unknown>>>(fn: () => T): T {
```
How can I resolve this? If you propose a fix, please make it concise.
Problem
Notifier plugins (
notifier-discord,notifier-slack,notifier-openclaw,notifier-webhook,notifier-composio,notifier-desktop) emit unconditionalconsole.warn()during theircreate()initialization when config is missing (no webhook URL, no token, unsupported OS).Since
getPluginRegistry()is called by almost every CLI command, these warnings spam the terminal on every invocation — even commands that have nothing to do with notifications.Affected commands (all of these trigger the spam)
ao status,ao session ls,ao session show,ao session kill,ao session restore,ao session claim-pr,ao session detail,ao session cleanup,ao spawn,ao send,ao start,ao stop,ao review-check,ao report,ao verify,ao plugin list,ao plugin installExample spam on every
ao statusrunFix
Add
suppressNotifierWarningsoption (default:true) tocreatePluginRegistry(). When enabled,console.warnis temporarily silenced during notifier plugincreate()calls via awithSuppressedWarn()wrapper.Only
ao doctordisables suppression (suppressNotifierWarnings: false), so warnings appear where they belong — during diagnostics.Files changed
packages/core/src/plugin-registry.ts— AddedCreatePluginRegistryOptionsinterface andwithSuppressedWarn()helper. AllregisterNotifier()calls now route through it.packages/core/src/index.ts— ExportCreatePluginRegistryOptionstype.packages/cli/src/commands/doctor.ts— Creates registry withsuppressNotifierWarnings: false.packages/cli/src/commands/plugin.ts— Removes manualconsole.warnsuppression hack, uses the new option instead.Tests
plugin-registry.test.ts: 37/37 pass ✅doctor.test.ts: 10/10 pass ✅plugin.test.ts: 6/6 pass ✅pnpm build: clean ✅Closes #1681