OpenClaw v2026.8.1 upgrade review for the Zulip connector
Assessment date: 2026-08-31
Decision: NO-GO until the P0 compatibility work and staging gates in this document pass
Current OpenClaw runtime: 2026.7.1
Target OpenClaw runtime: 2026.8.1
Current connector source/artifact: @yachint/openclaw-zulip 0.5.1
Connector source: /root/openclaw/plugins/openclaw-zulip
Zulip deployment repository: /root/zulip
Zulip Server: patched 12.1 image, deliberately outside the OpenClaw upgrade scope
Executive decision
The current Zulip connector is well designed around the deployment's important invariants: numeric owner authorization, one-to-one owner DMs, explicit channel allowlisting, mention gating, topic-scoped sessions, durable inbound deduplication, receipt-gated sends, bounded media handling, and private native approvals. The live 0.5.1 artifact has strong historical acceptance evidence on OpenClaw 2026.7.1.
It is not compatible with OpenClaw 2026.8.1 as it stands. This is not merely a conservative version-range mismatch. The source has several concrete 8.1 API/type breaks:
- package metadata explicitly rejects every 2026.8.x host;
- two source files import the removed
openclaw/plugin-sdk/channel-runtimesubpath; - inbound media cleanup imports
deleteMediaBuffer, which 8.1 no longer exports publicly; - the durable claim callback moved from top-level
onTurnAdoptedtoturnAdoptionLifecycle.onAdopted; openclaw/plugin-sdk/outbound-mediahas runtime JavaScript in the 8.1 npm artifact but no public declaration entry, making the current strict TypeScript import unsafe;- the plugin still emits legacy inbound media projection fields and manually supplies session-store wiring that 8.1 now deprecates; and
- static discovery metadata still uses the retired
channelEnvVarsshape and does not advertise its native approval capability throughapprovalFlags.
The correct path is a new connector compatibility line—recommended package version 0.6.0—built and tested specifically against OpenClaw 2026.8.1. The implementation should preserve the existing external-plugin SQLite journal for this upgrade. OpenClaw's host-managed ingress queue is attractive, but in 8.1 its storage-opening runtime is restricted to bundled or trusted official plugins; this out-of-tree connector cannot safely depend on it without a separate trust/distribution decision.
There is no evidence that Zulip Server itself must be upgraded. Keep the patched Zulip 12.1 image and APNs badge patch unchanged. Upgrade OpenClaw and the connector as one coordinated change, then re-run the connector's full automated and physical acceptance matrix.
Scope and evidence
The requested path ~/zulip is the Zulip server/deployment repository, not the TypeScript connector. The connector source lives at /root/openclaw/plugins/openclaw-zulip. Both were reviewed because the OpenClaw-side upgrade must preserve the server-side mobile notification contract.
The review covered:
- every connector source module, manifest, package file, TypeScript configuration, README, and test file;
- the installed/source compatibility metadata and the live deployment's non-secret channel/plugin configuration;
- the official
openclaw@2026.8.1npm artifact, including its exported subpaths and declaration files; - the official 2026.8.1 release notes and Plugin SDK migration/channel runtime documentation;
- the Docker Compose image strategy and Node runtime prerequisite;
/root/zulipoperational rules, immutable image lock, APNs badge patch contract, and integration documentation; and- current Git/worktree state, so unrelated in-progress RSS work is not confused with connector changes.
This was a read-only compatibility audit except for this report and the mandated task ledger. In accordance with the repository rules, no dependency installation, typecheck, build, test, package creation, Docker recreation, live API call, database mutation, or deployment was performed. Historical test/production results quoted here are evidence already recorded in the repository; they were not rerun during this assessment.
What OpenClaw 2026.8.1 changes that matters here
OpenClaw 2026.8.1, also described as OpenClaw 2.0, adds conversation search, portable/cloud sessions, durable progress views, structured questions, interactive widgets, protected credential requests, recurring automation approvals, and richer media continuity. Those user-facing additions are not direct connector blockers.
The relevant upgrade changes are lower-level:
- the Plugin SDK continues its move to small, focused public subpaths and removes the old
channel-runtimebarrel; - durable inbound ownership now uses the canonical
turnAdoptionLifecyclecontract; - routed channel-turn helpers can own session-store resolution and inbound-session recording;
- ordered media facts replace legacy parallel
MediaPath/MediaPaths-style projections; - native approval behavior is represented as capability facts and should also be declared in package discovery metadata;
- configured plugin discovery uses package-level
configuredStatemetadata rather than the old top-levelchannelEnvVarsmanifest field; - OpenClaw's core durable ingress machinery now owns bounded retry/dead-letter behavior for eligible plugins; and
- the host Node engine range is
>=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0.
The current container's Node 24.16.0 satisfies that engine range. CPU, memory, and disk inspection did not identify a resource blocker. Compatibility, packaging, and state migration—not machine capacity—are the gating concerns.
OpenClaw 8.1 also contains breaking OpenAI route and OpenProse migrations. The active configuration already uses canonical openai/* model routes and Codex runtime intent, which reduces risk, but copied sessions, tasks, cron records, and other persisted state must still be scanned and passed through staged doctor --fix. A clean current config does not prove every historical session or automation row is clean.
Current connector architecture
The connector is an external TypeScript ESM channel plugin with no patch to either OpenClaw core or Zulip core.
Inbound path
- The connector registers a Zulip long-poll event queue.
- It separately backfills owner DMs and explicitly configured channels after startup or queue expiry.
- It classifies only one-to-one DMs or allowed stream/topic messages.
- Authorization is based on stable numeric Zulip identities. Channel messages are mention-gated unless the sender is authorized and the content is an accepted control command.
- Accepted messages are durably inserted into
plugins/zulip/inbound.sqlitebefore agent dispatch. - The worker claims the oldest pending row, downloads and stores eligible attachments, constructs an OpenClaw inbound context, and runs the channel turn.
- The connector tombstones the journal row when OpenClaw adopts the run. OpenClaw then owns post-adoption recovery and outbound delivery.
- A pre-adoption failure releases the row for retry. On process restart, claimed rows are returned to pending.
Outbound path
- Text uses Markdown-aware chunking with a 9,500-character per-message limit.
- Every chunk's Zulip message ID is represented in the OpenClaw delivery receipt.
- Media is loaded through OpenClaw's outbound media policy, uploaded to Zulip, and delivered as a Zulip upload link.
- Durable final text/media delivery is routed through the channel message adapter.
Control plane
- Command ownership and elevated authorization resolve to the same stable numeric owner identities.
- Native exec/plugin approvals are delivered only through private owner DMs.
- Public/channel fallback prompts are suppressed so a failed private route cannot leak an approval ID or command into a topic.
- The connector registers its account-scoped Zulip sender in OpenClaw's task-scoped channel runtime context.
Server boundary
The /root/zulip repository owns a patched Zulip Server 12.1 image. Its APNs behavior is intentionally nonstandard and accepted on physical iOS devices: new unread messages increment the badge, foregrounding without reading does not clear it, and reading sends a badge-only clear. Nothing in OpenClaw 8.1 requires changing that server image.
Compatibility matrix
| Area | Current 0.5.1 state | OpenClaw 8.1 state | Decision |
|---|---|---|---|
| Host compatibility metadata | >=2026.7.1 <2026.8.0 |
Host is 2026.8.1 | P0 blocker |
| Peer/dev SDK | Exactly 2026.7.1 | Must compile against 2026.8.1 | P0 blocker |
plugin-sdk/channel-runtime |
Imported for approval/authorization types | Subpath removed | P0 blocker |
| Turn adoption | Top-level onTurnAdopted |
turnAdoptionLifecycle.onAdopted |
P0 blocker |
| Managed media deletion | Public deleteMediaBuffer import |
No longer exported | P0 blocker |
| Outbound media loader typing | Typed subpath in 7.1 | JS export exists, declaration export absent | Likely P0 typecheck blocker |
| Inbound media context | Legacy projection builder | Ordered media facts preferred |
P1 migration |
| Session wiring | Plugin supplies path/record function | Core-routed turn helpers preferred | P1 migration |
| Native approval metadata | Runtime capability only | Package approvalFlags: ["native"] available |
P1 metadata fix |
| Environment discovery | channelEnvVars in plugin manifest |
Use package configuredState.env |
P1 metadata fix |
| Durable ingress store | Plugin SQLite | Core queue restricted by plugin trust | Preserve for 0.6.0 |
| Retry disposition | Unlimited release/retry | Core model is bounded/dead-lettered | P1 reliability fix |
| Zulip Server | Patched 12.1 | No required API change found | Keep unchanged |
| Node runtime | Container 24.16.0 | 24.15.0–24.x supported | Pass |
Required P0 connector changes
P0-1: create an explicit OpenClaw 8.1 package line
The current package cannot be discovered as compatible by an 8.1 host. Update all related declarations together; changing only one field creates a misleading artifact.
Recommended package line:
{
"version": "0.6.0",
"openclaw": {
"install": {
"minHostVersion": ">=2026.8.1"
},
"compat": {
"pluginApi": ">=2026.8.1 <2026.9.0",
"minGatewayVersion": ">=2026.8.1 <2026.9.0"
},
"build": {
"openclawVersion": "2026.8.1",
"pluginSdkVersion": "2026.8.1"
}
},
"peerDependencies": {
"openclaw": "2026.8.1"
},
"devDependencies": {
"openclaw": "2026.8.1"
}
}
The upper compatibility bound is recommended because this plugin deliberately follows OpenClaw's release-specific channel contracts. It prevents a future 2026.9 host from silently loading an artifact that has never been validated against that SDK.
Regenerate package-lock.json with the exact 8.1 package. Do not hand-edit its version strings. Record the resulting OpenClaw integrity and the final connector tarball's size, entry count, SHA-256, npm shasum, and npm integrity.
P0-2: remove plugin-sdk/channel-runtime imports
Affected source:
src/authorization.tsimportsChannelCommandAdapterandChannelElevatedAdapter.src/approvals.tsimportsChannelApprovalCapability,ChannelApprovalNativeAdapter, andChannelActionAvailabilityState.
Use public 8.1 contracts without deep-importing hashed/internal declaration files:
- import
ChannelApprovalCapabilityfromopenclaw/plugin-sdk/channel-contract; - derive the native adapter from
NonNullable<ChannelApprovalCapability["native"]>; - derive action state from the public capability callback return type or define a minimal plugin-local structural union;
- derive commands and elevated adapters from
NonNullable<ChannelPlugin["commands"]>andNonNullable<ChannelPlugin["elevated"]>, or use equivalently narrow local structural types.
The connector already correctly uses channel-runtime-context for registering the live Zulip approval sender. Keep that implementation. The replacement is about the removed type barrel, not about redesigning the runtime-context registry.
P0-3: migrate durable turn adoption
Current code passes a top-level callback:
onTurnAdopted: () => {
adopted = true;
store.complete(claim);
}
OpenClaw 8.1's RunChannelTurnParams no longer has that property. Use the canonical lifecycle:
turnAdoptionLifecycle: {
admission: "exclusive",
onAdopted: () => {
adopted = true;
store.complete(claim);
},
}
exclusive matches the connector's durable, one-claim ownership model. Completion must remain adoption-gated; do not move it to onSettled. A turn that OpenClaw has not adopted must remain recoverable. Review and test onAbandoned, cancellation, deferral, and abort behavior explicitly so every pre-adoption exit either releases or preserves the claim and no post-adoption exit requeues it.
This is not only an excess-property compile error. In 8.1 the runtime forwards turnAdoptionLifecycle; a prebuilt JavaScript artifact's obsolete top-level callback is not a supported ownership signal.
P0-4: replace removed managed-media deletion API
src/inbound.ts records saved media IDs and calls deleteMediaBuffer(id, "inbound") if a later attachment fails. OpenClaw 8.1 still has that function internally, but it no longer exports it from plugin-sdk/media-runtime.
Do not deep-import the internal media-store chunk. Instead:
- retain each successful save's managed path as well as its ID;
- use a supported public cleanup path, with
unlinkIfExists(saved.path)frommedia-runtimeas the immediately available 8.1 option; - keep cleanup best-effort with
Promise.allSettled; - preserve the injectable cleanup seam for deterministic tests; and
- assert that cleanup is only attempted on the exact managed paths returned by OpenClaw.
Before accepting this change, test a two-attachment message where the first save succeeds and the second download or save fails. The first file must disappear, the journal decision must remain correct, and no path derived from Zulip input may be unlinked.
P0-5: resolve the untyped outbound-media export
The 8.1 npm artifact exports runtime JavaScript for openclaw/plugin-sdk/outbound-media, but its package export has no types target and there is no matching .d.ts. With this connector's strict TypeScript settings, the current direct import of loadOutboundMediaFromUrl is likely to fail the release typecheck.
The same function is declared through the public channel-core declaration surface in the inspected 8.1 artifact. The least invasive upgrade is therefore to import it from the already-used typed openclaw/plugin-sdk/channel-core surface, while opening an upstream packaging issue if a focused outbound-media declaration was intended. Do not add a broad declare module "openclaw/plugin-sdk/outbound-media" shim that could conceal future signature changes.
The existing call must continue passing maxBytes, mediaAccess, mediaLocalRoots, and mediaReadFile. These are security boundaries, not optional convenience arguments.
Required P1 SDK and metadata migration
P1-1: move to facts-first inbound media
The connector currently spreads buildChannelInboundMediaPayload(toInboundMediaFacts(...)) into extra. That recreates legacy singular/plural fields such as MediaPath, MediaPaths, and MediaTypes.
In 8.1, pass the ordered facts directly:
const media = toInboundMediaFacts(inboundMedia, {
messageId: String(message.id),
});
const ctxPayload = runtime.channel.inbound.buildContext({
// existing identity, route, reply, and message facts
media,
extra: {
CommandAuthorized: inbound.commandAuthorized,
},
});
The legacy media projection has an earliest removal review date of 2026-10-01, subject to the documented reader sweep. It is not the immediate 8.1 blocker, but publishing a new 0.6.0 artifact while retaining it would create near-term debt.
Tests must verify ordering, content type, message ID, multiple attachments, and the absence of legacy projection fields.
P1-2: let routed turn helpers own session wiring
The current resolveTurn result supplies:
agentId;routeSessionKey;storePathresolved byruntime.channel.session.resolveStorePath;recordInboundSession; and- the buffered reply dispatcher.
OpenClaw 8.1 deprecates manual resolveStorePath and recordInboundSession for channel turns. Its routed turn shape accepts a canonical route and keeps session recording in core. Migrate toward:
route: {
agentId: route.agentId,
sessionKey: route.sessionKey,
}
and retain only the fields required by the routed delivery plan.
This should be implemented in the same 0.6.0 work if practical, but only after checking the exact 8.1 ChannelTurnPlan declaration during typecheck. The critical behavioral gate is unchanged: DMs retain their canonical session, separate topics in the same Zulip channel remain isolated, replies return to the originating topic, and restart recovery never duplicates a reply.
P1-3: correct static discovery metadata
Remove the obsolete top-level channelEnvVars field from openclaw.plugin.json and move the cheap environment hint into the package channel metadata:
"configuredState": {
"env": {
"allOf": ["ZULIP_BOT_API_KEY"]
}
}
This is only a discovery hint for the default account. It must not replace runtime validation of baseUrl, botEmail, SecretRefs, enabled state, or named-account credentials.
Also declare:
"approvalFlags": ["native"]
under openclaw.channel. The runtime already exposes a native approval capability; the static package metadata should tell pre-runtime discovery/planning the same truth.
Update the manifest/schema alignment test to cover removal of channelEnvVars, the new configured-state hint, and the approval flag.
P1 reliability and security repairs strongly recommended before promotion
These issues are not all introduced by 8.1, but the upgrade is the right point to close them because several touch the same inbound lifecycle.
R1: add bounded retry and a real failed/dead-letter state
The SQLite schema allows failed, but connector code never transitions a row into it. release() always changes a failed claim back to pending; the worker then claims the oldest pending event again. Backoff is capped at 60 seconds but attempts are unlimited.
Impact:
- a permanently malformed or consistently failing event can retry forever;
- because
claimNext()always chooses the oldest pending row, one poison event can head-of-line block every later message for that account; - repeated pre-adoption failures can generate unbounded model/API work and noisy logs; and
- operators have no explicit retry-exhausted state to inspect or repair.
Implement an explicit policy, for example:
- distinguish retryable transport/provider failures from permanent validation/payload failures;
- cap delivery attempts for retryable failures;
- transition exhausted/permanent failures to
failedwith bounded error text and a tombstoned or redacted payload policy; - expose failed count, oldest failed age, and latest reason in status/diagnostics;
- allow later rows to continue after a failed poison event; and
- provide a narrowly scoped, documented operator requeue procedure rather than editing SQLite manually.
Do not confuse this connector journal policy with curator source acknowledgement. A failed Zulip curator delivery must still leave the curator source batch pending and unacknowledged. The journal can mark an inbound user event failed without acknowledging an unrelated outbound curator source.
R2: back off repeated BAD_EVENT_QUEUE_ID
The poll loop immediately sets the queue to null and continues after BAD_EVENT_QUEUE_ID. A single expired queue is normal, but repeated registration rejection can become a tight register/fail loop.
Apply jittered bounded backoff before re-registration after consecutive bad-queue failures, reset the counter only after a successful registration/poll cycle, and surface reconnect attempts in status. Retain immediate durable backfill after a successful new registration.
R3: validate all event and message IDs before cursor math
Queue registration validates its initial last_event_id, but getEvents() returns decoded objects without validating each event ID. The poll loop uses Math.max(lastEventId, event.id). A malformed value can turn the cursor into NaN and poison subsequent long-poll requests.
Require positive safe integers for queue event IDs and message IDs at the boundary. Reject or ignore malformed events without advancing any cursor. Add tests for NaN, floats, negatives, numeric strings, values beyond the safe integer range, and missing IDs.
R4: close stream bot-loop prevention
senderIsBot() searches display_recipient for the sender's is_bot flag. That works for private-recipient arrays. Zulip stream messages use a string display recipient, so the function cannot identify another bot posting in a channel.
The connector separately rejects its own numeric bot ID, so self-loop protection exists. The incomplete case is other bots in an allowed/open channel. Resolve bot identity from an explicit Zulip sender fact or a bounded/cached server identity lookup and reject bot-authored stream events before journal admission. Test both the connector's own bot and a distinct Generic bot.
R5: sanitize decoded inbound filenames
The upload downloader extracts the final URL segment and then applies decodeURIComponent. The resulting string is forwarded as originalFilename to the managed media store.
Normalize or reject:
- decoded
/and\\separators; ./..path components;- NUL and C0/C1 control characters;
- bidi/control characters that make logs or UI misleading;
- path-like drive prefixes; and
- unreasonably long names.
Keep the filename informational. The managed media ID/path must remain generated by OpenClaw, never selected by the Zulip filename. Cover %2F, %5C, %2E%2E, %00, mixed Unicode normalization, and malformed percent encoding.
R6: bound JSON API responses
Attachment bodies are streamed under a hard byte limit, but ordinary Zulip API responses call response.json() directly. A compromised/misconfigured endpoint could return an oversized success or error body and consume excessive memory.
Use a shared bounded JSON reader with separate conservative limits for ordinary API responses and long-poll event responses. Preserve timeout/abort behavior, reject malformed JSON deterministically, and never include raw response bodies or Authorization headers in errors.
R7: make multi-account approvals unambiguous
requestMatchesZulipAccount() currently accepts a request with no sourceAccountId for any account that evaluates it. In a multi-account deployment, this can produce duplicate owner prompts or select the wrong account.
Require the source account for Zulip-originated approval requests, or define a single deterministic default-account fallback and guarantee only that account handles an account-less request. Add named-account tests for capability availability, runtime-context lookup, target resolution, dedupe keys, pending delivery, resolution, expiry, and failure.
The delivery-level hasConfiguredDmRoute currently checks the default account. Confirm the 8.1 planner always provides the intended account or make the account dependency explicit.
R8: eliminate secret-bearing config retention
ResolvedZulipAccount contains the resolved apiKey (required for the client) and also retains the merged raw config, which may independently contain a plaintext/materialized key. The latter is not needed by most consumers and increases accidental serialization risk.
Retain only non-secret fields or a redacted config snapshot. Add tests that JSON serialization of account inspection/status objects cannot reveal the API key or Basic Authorization value. Continue using SecretRefs in tracked configuration.
R9: define the base-URL path policy
normalizeZulipBaseUrl() accepts an HTTPS URL with a non-root path. API calls append /api/v1/..., while upload validation requires a root /user_uploads/ path. Path-prefixed deployments therefore have ambiguous behavior.
Choose and document one contract:
- reject any non-root base path; or
- correctly compose the configured prefix for API and upload paths while still enforcing exact origin and prefix boundaries.
For the current https://chat.yachint.com deployment, rejecting non-root paths is the simpler and safer policy.
R10: reject unsafe topic controls
Targets percent-decode the topic and allow arbitrary characters. Reject NUL, CR/LF, and other unsafe controls before using the value in API form fields, session keys, status, or logs. Preserve Zulip's valid empty topic and normal Unicode topics.
R11: propagate cancellation through in-flight operations
The monitor's outer loops receive an abort signal, but typing calls, inbound attachment downloads, some sends, and media uploads often rely only on their own timeout. Pass the account lifecycle signal through every supported network/media operation so gateway shutdown does not wait for avoidable timeouts. Verify no partial upload or claim ownership is left ambiguous.
R12: account for WAL/SHM and mount behavior
The journal enables SQLite WAL with synchronous=FULL. Preserve inbound.sqlite, inbound.sqlite-wal, and inbound.sqlite-shm as a consistent unit when backing up or staging. Never copy only the main database while the gateway is writing.
OpenClaw 8.1 includes filesystem-sensitive SQLite improvements. Validate the connector journal on the actual production mount type. Do not replace WAL or permissions casually: the current mode 0600, directory 0700, and UID/GID 1000:1000 are part of the accepted recovery contract.
Lower-priority documentation and maintainability work
- Update the connector README from its 2026.7.1/npm-11-specific installation notes after testing the exact Node/npm pair in the 8.1 image.
- Record 0.6.0 artifact provenance and keep the 0.5.1/0.5.0/0.4.2 archives for rollback.
- Mark
/root/zulip/docs/openclaw-zulip-integration-plan.mdsections that say Discord is enabled as historical. The current baseline is Zulip-only; Discord, Telegram, and WhatsApp must remain absent or disabled. - Replace the old instruction to freeze OpenClaw at 2026.7.1 with the accepted 8.1 baseline only after production acceptance.
- Document the durable journal's failure/requeue model once bounded retry is implemented.
- Add a short compatibility table mapping connector releases to supported OpenClaw release ranges.
What must not change
The migration is acceptable only if all of these invariants remain true:
- Zulip is the sole active messaging channel.
- Telegram, WhatsApp, and Discord do not regain tokens, allowlists, listeners, destinations, approval routes, or elevated trust.
- Stable numeric Zulip user ID
8remains the owner identity. - Owner DMs remain one-to-one and allowlisted.
- Channel
4remains the only inbound allowlist destination. - Channel messages remain mention-gated except authorized control commands.
- Channel
5remains the curator destination with topicstwitterandhacker-news. - Critical alerts and the six-hour heartbeat remain private owner DMs to
user:8. - Approval prompts, commands, IDs, resolutions, failures, and expiry notices remain private.
- Separate Zulip topics remain separate canonical OpenClaw sessions.
- Replies, typing indicators, and generated media return to the originating DM/topic.
- Duplicate delivery and restart recovery never create duplicate agent replies.
- Journal payloads are erased/tombstoned after completion and the database remains private.
- Failed curator Zulip delivery leaves the source batch pending and does not acknowledge it.
- Inbound media keeps the 20 MiB per-file and 10-attachment deployment limits, managed-store persistence, exact-origin download restriction, and partial-save cleanup.
- OpenClaw's image-processing 10 MiB and audio-processing 20 MiB model limits remain understood separately from transport storage limits.
- Outbound text remains Markdown-aware and below Zulip's silent 10,000-character truncation boundary.
- Every logical multi-part send returns every concrete Zulip message ID in its receipt.
- API keys remain in the untracked environment/SecretRef path and never enter tracked JSON, logs, errors, diagnostics, or package artifacts.
- The patched Zulip Server 12.1 APNs behavior remains intact.
Automated verification plan
Run these only in an isolated plugin/staging environment after implementation authorization. Do not run them against the live mounted gateway as a shortcut.
Source and dependency gate
- Install dependencies from the regenerated lockfile with scripts disabled.
- Confirm
node_modules/openclaw/package.jsonis exactly 2026.8.1. - Verify no source or test imports a removed, internal, or undeclared SDK path.
- Run strict TypeScript typecheck.
- Run the complete mocked suite.
- Run the production build from a clean
distdirectory. - Run production-only dependency audit and require zero known runtime vulnerabilities.
New focused tests
Add explicit coverage for:
turnAdoptionLifecycleadoption, deferral, abandonment, cancellation, and pre-/post-adoption failure;- facts-first ordered media without legacy projection fields;
- partial managed-media cleanup through the new public API;
- routed session wiring and topic isolation;
- failed/dead-letter transition and continued processing after a poison event;
- retryable versus permanent failure classification;
- repeated
BAD_EVENT_QUEUE_IDbackoff and recovery; - malformed queue event/message IDs;
- another bot posting in an allowed stream;
- decoded filename traversal/control characters;
- bounded JSON success, error, and long-poll bodies;
- named-account approval selection and missing-source-account behavior;
- removal of secrets from serialized resolved account/status data;
- root versus path-prefixed base URLs;
- unsafe topic controls while retaining empty and Unicode topics; and
- shutdown during typing, download, upload, and send.
Existing regression matrix to retain
- allowed owner DM and rejected unauthorized/spoofed DM;
- self-message and bot-message loop prevention;
- explicit channel allowlist, per-channel sender allowlist, and mention gate;
- no-mention authorized control command;
- private refusal/reroute for channel approval commands;
- separate sessions for two topics in one channel, including the empty topic;
- duplicate event suppression;
- claimed-row process restart and exact-once recovery;
- DM and channel backfill after queue expiry;
- authenticated exact-origin inbound photo, audio, PDF, and multiple attachments;
- per-file and attachment-count rejection without an agent run;
- cleanup when a later attachment fails;
- outbound image, audio, PDF, and general file delivery;
- short text and greater-than-20,000-character ordered chunking;
- concrete receipts for every chunk/media send;
- approval pending, allow-once, deny, repeated resolution, expiry, transport failure, and public leak prevention;
- static/runtime schema alignment; and
- status snapshots that distinguish REST probe success from a running event monitor.
Package gate
- Run
npm pack --jsonusing the exact npm version shipped in the target 8.1 image. - Confirm
prepackrebuiltdistfrom the reviewed source. - Inspect the archive: only expected
dist, manifest, README, and package metadata may ship. - Verify package version/compatibility/build metadata, entrypoints, setup entry, configured state, approval flags, and peer dependency.
- Record size, entry count, SHA-256, npm shasum, and integrity.
- Install the immutable archive through OpenClaw's
npm-pack:lifecycle in a disposable 8.1 state directory. - Require plugin discovery, config validation, plugin inspection, plugin doctor, and SecretRef audit to pass.
- Prove no credential or live-state file exists in the archive.
Staging and deployment plan
Stage 0: select immutable host artifacts
The Compose file references openclaw:local for gateway, installer, and CLI, but this repository does not currently contain the Dockerfile described in its comments. Before any upgrade, define a reproducible 8.1 image source and record its immutable digest. Gateway, installer, and CLI must use the same OpenClaw build.
The official Codex and Perplexity plugin packages must be aligned with the 8.1 host as part of staging. Verify their exact versions, integrity, and architecture-specific Codex binary. Keep the trusted plugin allowlist intact; do not accidentally remove the Zulip, media, provider, browser, memory, or control-plane plugins it protects.
Stage 1: consistent backup
With an authorized quiesce procedure, capture:
- tracked and live-mounted
openclaw.jsonplus last-known-good copy; - session/state databases and sidecars;
- task and cron state;
- delivery queues;
plugins/zulip/inbound.sqliteplus WAL/SHM;- currently installed plugin package projects and immutable archives;
- Compose configuration and current image digest; and
- curator prompts and source acknowledgement state.
Verify backup readability. A rollback after an 8.1 schema migration must restore this pre-upgrade state; reverting only the container image is insufficient.
Stage 2: copied-state migration
On a disposable copy:
- start the exact target 8.1 image with the 0.6.0 connector and aligned official packages;
- run config validation and non-mutating inspection first;
- run
doctor --fixonly on copied state; - review every proposed/applied migration, particularly model routes, OpenProse cleanup, plugin metadata, sessions, tasks, and automations;
- verify Zulip remains the sole configured/connected channel; and
- run the full package and functional gates.
Stage 3: authorized production promotion
Production promotion should be one controlled maintenance event. Install only the reviewed immutable 0.6.0 artifact, update all OpenClaw services to the same pinned 8.1 image, validate configuration, recreate the gateway once, and collect bounded startup/status evidence.
Expected evidence:
- gateway, installer, and LiteLLM healthy;
- exact OpenClaw 2026.8.1 version and image digest;
- exact Zulip connector 0.6.0 version/hash/integrity;
- no compatibility, plugin allowlist, removed SDK, or config-overwrite warnings;
- Zulip Generic bot probe succeeds against Server 12.1;
- Zulip event monitor starts;
- no Discord, Telegram, or WhatsApp listener starts;
- cron and task registry list correctly; and
- journal integrity, ownership, permissions, cursor, and state counts are sane.
Stage 4: live logical acceptance
Use new canary messages and record their IDs:
- owner DM produces exactly one reply;
- unauthorized DM produces no run/reply;
- allowlisted unmentioned channel message is ignored;
- mentioned owner message in channel
4produces one reply in the same topic; - authorized
/whoamiworks without a mention; - two topics stay in separate sessions;
- inbound image, audio, PDF, and multiple attachments are understood;
- oversized media is rejected clearly without an agent turn;
- generated image/audio/PDF/general file arrives and renders/plays/downloads;
- a greater-than-20,000-character response arrives in ordered sub-9,500-character parts with its final sentinel;
- owner-DM diagnostics approval passes allow-once, deny, replay-safe failure, expiry, and channel-to-private reroute without public leakage;
- a controlled gateway stop/start recovers a queued event exactly once; and
- a second restart produces no duplicate.
Stage 5: curator acceptance
Run controlled Twitter and Hacker News candidates only after ordinary channel acceptance. For each:
- the message must land in channel
5and the correct topic; - rich Markdown, links, timestamps, image, and metrics must render correctly;
- the delivery receipt must contain a concrete Zulip message ID;
- the source row/batch may be acknowledged only after that receipt; and
- a forced Zulip delivery failure must leave the source pending.
Stage 6: physical APNs acceptance
On the real owner iPhone:
- begin with no unread messages and badge
0; - send one owner DM and confirm badge
1; - send a second owner DM and confirm badge
2; - foreground Zulip without opening the DM and confirm the badge stays
2; - read the messages and confirm badge
0; and - separately confirm that reading in the web client clears the iOS badge.
This is a release gate. API success and connector tests cannot prove APNs presentation behavior.
Rollback plan
Rollback must be prepared before promotion.
- Keep the current pinned OpenClaw 2026.7.1 image/build and exact connector 0.5.1 archive.
- Keep a consistent pre-upgrade copy of every state database and sidecar.
- If the 8.1 gateway fails before state migration, restore the prior image and 0.5.1 artifact.
- If 8.1 or Doctor changed state/schema, stop the new gateway and restore the entire pre-upgrade state set before returning to 7.1. Do not point 7.1 at 8.1-migrated databases.
- Restore connector configuration only from the known-good SecretRef-bearing copy; never synthesize a config from logs.
- Verify 7.1, 0.5.1, the Zulip monitor, exact-once journal recovery, owner DM, cron/tasks, and APNs behavior after rollback.
- Leave the Zulip Server image unchanged throughout; OpenClaw rollback must not retag, rebuild, or prune the patched server image.
Acceptance criteria for declaring the upgrade complete
The upgrade is complete only when all of the following are true:
- every P0 source/metadata issue is removed;
- the P1 SDK metadata/media/session migrations are either complete or explicitly documented with a safe removal window;
- the bounded retry/dead-letter issue is fixed or consciously accepted by the owner with an operator-visible mitigation;
- strict typecheck, all tests, clean build, dependency audit, archive inspection, disposable install, config validation, plugin doctor, and SecretRef audit pass against exact 8.1 artifacts;
- copied-state Doctor migration and rollback rehearsal pass;
- production starts healthy with only Zulip active;
- DM, topic, media, long-text, approval, restart, and curator live gates pass;
- physical
0 -> 1 -> 2 -> 0badge acceptance passes; and - artifact hashes, image digest, canary message IDs, journal evidence, and rollback assets are recorded in the project ledger/README.
Until then, keep production on OpenClaw 2026.7.1 with Zulip connector 0.5.1.
File-level change checklist
| File | Required change |
|---|---|
package.json |
Bump connector line; update install/compat/build/peer/dev OpenClaw metadata; add configuredState.env and approvalFlags; keep entries synchronized. |
package-lock.json |
Regenerate against exact OpenClaw 2026.8.1; never hand-edit. |
openclaw.plugin.json |
Remove channelEnvVars; retain and test static config schema/SecretRef surface. |
src/authorization.ts |
Replace removed channel-runtime type imports with public-derived/local narrow types. |
src/approvals.ts |
Import public approval capability contract; derive native/state types; make multi-account routing deterministic. |
src/inbound.ts |
Use turnAdoptionLifecycle; facts-first media; supported cleanup; routed session plan; ID validation; queue-expiry backoff; cancellation propagation. |
src/inbound-store.ts |
Add failed/dead-letter transition, bounded retry policy, operator-visible state, and later-row progress; preserve permissions/WAL/recovery. |
src/channel.ts |
Move outbound loader to a typed public 8.1 export; preserve media access roots/read callback and durable receipt behavior. |
src/client.ts |
Bounded JSON reader; filename sanitization; path-prefix policy; validated event IDs; abort propagation. |
src/config.ts / src/types.ts |
Remove/redact secret-bearing raw config retention; make discovery/account semantics match metadata. |
src/targets.ts |
Reject unsafe topic controls while preserving empty/Unicode topics. |
| Test files | Add every focused regression listed above; update SDK mocks and schema assertions. |
README.md |
Document 0.6.0/8.1 compatibility, exact gates, provenance, retry model, and new production evidence. |
/root/zulip/docs/openclaw-zulip-integration-plan.md |
Mark Discord fallback and 7.1 freeze as historical after acceptance. |
References
- OpenClaw v2026.8.1 release notes
- OpenClaw Plugin SDK migration guide
- OpenClaw channel plugin guide
- OpenClaw channel outbound and durable ingress API
- OpenClaw plugin runtime helpers
- OpenClaw 2.0 announcement
Final recommendation
Proceed with an isolated connector 0.6.0 migration, not with a production host upgrade yet. Fix the five source-level compatibility breaks, modernize discovery/approval/media/session contracts, close the poison-event retry gap, and prove the result against the exact 8.1 npm and container artifacts. Preserve the external SQLite journal and patched Zulip 12.1 server for this release. Promote only after disposable installation, copied-state migration, rollback rehearsal, exact-once restart recovery, private approval, curator receipt-gating, and physical badge acceptance all pass.