feat: Zulip channel plugin with concurrent message processing#15051
feat: Zulip channel plugin with concurrent message processing#15051FtlC-ian wants to merge 13 commits intoopenclaw:mainfrom
Conversation
| mediaPaths.push(saved.path); | ||
| mediaTypes.push(saved.contentType); | ||
| mediaUrls.push(uploadUrl); | ||
| } |
There was a problem hiding this comment.
Silent media download failures
The /user_uploads download loop swallows all exceptions (catch (err) {}), so attachment fetch failures become invisible and users will see missing media with no diagnostic trail. Please log (at least debug/verbose) when a download/save fails so the operator can understand why attachments aren’t being forwarded.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/zulip/src/zulip/monitor.ts
Line: 333:336
Comment:
**Silent media download failures**
The `/user_uploads` download loop swallows all exceptions (`catch (err) {}`), so attachment fetch failures become invisible and users will see missing media with no diagnostic trail. Please log (at least debug/verbose) when a download/save fails so the operator can understand why attachments aren’t being forwarded.
How can I resolve this? If you propose a fix, please make it concise.| } else { | ||
| tempFilePath = await writeTempFile(fetched.buffer, filename); | ||
| tempFileCleanup = true; | ||
| } | ||
| const upload = await uploadZulipFile(client, tempFilePath); | ||
| mediaUrl = upload.url; | ||
| if (tempFileCleanup && tempFilePath) { | ||
| await fsPromises.unlink(tempFilePath).catch(() => undefined); | ||
| } |
There was a problem hiding this comment.
Temp directory leak
When mediaUrl is a remote URL, writeTempFile() creates a mkdtemp directory, but the cleanup only unlinks tempFilePath and never removes the containing temp directory. Over time this will leave many empty zulip-upload-* dirs in the system temp folder. Consider removing the temp dir after the upload (or reuse core.channel.media.saveMediaBuffer exclusively).
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/zulip/src/zulip/send.ts
Line: 176:184
Comment:
**Temp directory leak**
When `mediaUrl` is a remote URL, `writeTempFile()` creates a `mkdtemp` directory, but the cleanup only unlinks `tempFilePath` and never removes the containing temp directory. Over time this will leave many empty `zulip-upload-*` dirs in the system temp folder. Consider removing the temp dir after the upload (or reuse `core.channel.media.saveMediaBuffer` exclusively).
How can I resolve this? If you propose a fix, please make it concise.|
Great to see this coming, from my side, thank you!! |
bfc1ccb to
f92900f
Compare
3f3c879 to
4ea2be4
Compare
|
Just tested this branch, thanks for your work on this! Based on a quick test, it works great, except there's some oddities with message processing. At one point the agent lost track of the thread context, and on another thread, when I asked the agent to fetch some data via Zotero MCP before answering, it posted the answer twice, and this in between the two answers:
Please let me know if you need more details or if I can help more with testing. |
|
Hi @FtlC-ian! 👋 I've been reviewing your Zulip plugin implementation and I'm impressed with the work you've done here. The typing notification implementation in particular is well-designed:
I noticed there's an opportunity to add unit tests for the typing functionality. The existing test coverage includes:
Missing test coverage for:
I'd like to contribute tests for the typing functionality to improve the plugin's reliability. Would you be open to collaborating on this? I can either:
Let me know what works best for you! Quick PR assessment:
Thanks for the great work on this plugin! |
|
Hey @baustin27, thanks for the kind words and the thorough review! 🙏 Adding typing tests is a great idea — that's a real gap in the current coverage. I'd say option 2 (follow-up PR after merge) would work best. The branch has been pretty active with rebases and it'd be easier to avoid coordination headaches on a fork branch. Once this lands, feel free to open a PR with the typing tests and I'll be happy to review it. Re: the pnpm-lock conflict — yeah, that's just a rebase artifact, easy fix when we're ready to merge. Thanks again! |
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
- Add logging for media download failures (silent exception swallowing) - Fix temp directory leak by tracking and removing temp dirs after upload - Update pnpm-lock.yaml with Zulip extension dependencies Addresses CI failures and Greptile review comments.
…n file paths
OpenClaw core URL-encodes MessageThreadId for filesystem safety. The plugin
was passing raw topic names ('general chat') which became '-topic-general%20chat'
in session file paths.
Changes:
- Add sanitizeThreadId() to replace spaces and unsafe chars with hyphens
- Update resolveThreadSessionKeys() to return sanitizedThreadId
- Pass threadKeys.sanitizedThreadId instead of raw topic to MessageThreadId
Result: Session files now use clean names like 'general-chat' instead of 'general%20chat'
Zulip ANDs narrow terms together, so multiple ["stream", "x"] filters in one narrow match nothing (stream=A AND stream=B is always empty). For single-stream configs, keep the narrow filter for efficiency. For multiple streams or "*", switch to all_public_streams=true and filter client-side.
- Add zulip to MARKDOWN_CAPABLE_CHANNELS - Add mode? to ChannelPlugin queue defaults type - Add resolvePluginMode() to queue settings - Ensure plugin registry initialized in message command - Add resolveZulipSession() for default topic resolution
CI formatting check was failing after rebase. Auto-formatted 4 files to match project style.
- Use createScopedPairingAccess() for readAllowFromStore/upsertPairingRequest
(upstream now requires accountId in all pairing API calls)
- Use chatType ('direct'|'channel') not kind ('dm'|'channel') for peer.kind
in resolveAgentRoute() to match ChatType constraint
269121c to
b3b9f8c
Compare
21e5b75 to
b3b9f8c
Compare
ad93b68 to
0504c6e
Compare
|
Please make this as a third-party plugin that you maintain yourself in your own repo. Docs: https://docs.openclaw.ai/plugin. Feel free to open a PR after to add it to our community plugins page: https://docs.openclaw.ai/plugins/community |
|
Published as a standalone third-party plugin: https://github.com/FtlC-ian/openclaw-channel-zulip — install with |
|
Thank you for this |
Summary
Adds a full-featured Zulip channel plugin with concurrent message processing that significantly improves conversation flow compared to existing Zulip PRs.
Core Features
/user_uploads//api/v1/user_uploads[[zulip_topic: <topic>]]Security & SDK Compliance
fetchWithSsrFGuard()(no rawfetch())resolvePreferredOpenClawTmpDir()(noos.tmpdir())resolveDmGroupAccessWithLists()shared resolver (no manual pairing-store group auth)createScopedPairingAccess()for account-scoped pairing operationscheck-no-raw-channel-fetch,check-no-random-messaging-tmp,check-no-pairing-store-group-auth,check-pairing-account-scope,check-channel-agnostic-boundaries,check-no-raw-window-open)Key Improvement Over Existing PRs
The Problem
Existing Zulip PRs (#9643, #14182) process messages sequentially, causing:
The Solution
This PR implements concurrent message processing with staggered start times:
Verified Impact
Technical Details
Comparison with PR #22308
PR #22308 provides a minimal Zulip skeleton (~1,670 lines). This PR is the full-featured implementation (~3,500+ lines) with:
References
Testing