Skip to content

feat(discord): route GUILD_MEMBER_ADD events to agents with optional welcome trigger#32306

Open
pdd-cli wants to merge 2 commits intoopenclaw:mainfrom
pdd-cli:pr/discord-member-add
Open

feat(discord): route GUILD_MEMBER_ADD events to agents with optional welcome trigger#32306
pdd-cli wants to merge 2 commits intoopenclaw:mainfrom
pdd-cli:pr/discord-member-add

Conversation

@pdd-cli
Copy link

@pdd-cli pdd-cli commented Mar 2, 2026

Closes #23978

Summary

  • Adds DiscordMemberAddListener that fires when a user joins a Discord guild and delivers a system event to the matched agent session.
  • Gated on intents.guildMembers: true (existing privileged intent flag — must also be enabled in the Discord Developer Portal).
  • Opt-in per guild via memberJoinNotifications: "on" (default: "off" to avoid floods on existing deployments).
  • Optional memberJoinChannel per guild: if set, triggers a gateway agent call to post a welcome message to that channel. Falls back to Discord's systemChannelId if no explicit channel is configured.

Config example

"guilds": {
  "123456789": {
    "memberJoinNotifications": "on",
    "memberJoinChannel": "987654321"
  }
}

Test plan

  • Join a guild with memberJoinNotifications: "off" (default) — no event fired
  • Join a guild with memberJoinNotifications: "on" — system event delivered to matched agent
  • Join with memberJoinChannel set — agent posts welcome to that channel
  • Join with no memberJoinChannel but Discord systemChannelId set — falls back correctly
  • Bot join events are skipped

@openclaw-barnacle openclaw-barnacle bot added channel: discord Channel integration: discord size: S labels Mar 2, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR adds a DiscordMemberAddListener that routes GUILD_MEMBER_ADD events to the matched agent session and optionally triggers a welcome-message gateway call. The feature is well-scoped: it is gated behind the existing intents.guildMembers flag, defaults to "off" per guild to prevent unexpected floods on existing deployments, and correctly skips bot users (including the self-bot).

Key points:

  • Config types, Zod schema, and the resolved-guild-entry type are all updated consistently.
  • Bot-skip logic (generic user.bot flag + specific botUserId identity check) and the "off" default are appropriate safeguards.
  • Registration in provider.ts mirrors the existing DiscordPresenceListener pattern.
  • One type-safety issue: data.guild.systemChannelId is accessed without optional chaining on line 702, while data.guild?.name and data.guild?.id use optional chaining just a few lines above. Although the control flow guarantees a non-null guild at that point, TypeScript's strict-null analysis does not track that invariant and will flag this as an error. Using data.guild?.systemChannelId?.trim() aligns with the surrounding style and eliminates the risk.

Confidence Score: 4/5

  • Safe to merge after fixing the missing optional-chain on data.guild.systemChannelId.
  • The feature is well-structured, correctly opt-in, and mirrors existing listener patterns. The single issue (direct property access on a potentially-null object) is a TypeScript type-safety concern that is unlikely to cause a runtime crash due to control-flow guarantees, but should still be fixed for correctness and to avoid a strict-null compile error.
  • src/discord/monitor/listeners.ts — line 702: data.guild.systemChannelId direct access without optional chaining.

Last reviewed commit: 15c1036

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +700 to +703
const welcomeChannelId =
guildInfo?.memberJoinChannel?.trim() ||
(typeof data.guild.systemChannelId === "string" ? data.guild.systemChannelId.trim() : null) ||
null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null-guard on data.guild when reading systemChannelId

A few lines earlier the same object is accessed with optional chaining (data.guild?.name on line 678, data.guild?.id on line 686), making it clear that the type of data.guild is nullable. The direct property read data.guild.systemChannelId on line 702 will throw a TypeError at runtime if data.guild is null or undefined.

In practice the control flow guarantees non-null here (a null guild causes resolveDiscordGuildEntry to return null, which then defaults mode to "off" and returns early), but TypeScript's static analysis doesn't track that invariant. This will likely produce a strict-null type error and is a fragile assumption.

Suggested change
const welcomeChannelId =
guildInfo?.memberJoinChannel?.trim() ||
(typeof data.guild.systemChannelId === "string" ? data.guild.systemChannelId.trim() : null) ||
null;
const welcomeChannelId =
guildInfo?.memberJoinChannel?.trim() ||
data.guild?.systemChannelId?.trim() ||
null;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/discord/monitor/listeners.ts
Line: 700-703

Comment:
**Missing null-guard on `data.guild` when reading `systemChannelId`**

A few lines earlier the same object is accessed with optional chaining (`data.guild?.name` on line 678, `data.guild?.id` on line 686), making it clear that the type of `data.guild` is nullable. The direct property read `data.guild.systemChannelId` on line 702 will throw a `TypeError` at runtime if `data.guild` is `null` or `undefined`.

In practice the control flow guarantees non-null here (a null guild causes `resolveDiscordGuildEntry` to return `null`, which then defaults `mode` to `"off"` and returns early), but TypeScript's static analysis doesn't track that invariant. This will likely produce a strict-null type error and is a fragile assumption.

```suggestion
    const welcomeChannelId =
      guildInfo?.memberJoinChannel?.trim() ||
      data.guild?.systemChannelId?.trim() ||
      null;
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15c103648e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


const userTag = formatDiscordUserTag(user);
const text = `Discord member joined: ${userTag} joined ${guildSlug}`;
const contextKey = `discord:member-add:${data.guild?.id}:${user.id}`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope member-add idempotency key by account or event instance

contextKey is reused as the agent call idempotencyKey, but its value only includes guild and user IDs. The gateway dedupes agent requests by agent:${idempotencyKey} in a global cache window, so distinct join events can be dropped (for example, a quick leave/rejoin of the same user, or two configured Discord accounts receiving the same guild join) even though they should each trigger a welcome run.

Useful? React with 👍 / 👎.

accountId: params.accountId,
guildId: data.guild?.id,
memberRoleIds,
peer: null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve guild/channel session isolation for join-triggered runs

Resolving the route with peer: null collapses these join-triggered agent runs into the agent main session instead of a guild/channel-scoped session. That causes conversation state for welcome generation to be shared across unrelated guilds/channels, so prior joins in one guild can influence welcome messages delivered to another.

Useful? React with 👍 / 👎.

@pdd-cli pdd-cli force-pushed the pr/discord-member-add branch from 15c1036 to 2cc790e Compare March 4, 2026 00:20
Prompt Driven added 2 commits March 3, 2026 16:20
Adds a DiscordMemberAddListener that fires when a user joins a Discord
guild and delivers a system event to the matched agent session. Gated on
the existing intents.guildMembers privileged intent flag (must also be
enabled in the Discord Developer Portal). Opt-in per guild via
memberJoinNotifications: 'on' (default: 'off' to avoid high-volume
floods on existing deployments).

Closes openclaw#23978
@pdd-cli pdd-cli force-pushed the pr/discord-member-add branch from 2cc790e to 5b4897f Compare March 4, 2026 00:20
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b4897fa0c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +650 to +653
const guildInfo = resolveDiscordGuildEntry({ guild: data.guild, guildEntries });

// Default is off — must be explicitly enabled per guild
const mode = guildInfo?.memberJoinNotifications ?? "off";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve member-add guild by ID when guild cache is missing

This handler only resolves guildInfo from data.guild, then defaults memberJoinNotifications to "off" when that object is unavailable; in that case the function returns early and skips both system-event enqueue and welcome triggering even for guilds explicitly configured by ID. Other Discord ingress paths in this file already treat the guild object as optional and key off raw guild IDs, so this listener should also use an ID fallback (for routing/config lookup) instead of hard-depending on a hydrated guild object.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(discord): route GUILD_MEMBER_ADD events to agents

1 participant