Skip to content

feat: Zulip channel plugin#12183

Closed
FtlC-ian wants to merge 17 commits intoopenclaw:mainfrom
FtlC-ian:feat/zulip-plugin
Closed

feat: Zulip channel plugin#12183
FtlC-ian wants to merge 17 commits intoopenclaw:mainfrom
FtlC-ian:feat/zulip-plugin

Conversation

@FtlC-ian
Copy link
Copy Markdown

@FtlC-ian FtlC-ian commented Feb 8, 2026

Zulip Channel Plugin

Adds a full-featured Zulip channel plugin for OpenClaw.

Features

  • Event queue polling with long-poll and exponential backoff
  • Authenticated upload downloads from Zulip's /user_uploads/ paths
  • Outbound file uploads via /api/v1/user_uploads
  • Reaction indicators — 👀 on start, ✅ on success, ⚠️ on error (configurable)
  • Topic directive — agents can switch topics with [[zulip_topic: <topic>]]
  • HTTP retry with exponential backoff for 429/502/503/504
  • Full actions API — stream CRUD, user management, reactions, search, pins
  • Emoji normalization — accepts both :eyes: and eyes formats
  • Topic → session mapping — each topic gets its own conversation session
  • Channel docs and unit tests (19 passing)

Config

{
  channels: {
    zulip: {
      enabled: true,
      url: "https://zulip.example.com",
      email: "[email protected]",
      apiKey: "...",
      streams: ["my-stream"],
      reactions: { enabled: true, onStart: "eyes", onSuccess: "check_mark" },
    },
  },
}

Context

This was developed to address the Zulip integration request in #5163. We reviewed both existing PRs (#8365 by @rafaelreis-r and #9643 by @jamie-dit) and incorporated the best ideas from each:

What this PR adds beyond both:

  • Full channel actions API (stream CRUD, user management, org settings)
  • Authenticated upload download + outbound upload support
  • Comprehensive backoff/retry across all API calls (not just polling)

Testing

  • ✅ Unit tests: 19 passing (upload extraction, emoji normalization, channel config)
  • ✅ Manual testing against live Zulip instance
  • ✅ Security audit (no blocking issues)

Refs: #5163, #8365, #9643

Greptile Overview

Greptile Summary

This PR adds a new @openclaw/zulip channel plugin under extensions/zulip, including config schema, onboarding hooks, outbound send support (including uploads), and an event-queue long-polling monitor that routes stream/topic messages into per-topic sessions. It also updates the channel docs index and marks Zulip as markdown-capable in src/utils/message-channel.ts.

Key integration points:

  • Channel plugin registration via extensions/zulip/index.ts + openclaw.plugin.json + per-extension package.json.
  • Runtime message ingestion via extensions/zulip/src/zulip/monitor.ts (event queue registration + long-poll loop, routing/session mapping, optional reactions/typing indicators, media download from /user_uploads/).
  • Outbound messaging via extensions/zulip/src/zulip/send.ts (target parsing, optional upload to Zulip, markdown table conversion).

Primary merge blockers are config write/read mismatches in the channel setup path (URL written to a different key than the rest of the plugin reads), which will prevent successful configuration from actually working at runtime.

Confidence Score: 3/5

  • This PR is close, but has a config write/read mismatch that will break real-world setup.
  • Main functionality is self-contained in the new extension and the overall approach matches existing channel patterns, but the setup path writes the Zulip base URL under the wrong config key, which will cause configured accounts to fail to start. Fixing that and ensuring event polling types are stable would make this safer to merge.
  • extensions/zulip/src/channel.ts, extensions/zulip/src/zulip/monitor.ts

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Adds a full Zulip channel plugin with:
- Event queue polling with exponential backoff
- Authenticated upload downloads from /user_uploads/
- Outbound file uploads via /api/v1/user_uploads
- Reaction indicators (eyes → check_mark/warning)
- Topic directive [[zulip_topic: <topic>]]
- HTTP retry with backoff (429/502/503/504)
- Full actions API (stream CRUD, user management, reactions)
- Channel docs and unit tests

Refs: #5163, openclaw#8365, openclaw#9643
@openclaw-barnacle openclaw-barnacle bot added the docs Improvements or additions to documentation label Feb 8, 2026
Copy link
Copy Markdown
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.

2 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +281 to +283
}
return null;
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wrong config key
applyAccountConfig writes the base URL to channels.zulip.url, but the rest of the plugin reads account.baseUrl (resolved from the url field in config). This leaves the default account unconfigured at runtime even though setup succeeded.

Suggested change
}
return null;
},
...(baseUrl ? { url: baseUrl } : {}),
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/zulip/src/channel.ts
Line: 281:283

Comment:
**Wrong config key**
`applyAccountConfig` writes the base URL to `channels.zulip.url`, but the rest of the plugin reads `account.baseUrl` (resolved from the `url` field in config). This leaves the default account unconfigured at runtime even though setup succeeded.

```suggestion
                    ...(baseUrl ? { url: baseUrl } : {}),
```

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

Comment on lines +306 to +308
...next,
channels: {
...next.channels,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wrong config key
Same issue as the default-account path above: the account-scoped config writes channels.zulip.accounts[accountId].url but uses the url field name inconsistently elsewhere (resolved as baseUrl). This makes non-default accounts appear configured but fail at runtime when account.baseUrl is read.

Suggested change
...next,
channels: {
...next.channels,
...(baseUrl ? { url: baseUrl } : {}),
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/zulip/src/channel.ts
Line: 306:308

Comment:
**Wrong config key**
Same issue as the default-account path above: the account-scoped config writes `channels.zulip.accounts[accountId].url` but uses the `url` field name inconsistently elsewhere (resolved as `baseUrl`). This makes non-default accounts appear configured but fail at runtime when `account.baseUrl` is read.

```suggestion
                ...(baseUrl ? { url: baseUrl } : {}),
```

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

Comment on lines +719 to +722
onModelSelected,
},
});
} catch (err) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Event ID type mismatch
lastEventId is assigned from events[events.length - 1].id without normalization; Zulip's event IDs are numbers, and passing a number where the client expects a string can break subsequent polling (e.g., if request building/URLSearchParams assumes string). Consider coercing to String(...) at assignment to keep the type stable across re-register + retry paths.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/zulip/src/zulip/monitor.ts
Line: 719:722

Comment:
**Event ID type mismatch**
`lastEventId` is assigned from `events[events.length - 1].id` without normalization; Zulip's event IDs are numbers, and passing a number where the client expects a string can break subsequent polling (e.g., if request building/URLSearchParams assumes string). Consider coercing to `String(...)` at assignment to keep the type stable across re-register + retry paths.

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

Addresses Greptile review feedback: ensure lastEventId stays
as a number type when assigned from event response data.
Copy link
Copy Markdown
Author

@FtlC-ian FtlC-ian left a comment

Choose a reason for hiding this comment

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

Thanks for the review! Addressing the feedback:

Config key (lines 283, 308): This is actually correct as-is. The config schema field is url (see config-schema.ts), and accounts.ts reads merged.url to resolve baseUrl. Writing { url: baseUrl } maps the user input to the correct config key.

Event ID type (line 722): Good catch — added Number() coercion in 4e52c40 to keep the type stable.

@rafaelreis-r
Copy link
Copy Markdown
Contributor

Note for Zulip plugin users: Zulip natively renders markdown tables, but OpenClaw's default table mode converts them to code blocks. To get proper table rendering in Zulip, add this to your config:

\\json
{
channels: {
zulip: {
markdown: {
tables: off
}
}
}
}
\\

Without this, tables appear as monospace code instead of formatted tables. Might be worth setting \off\ as the default for the Zulip channel since Zulip supports GFM tables natively.

@FtlC-ian
Copy link
Copy Markdown
Author

Good catch! Added ["zulip", "off"] to DEFAULT_TABLE_MODES in c77c9c5 — Zulip now defaults to passing tables through as-is since it handles GFM tables natively. No config needed.

Adds a full Zulip channel plugin with:
- Event queue polling with exponential backoff
- Authenticated upload downloads from /user_uploads/
- Outbound file uploads via /api/v1/user_uploads
- Reaction indicators (eyes → check_mark/warning)
- Topic directive [[zulip_topic: <topic>]]
- HTTP retry with backoff (429/502/503/504)
- Full actions API (stream CRUD, user management, reactions)
- Channel docs and unit tests

Refs: #5163, openclaw#8365, openclaw#9643
Addresses Greptile review feedback: ensure lastEventId stays
as a number type when assigned from event response data.
Renamed 4 duplicate function names to avoid code-analysis failures:
- normalizeEmojiName → normalizeZulipEmojiName (uploads.ts)
- readStringArrayParam → parseStringArrayParam (actions.ts)
- resolveLocalPath → resolveZulipLocalPath (send.ts)
- sleep → delay (monitor.ts, client.ts)

Formatted accounts.ts and monitor.ts with oxfmt.
@openclaw-barnacle openclaw-barnacle bot added channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: line Channel integration: line labels Feb 11, 2026
@openclaw-barnacle openclaw-barnacle bot added extensions: llm-task Extension: llm-task extensions: lobster Extension: lobster extensions: memory-core Extension: memory-core extensions: memory-lancedb Extension: memory-lancedb extensions: open-prose Extension: open-prose extensions: qwen-portal-auth Extension: qwen-portal-auth cli CLI command changes scripts Repository scripts commands Command implementations docker Docker and sandbox tooling agents Agent runtime and tooling channel: feishu Channel integration: feishu channel: twitch Channel integration: twitch extensions: device-pair extensions: minimax-portal-auth extensions: phone-control channel: irc labels Feb 11, 2026
@thewilloftheshadow thewilloftheshadow added the trigger-response This label just triggers the auto-response workflow and should be ignored otherwise. label Feb 12, 2026
@openclaw-barnacle openclaw-barnacle bot removed the trigger-response This label just triggers the auto-response workflow and should be ignored otherwise. label Feb 12, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it has more than 20 labels, which usually means the branch is too noisy. Please recreate the PR from a clean branch.

@thewilloftheshadow thewilloftheshadow added the trigger-response This label just triggers the auto-response workflow and should be ignored otherwise. label Feb 12, 2026
@openclaw-barnacle openclaw-barnacle bot removed the trigger-response This label just triggers the auto-response workflow and should be ignored otherwise. label Feb 12, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it has more than 20 labels, which usually means the branch is too noisy. Please recreate the PR from a clean branch.

FtlC-ian added a commit to FtlC-ian/openclaw that referenced this pull request Feb 12, 2026
Adds a full-featured Zulip channel plugin with:

Core Features:
- Event queue polling with exponential backoff and auto-recovery
- Authenticated upload downloads from /user_uploads/
- Outbound file uploads via /api/v1/user_uploads
- Reaction indicators (eyes → check_mark/warning)
- Topic directive [[zulip_topic: <topic>]]
- HTTP retry with backoff (429/502/503/504)
- Full actions API (stream CRUD, user management, reactions)
- Channel docs and unit tests

Key Improvement Over Existing PRs:
- Implements CONCURRENT message processing with staggered start times
- Other PRs (openclaw#9643, openclaw#14182) process messages sequentially, causing
  reply delays and "message dumps" when multiple messages arrive
- This implementation starts processing each message immediately with
  a 200ms stagger for natural conversation flow
- Replies arrive as each finishes instead of all at once

Technical Details:
- Fire-and-forget message processing with per-message error handling
- Maintains event cursor and queue ID correctly during concurrent processing
- Tested live and confirmed significant UX improvement

Refs: openclaw#5163, openclaw#8365, openclaw#9643, openclaw#12183
simpliq-marvin pushed a commit to simpliq-marvin/openclaw that referenced this pull request Feb 26, 2026
Adds a full-featured Zulip channel plugin with:

Core Features:
- Event queue polling with exponential backoff and auto-recovery
- Authenticated upload downloads from /user_uploads/
- Outbound file uploads via /api/v1/user_uploads
- Reaction indicators (eyes → check_mark/warning)
- Topic directive [[zulip_topic: <topic>]]
- HTTP retry with backoff (429/502/503/504)
- Full actions API (stream CRUD, user management, reactions)
- Channel docs and unit tests

Key Improvement Over Existing PRs:
- Implements CONCURRENT message processing with staggered start times
- Other PRs (openclaw#9643, openclaw#14182) process messages sequentially, causing
  reply delays and "message dumps" when multiple messages arrive
- This implementation starts processing each message immediately with
  a 200ms stagger for natural conversation flow
- Replies arrive as each finishes instead of all at once

Technical Details:
- Fire-and-forget message processing with per-message error handling
- Maintains event cursor and queue ID correctly during concurrent processing
- Tested live and confirmed significant UX improvement

Refs: openclaw#5163, openclaw#8365, openclaw#9643, openclaw#12183
FtlC-ian added a commit to FtlC-ian/openclaw that referenced this pull request Feb 27, 2026
Adds a full-featured Zulip channel plugin with:

Core Features:
- Event queue polling with exponential backoff and auto-recovery
- Authenticated upload downloads from /user_uploads/
- Outbound file uploads via /api/v1/user_uploads
- Reaction indicators (eyes → check_mark/warning)
- Topic directive [[zulip_topic: <topic>]]
- HTTP retry with backoff (429/502/503/504)
- Full actions API (stream CRUD, user management, reactions)
- Channel docs and unit tests

Key Improvement Over Existing PRs:
- Implements CONCURRENT message processing with staggered start times
- Other PRs (openclaw#9643, openclaw#14182) process messages sequentially, causing
  reply delays and "message dumps" when multiple messages arrive
- This implementation starts processing each message immediately with
  a 200ms stagger for natural conversation flow
- Replies arrive as each finishes instead of all at once

Technical Details:
- Fire-and-forget message processing with per-message error handling
- Maintains event cursor and queue ID correctly during concurrent processing
- Tested live and confirmed significant UX improvement

Refs: openclaw#5163, openclaw#8365, openclaw#9643, openclaw#12183
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: android App: android app: ios App: ios app: macos App: macos app: web-ui App: web-ui channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser cli CLI command changes commands Command implementations docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: copilot-proxy Extension: copilot-proxy extensions: device-pair extensions: diagnostics-otel Extension: diagnostics-otel extensions: google-antigravity-auth Extension: google-antigravity-auth extensions: google-gemini-cli-auth Extension: google-gemini-cli-auth extensions: llm-task Extension: llm-task extensions: lobster Extension: lobster extensions: memory-core Extension: memory-core extensions: memory-lancedb Extension: memory-lancedb extensions: minimax-portal-auth extensions: open-prose Extension: open-prose extensions: phone-control extensions: qwen-portal-auth Extension: qwen-portal-auth gateway Gateway runtime scripts Repository scripts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants