# Agent Control Plane and RSS Delivery — Zulip Handoff Canvas

> **Purpose:** Give an implementation agent enough context to continue the project without rereading the original conversation.
>
> **Status:** Product direction chosen; implementation not yet started.
>
> **Decision date:** 8 August 2026

---

## 1. Executive summary

The project began as a search for a better control plane for personal and autonomous AI agents such as **OpenClaw** and **Hermes Agent**, beyond the usual Telegram, Discord, and Slack interfaces. The required experience is conversation-native rather than dashboard-heavy: durable threaded conversations, granular notifications, predictable unread behavior, strong iOS support, and the ability to distinguish messages that have actually been read from messages merely opened in a channel.

The final platform choice is **self-hosted Zulip**. Its decisive advantage is the first-class **channel + topic** model. Topics are not optional side threads; they are the normal unit of conversation. This fits agent sessions, approval requests, background jobs, alerts, and RSS sources much better than a flat channel containing interleaved messages. Zulip also offers per-channel and followed-topic notification controls, topic muting/following, useful unread navigation, a capable API, bot accounts, and configurable message retention for a resource-constrained deployment.

Mattermost was the other serious contender. It is closer to Slack, has familiar channels and reply threads, and may have a shorter path for integrations that already target Slack/Mattermost-style systems. It was rejected for this project because its conversation and unread model is less aligned with the desired workflow: reply threads are secondary to the channel timeline, and it does not provide the same topic-centric inbox and catch-up experience as Zulip.

RSS delivery is also part of the control plane. Existing feeds currently publish through **MonitoRSS into Discord**. The target design is **Miniflux → lightweight relay → Zulip API**, with RSS categories mapped to Zulip channels and publications mapped to topics. Messages should use clean Zulip Markdown and native website previews. The visual goal is roughly 80–90% of a Telegram link preview and 55–65% of a rich Discord embed; exact Discord cards are not possible without modifying Zulip clients.

---

## 2. User goal and operating constraints

### Primary goal

Create a self-hosted, mobile-friendly conversational control plane where humans can:

- Interact natively with OpenClaw, Hermes Agent, and future agents.
- Keep separate agent tasks and discussions organized in threads/topics.
- Notice urgent approvals without being notified for every background update.
- Return after an absence and understand exactly what remains unread.
- Read and manage high-volume automated RSS output without drowning normal agent conversations.

### Important constraints

- The server will run on a **VPS with limited disk space**.
- Cleanup and bounded retention are therefore requirements, not optional housekeeping.
- The iOS app should have granular notifications and minimal idle battery impact.
- There is no desire to maintain a permanent fork of OpenClaw, Hermes, or Zulip.
- Many RSS subscriptions already exist in MonitoRSS and should be migrated rather than manually recreated.
- Message presentation matters, but conversation organization and unread correctness matter more than Discord-style visual polish.

---

## 3. Interaction model being sought

The desired system behaves more like an asynchronous work inbox than a group chat:

1. An agent starts or updates a bounded task.
2. Every task has a stable conversation location.
3. Routine progress is visible but quiet.
4. Approval requests, failures, and direct mentions can notify immediately.
5. Reading messages progressively reduces the unread state; opening a busy channel should not silently mark everything read.
6. RSS and background automation remain browseable without polluting important agent work.

Example conceptual organization:

```text
Channel: agents — openclaw
  Topic: deploy staging build
  Topic: weekly dependency audit
  Topic: investigate payment failure

Channel: agents — hermes
  Topic: research agent control planes
  Topic: summarize customer calls

Channel: approvals
  Topic: production database migration

Channel: RSS — AI
  Topic: MIT News
  Topic: Hacker News
  Topic: The Pragmatic Engineer
```

---

## 4. Platforms considered

The initial problem was broader than choosing between Zulip and Mattermost. Telegram, Discord, and Slack were the familiar baselines, but each had friction for an agent control plane.

| Platform | Useful qualities | Main mismatch for this project |
|---|---|---|
| Telegram | Excellent mobile app, efficient push delivery, simple bots, link previews | Conversation organization is comparatively flat; topics depend on group/forum setup; notification and work-inbox controls are weaker |
| Discord | Strong bot ecosystem, webhooks, rich embeds, threads, attractive automation messages | Designed around community chat; threads and unread semantics are not ideal for disciplined asynchronous task handling |
| Slack | Mature integrations, familiar workplace model, threads, granular notifications | SaaS dependence/cost and channel-first conversation model; self-hosting is not available |
| Mattermost | Self-hosted Slack-like experience, integrations, familiar mobile app | Threads remain attached to a flat channel timeline; weaker topic-first catch-up model |
| Zulip | Self-hosted, first-class topics, topic following/muting, structured unread workflow, capable API | Fewer ready-made agent integrations and less visual customization than Discord embeds |

Other possible products may exist, but no additional contender identified in the conversation displaced Zulip on the combined requirements of self-hosting, topic-native conversation, unread handling, and notifications.

---

## 5. Zulip versus Mattermost decision

### Why Zulip won

#### Topics are native, not an attachment

Every Zulip channel message belongs to a topic. This provides a durable address for an agent job, source, incident, or approval. Users can follow, mute, search, and navigate conversations by topic. Mattermost supports threaded replies, but the main channel remains a chronological stream, so unrelated top-level posts are easier to interleave.

#### Better high-volume catch-up model

Zulip exposes unread conversations and supports navigation to the next unread topic, next unread followed topic, and next unread direct-message conversation. Reading strategies are built around moving through individual conversations. Jumping to the bottom explicitly marks the current view read; merely having a channel open is not intended to erase the entire backlog.

The earlier comparison with Telegram/Discord/Mattermost focused on whether unread counts change while scrolling. The practical conclusion was:

- **Zulip:** closest to the desired progressive conversation-reading workflow. It tracks unread messages and conversations, and jumping to the bottom can explicitly mark the current view read.
- **Telegram:** commonly reduces unread state as messages are traversed/read, producing the expected progressive feeling.
- **Discord:** often behaves more conservatively around unread markers and channel state.
- **Mattermost:** should not be selected on the assumption that it exactly mirrors Slack or Telegram progressive unread behavior. Its channel/read-marker model is not the same as Zulip's topic-centric workflow.

Exact behavior can vary by client version and view. The implementation agent should run an iOS and desktop acceptance test before migrating fully.

#### Notification control matches agent work

Zulip can distinguish:

- Direct messages, mentions, and alert words.
- General channel messages.
- Messages in followed topics.
- Per-channel overrides.
- Muted channels and muted topics.
- Whether mobile notifications should still arrive while the user is online elsewhere.

This allows a useful policy: mute noisy agent and RSS channels by default, follow active task topics, and reserve push notifications for followed topics, mentions, approvals, and failures.

### What is lost by not choosing Mattermost

- A more familiar Slack-like interface for new users.
- Potentially easier reuse of Slack/Mattermost-compatible integrations.
- Richer attachment/card conventions in integrations that already support Mattermost.
- A broader workplace-chat ecosystem and more conventional reply-thread behavior.
- Potentially less custom adapter work for agents that already expose a Mattermost connector.

These losses were accepted because the topic and unread model is more important than connector convenience.

### iOS app and battery conclusion

Zulip appeared stronger for notification granularity because of followed topics and per-channel policies. Mattermost remains a mature iOS application, but its notification hierarchy is more channel/mention oriented.

No reliable controlled benchmark of **idle battery drain** was found in the conversation. Both should normally rely on Apple push notifications rather than continuous foreground polling. Actual drain will depend on app version, server/push configuration, notification volume, and whether background refresh is enabled. The handoff agent should not claim a battery winner without measuring both apps on the target iPhone over comparable 24-hour periods.

---

## 6. Agent integration: OpenClaw and Hermes Agent

### Current working conclusion

Neither OpenClaw nor Hermes Agent should be assumed to include a first-party Zulip connector. Their exact connector/plugin status can change and must be verified against the versions that will be deployed.

### Do their core source trees need to be modified?

Probably **not**. The preferred implementation is an external adapter or plugin using public extension points:

```text
Zulip events/API
      ↕
Zulip adapter/plugin
      ↕
OpenClaw or Hermes public message/session interface
```

Possible adapter behavior:

- Receive Zulip messages through an outgoing webhook bot or the real-time events API.
- Convert `channel + topic + sender + message` into an agent session/event.
- Preserve a mapping between a Zulip topic and an agent conversation/session ID.
- Post agent responses through a Zulip bot using `POST /api/v1/messages`.
- Add reactions or status messages for queued/running/waiting/failed states.
- Restrict commands by user, channel, and topic.
- Require explicit confirmation for privileged actions.

Only modify OpenClaw/Hermes core code if the selected release exposes no stable plugin, webhook, command, or transport interface. Even then, prefer a small upstreamable connector module over a private fork.

### Required verification before implementation

- Exact OpenClaw and Hermes versions.
- Available transport/plugin APIs.
- Existing Mattermost, Slack, Discord, or generic webhook adapters that can be reused.
- Authentication and authorization model.
- Whether conversation IDs can be supplied externally.
- Streaming/edit-message support and message-length behavior.
- Attachment upload/download behavior.

---

## 7. Self-hosting and retention

### Self-hosting

Zulip supports self-hosting. Production deployment requires normal operational care: HTTPS, email delivery, backups, upgrades, monitoring, and mobile push configuration. Self-hosted mobile notifications must be explicitly verified after setup.

### Message expiry requirement

The original question was whether Zulip supports Telegram-style disappearing messages such as “delete everything older than seven days.” The actual need is server storage control, not secrecy or ephemeral chat.

Use **server-side message retention** for noisy automation/RSS channels if the deployed Zulip edition/version supports the necessary scope. Do not model this as client-side disappearing messages. The implementation agent must confirm whether retention can be configured globally, per organization, or per channel in the chosen release and plan.

Suggested policy:

| Data | Suggested retention |
|---|---:|
| RSS channels | 7–30 days |
| Routine agent progress/logs | 30–90 days |
| Approval and audit conversations | Longer, based on risk/compliance needs |
| Uploaded RSS images | Avoid uploading where possible |
| Backups | Separate lifecycle policy; retention deletion does not automatically shrink every backup |

Storage planning must include PostgreSQL, search indexes, uploaded files, preview/cache data, logs, and backups—not merely visible message text. Database deletion may not immediately return filesystem space without normal PostgreSQL maintenance.

---

## 8. Message customization: what Zulip can and cannot do

The desired examples were a rich Discord RSS embed and a Telegram link-preview card. A realistic Zulip approximation was generated during the conversation and judged sufficient.

### Supported presentation

- Bot display name and fixed avatar.
- Bot indicator/badge.
- Bold or linked headline.
- Italic subtitle/byline.
- Short or long excerpts.
- Block quotes.
- Lists and tables.
- Inline or linked images.
- Native website previews produced from website metadata.
- A text link such as `Read the full article →`.
- Channel and topic organization around the message.
- Collapsible spoiler formatting as an optional “show more” mechanism.

### Not supported as native message controls

- Discord-style arbitrary embed objects.
- Per-message colored sidebars.
- Arbitrarily arranged structured fields.
- A guaranteed thumbnail fixed to the right of the text.
- Native styled action buttons such as Discord's `View` button.
- Per-message custom CSS or arbitrary HTML.
- Reliable per-message sender/avatar override using one bot credential.

Zulip messages use Zulip-flavored Markdown and server-controlled rendering. Website previews depend on Open Graph/site metadata and server settings. Poor metadata, anti-bot protections, or inaccessible images can produce incomplete previews.

### Bot identity strategy

Zulip bot identity is tied to its API credentials. To reproduce publisher branding exactly, create separate bots such as `MIT News` and `The Pragmatic Engineer`, each with its own avatar. This becomes cumbersome across many feeds.

Recommended compromise:

- One generic `RSS Feeds` bot for the long tail.
- Dedicated branded bots only for roughly 10–20 important publications.
- Always use the topic name and message footer to identify the publication.

---

## 9. RSS replacement for MonitoRSS

### Options considered

| Option | Advantages | Disadvantages | Decision |
|---|---|---|---|
| Zulip's official `rss-bot` | Very low overhead; no additional web UI/database; cron-based | Basic formatting and management; weaker for bulk feeds and custom routing | Good fallback/minimal deployment |
| Miniflux + relay | Web UI, feed health, categories, deduplication, OPML, webhooks, flexible Zulip formatting | Requires Miniflux, PostgreSQL, and a small relay | **Recommended** |
| n8n | Visual workflows, transformations, summaries, complex routing | Heavier in memory/operations; excessive for simple RSS forwarding | Optional future enhancement |
| Custom polling bridge | Lowest possible overhead and complete control | Must build feed management, polling, retries, deduplication, and UI/config | Use only if Miniflux is too heavy |

### Chosen architecture

```text
Existing RSS feeds
       ↓
    Miniflux
  polling, health,
  categories, dedupe
       ↓ webhook
RSS-to-Zulip relay
 clean HTML, select image,
 format Markdown, route,
 retry and dedupe
       ↓
   Zulip REST API
 channel + topic + bot
```

Miniflux should use its own PostgreSQL database. It may share the same PostgreSQL server with Zulip, but its tables should not be mixed into Zulip's database.

### Relay responsibilities

1. Validate the Miniflux webhook signature.
2. Accept `new_entries` events.
3. Extract feed name, category, article title, URL, author, publication date, summary/content, tags, and media/enclosures.
4. Sanitize feed HTML and convert a safe subset into Zulip Markdown.
5. Select a useful image from media/enclosure fields or page metadata.
6. Truncate excerpts to a configurable limit, initially around 300–600 characters.
7. Resolve category/feed rules into a Zulip channel, topic, and bot credential.
8. Post through the Zulip send-message API.
9. Store a stable article identity/hash to avoid duplicates during retries.
10. Log failures without retaining entire article bodies indefinitely.
11. Retry transient Zulip/network failures with bounded exponential backoff.
12. Offer a dry-run/preview mode before bulk migration.

Important caveat: Miniflux documentation states that its webhook delivery may not retry failed events. Therefore, the relay endpoint must acknowledge only after safely accepting/persisting the event, or a reconciliation job should periodically compare recent Miniflux entries with delivered IDs.

---

## 10. RSS channel/topic structure

### Recommended default: topic per publication

```text
Channel: RSS — Technology
  Topic: MIT News
  Topic: The Pragmatic Engineer
  Topic: Hacker News
```

This is best for passive consumption. Users can follow or mute an entire publication, unread counts remain understandable, and the number of topics stays bounded by the number of feeds.

### Alternative: topic per article

```text
Channel: RSS — Technology
  Topic: Alexander Rakhlin named director…
  Topic: Why is Meta destroying its engineering organization?
```

This is better when agents or humans will discuss each article in depth. It creates many short-lived topics and makes publication-level muting less convenient.

### Chosen initial structure

Use **topic per publication**. If a particular article needs analysis, start a dedicated topic in a research/agent channel and link back to the RSS message. Reconsider topic-per-article only for selected high-value feeds.

Suggested mappings:

| Miniflux category | Zulip channel | Topic |
|---|---|---|
| Artificial Intelligence | `RSS — AI` | Feed name |
| Technology | `RSS — Technology` | Feed name |
| Security | `RSS — Security` | Feed name |
| Business | `RSS — Business` | Feed name |
| Uncategorized | `RSS — General` | Feed name |

### Example target message

```markdown
## [Alexander Rakhlin named director of the MIT Statistics and Data Science Center](ARTICLE_URL)

*Institute for Data, Systems, and Society*

Alexander “Sasha” Rakhlin PhD '06 has been named the next director
of the MIT Statistics and Data Science Center…

> “Sasha is one of the sharpest theoretical minds working in
> statistics and machine learning today…”

[Read the full article →](ARTICLE_URL)

`MIT News · Artificial Intelligence · 8 April 2026`
```

Sending the article URL allows Zulip to attempt a native website preview. The exact card layout remains controlled by Zulip and the source site's metadata.

---

## 11. Proposed relay configuration

```yaml
defaults:
  bot: rss
  channel: "RSS — General"
  topic: feed_name
  excerpt_characters: 500
  include_author: true
  include_date: true
  include_image: auto
  website_preview: true

categories:
  Artificial Intelligence:
    channel: "RSS — AI"
  Technology:
    channel: "RSS — Technology"
  Security:
    channel: "RSS — Security"
  Business:
    channel: "RSS — Business"

feeds:
  MIT News - Artificial Intelligence:
    bot: mit-news
    topic: "MIT News"
  The Pragmatic Engineer:
    bot: pragmatic-engineer
    topic: "The Pragmatic Engineer"

retention:
  delivered_ids_days: 90
  failure_logs_days: 30
```

Secrets such as Zulip bot keys, Miniflux credentials, and webhook secrets must be injected through environment variables or a secret manager, not committed in this configuration.

---

## 12. MonitoRSS migration plan

There was no clearly documented one-click MonitoRSS-to-Zulip or MonitoRSS-to-OPML importer identified. Migration is nevertheless practical because the essential data is the list of feed URLs plus optional names/categories/formatting rules.

### Source extraction

- **Self-hosted MonitoRSS:** inspect its supported API and database schema, then export feed URLs and relevant routing/template fields. Avoid coupling the new relay directly to the old database.
- **Hosted MonitoRSS:** use any dashboard/API export available in the deployed plan. If none exists, obtain a structured list through the least fragile permitted method.
- Do not scrape or automate the dashboard until supported export/API paths have been checked.

### Transformation

Transform the extracted data into:

1. An OPML file for Miniflux subscriptions.
2. A mapping report containing old Discord destination, feed name, URL, category, filters, and custom formatting.
3. Relay configuration mapping categories/feeds to Zulip channels, topics, and optional branded bots.

Suggested intermediate CSV columns:

```text
feed_name,feed_url,category,old_discord_channel,old_template,
zulip_channel,zulip_topic,zulip_bot,enabled,notes
```

### Safe cutover

1. Export and back up MonitoRSS configuration.
2. Normalize and deduplicate feed URLs.
3. Import a small test OPML into Miniflux.
4. Run the relay in dry-run mode and inspect formatted messages.
5. Enable delivery into a private Zulip test channel.
6. Validate images, excerpts, Unicode, long titles, dates, duplicate handling, and broken feeds.
7. Import the remaining feeds in batches.
8. Run MonitoRSS and Zulip in parallel briefly, comparing delivery counts.
9. Disable MonitoRSS only after parity is acceptable.
10. Retain the export and migration report for rollback.

Historical Discord messages do not need to be migrated unless explicitly requested. The initial goal is subscription/configuration continuity, not replaying the entire old feed history.

---

## 13. VPS storage and cleanup design

### Principles

- Link to publisher-hosted images instead of uploading copies when feasible.
- Keep only delivery metadata in the relay, not full article bodies.
- Apply short retention to high-volume RSS channels.
- Rotate service logs.
- Back up configuration and databases, but apply an explicit backup lifecycle.
- Monitor free disk space and PostgreSQL growth.

### Data lifecycle

| Component | Data retained | Cleanup mechanism |
|---|---|---|
| Miniflux | Feeds, entries, status, metadata | Periodic removal of old read entries according to supported settings/jobs |
| Relay | Delivered IDs/hashes, retry state, minimal failure details | TTL purge, e.g. 90 days for delivered IDs |
| Zulip | Rendered RSS and agent messages | Server retention policy, ideally shorter for RSS channels |
| Reverse proxy/system services | Access/error logs | `logrotate` or equivalent |
| Backups | Database/config snapshots | Separate daily/weekly retention policy |

Do not estimate disk consumption solely from RSS text. Images uploaded to Zulip, full-text feed content, database indexes, search indexes, and backups are the likely growth drivers.

---

## 14. Implementation phases

### Phase 0 — verify assumptions

- Confirm target Zulip version and edition.
- Confirm retention granularity and mobile push setup for self-hosting.
- Test unread behavior on the actual iOS and desktop clients.
- Confirm OpenClaw/Hermes extension APIs and existing connectors.
- Confirm how MonitoRSS feed data can be exported.

### Phase 1 — Zulip pilot

- Deploy/update self-hosted Zulip with HTTPS, email, backups, and push notifications.
- Create agent, approval, and RSS channel taxonomy.
- Configure default notification policy.
- Test topic following/muting and unread catch-up on iOS.

### Phase 2 — basic agent adapter

- Connect one agent to one private Zulip channel.
- Map each topic to a stable agent session.
- Implement identity/authorization checks.
- Support text messages, errors, and approval requests before streaming or attachments.

### Phase 3 — RSS pilot

- Deploy Miniflux and its separate database.
- Deploy the relay with one generic RSS bot.
- Test five varied feeds, including feeds with missing images and malformed HTML.
- Confirm Markdown and previews on desktop and iOS.

### Phase 4 — MonitoRSS migration

- Export, normalize, convert to OPML, import in batches, and run parallel delivery.
- Add branded bots only after the generic flow is stable.

### Phase 5 — cleanup and hardening

- Enable retention and TTL jobs.
- Add health checks, reconciliation, metrics, rate limiting, and alerting.
- Document backup/restore and key rotation.

---

## 15. Acceptance criteria

### Agent control plane

- A Zulip topic maps deterministically to one agent conversation/session.
- A new agent reply appears in the correct channel/topic.
- Unauthorized users cannot execute agent commands.
- Approval-required actions are visually and operationally distinct.
- Muted background channels do not generate unwanted push notifications.
- Followed task topics generate the intended mobile notifications.
- Reading one topic does not unexpectedly clear unrelated unread conversations.

### RSS

- Existing feed URLs can be imported without manual re-entry.
- Each article is delivered at most once under normal retries/restarts.
- Category routing and feed topics match configuration.
- Messages remain readable when images or website previews fail.
- Long/HTML-heavy descriptions are safely cleaned and truncated.
- The result is usable in both desktop and iOS Zulip clients.
- Old messages, entries, relay IDs, logs, and backups are cleaned according to policy.

### Operations

- Disk alerts trigger before the VPS is critically full.
- Database and configuration restore has been tested.
- Bot/API credentials are not stored in source control.
- MonitoRSS can remain active until delivery parity is confirmed.

---

## 16. Risks and unresolved questions

1. **Agent connector availability:** Recheck current OpenClaw/Hermes releases; prior conclusions may age quickly.
2. **Unread semantics:** Perform a hands-on iOS test with a deliberately long unread topic and multiple channels.
3. **Battery drain:** No evidence-backed winner was established; measure on the target device.
4. **Retention scope:** Confirm exact global/per-channel controls in the deployed Zulip edition and version.
5. **Self-hosted push:** Verify Zulip's supported push-notification configuration and any service/plan dependencies.
6. **MonitoRSS export:** Establish an official/API/database extraction path and determine which custom rules can be translated.
7. **Webhook loss:** Design around Miniflux webhook non-retry behavior through durable intake or reconciliation.
8. **Website previews:** Some sources will have missing/broken Open Graph metadata or block preview fetching.
9. **Publisher-specific bots:** Decide the acceptable operational limit before creating many credentials.
10. **Rate limits and bursts:** Batch-heavy feeds can create bursts; relay must queue and pace Zulip API calls.
11. **Security:** Agent commands require strict allowlists and explicit approval gates for destructive or privileged actions.

---

## 17. Key references

### Zulip

- [RSS integration](https://zulip.com/integrations/rss)
- [Send a message API](https://zulip.com/api/send-message)
- [Message formatting](https://zulip.com/help/format-your-message-using-markdown)
- [Website, image, video and audio previews](https://zulip.com/help/image-video-and-website-previews)
- [Mobile notifications](https://zulip.com/help/mobile-notifications)
- [Channel notifications](https://zulip.com/help/channel-notifications)
- [Topic notifications](https://zulip.com/help/topic-notifications)
- [Follow a topic](https://zulip.com/help/follow-a-topic)
- [Reading strategies](https://zulip.com/help/reading-strategies)
- [List of topics in a channel](https://zulip.com/help/list-of-topics)

### Miniflux

- [Webhook documentation](https://miniflux.app/docs/webhooks.html)
- [API, including OPML import/export](https://miniflux.app/docs/api.html)
- [Main documentation](https://miniflux.app/docs/)

### MonitoRSS

- [MonitoRSS website](https://monitorss.xyz/)
- [MonitoRSS source repository](https://github.com/synzen/monitorss)

---

## 18. Concise handoff directive for the next agent

> Continue this project as an implementation and validation task. Preserve the decision to use self-hosted Zulip unless testing reveals a blocking failure. First verify the target Zulip retention/push behavior, current OpenClaw and Hermes extension interfaces, and the available MonitoRSS export path. Then prototype two thin components: (1) a Zulip adapter that maps channel/topic conversations to stable agent sessions without modifying agent core code, and (2) a durable Miniflux-to-Zulip relay that formats safe Markdown, routes category → channel and feed → topic, handles images/previews gracefully, deduplicates deliveries, and supports a dry-run. Optimize for a limited-disk VPS, iOS notification control, predictable unread behavior, secure approvals, and reversible migration.
