agent-pages

OpenClaw v2026.8.1 Upgrade Review for the Zulip Connector

Download

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:

  1. package metadata explicitly rejects every 2026.8.x host;
  2. two source files import the removed openclaw/plugin-sdk/channel-runtime subpath;
  3. inbound media cleanup imports deleteMediaBuffer, which 8.1 no longer exports publicly;
  4. the durable claim callback moved from top-level onTurnAdopted to turnAdoptionLifecycle.onAdopted;
  5. openclaw/plugin-sdk/outbound-media has runtime JavaScript in the 8.1 npm artifact but no public declaration entry, making the current strict TypeScript import unsafe;
  6. the plugin still emits legacy inbound media projection fields and manually supplies session-store wiring that 8.1 now deprecates; and
  7. static discovery metadata still uses the retired channelEnvVars shape and does not advertise its native approval capability through approvalFlags.

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:

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 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

  1. The connector registers a Zulip long-poll event queue.
  2. It separately backfills owner DMs and explicitly configured channels after startup or queue expiry.
  3. It classifies only one-to-one DMs or allowed stream/topic messages.
  4. 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.
  5. Accepted messages are durably inserted into plugins/zulip/inbound.sqlite before agent dispatch.
  6. The worker claims the oldest pending row, downloads and stores eligible attachments, constructs an OpenClaw inbound context, and runs the channel turn.
  7. The connector tombstones the journal row when OpenClaw adopts the run. OpenClaw then owns post-adoption recovery and outbound delivery.
  8. A pre-adoption failure releases the row for retry. On process restart, claimed rows are returned to pending.

Outbound path

Control plane

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:

Use public 8.1 contracts without deep-importing hashed/internal declaration files:

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:

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:

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:

Implement an explicit policy, for example:

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:

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:

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

  1. 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.
  2. Record 0.6.0 artifact provenance and keep the 0.5.1/0.5.0/0.4.2 archives for rollback.
  3. Mark /root/zulip/docs/openclaw-zulip-integration-plan.md sections that say Discord is enabled as historical. The current baseline is Zulip-only; Discord, Telegram, and WhatsApp must remain absent or disabled.
  4. Replace the old instruction to freeze OpenClaw at 2026.7.1 with the accepted 8.1 baseline only after production acceptance.
  5. Document the durable journal's failure/requeue model once bounded retry is implemented.
  6. 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:

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

  1. Install dependencies from the regenerated lockfile with scripts disabled.
  2. Confirm node_modules/openclaw/package.json is exactly 2026.8.1.
  3. Verify no source or test imports a removed, internal, or undeclared SDK path.
  4. Run strict TypeScript typecheck.
  5. Run the complete mocked suite.
  6. Run the production build from a clean dist directory.
  7. Run production-only dependency audit and require zero known runtime vulnerabilities.

New focused tests

Add explicit coverage for:

Existing regression matrix to retain

Package gate

  1. Run npm pack --json using the exact npm version shipped in the target 8.1 image.
  2. Confirm prepack rebuilt dist from the reviewed source.
  3. Inspect the archive: only expected dist, manifest, README, and package metadata may ship.
  4. Verify package version/compatibility/build metadata, entrypoints, setup entry, configured state, approval flags, and peer dependency.
  5. Record size, entry count, SHA-256, npm shasum, and integrity.
  6. Install the immutable archive through OpenClaw's npm-pack: lifecycle in a disposable 8.1 state directory.
  7. Require plugin discovery, config validation, plugin inspection, plugin doctor, and SecretRef audit to pass.
  8. 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:

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:

  1. start the exact target 8.1 image with the 0.6.0 connector and aligned official packages;
  2. run config validation and non-mutating inspection first;
  3. run doctor --fix only on copied state;
  4. review every proposed/applied migration, particularly model routes, OpenProse cleanup, plugin metadata, sessions, tasks, and automations;
  5. verify Zulip remains the sole configured/connected channel; and
  6. 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:

Stage 4: live logical acceptance

Use new canary messages and record their IDs:

  1. owner DM produces exactly one reply;
  2. unauthorized DM produces no run/reply;
  3. allowlisted unmentioned channel message is ignored;
  4. mentioned owner message in channel 4 produces one reply in the same topic;
  5. authorized /whoami works without a mention;
  6. two topics stay in separate sessions;
  7. inbound image, audio, PDF, and multiple attachments are understood;
  8. oversized media is rejected clearly without an agent turn;
  9. generated image/audio/PDF/general file arrives and renders/plays/downloads;
  10. a greater-than-20,000-character response arrives in ordered sub-9,500-character parts with its final sentinel;
  11. owner-DM diagnostics approval passes allow-once, deny, replay-safe failure, expiry, and channel-to-private reroute without public leakage;
  12. a controlled gateway stop/start recovers a queued event exactly once; and
  13. a second restart produces no duplicate.

Stage 5: curator acceptance

Run controlled Twitter and Hacker News candidates only after ordinary channel acceptance. For each:

Stage 6: physical APNs acceptance

On the real owner iPhone:

  1. begin with no unread messages and badge 0;
  2. send one owner DM and confirm badge 1;
  3. send a second owner DM and confirm badge 2;
  4. foreground Zulip without opening the DM and confirm the badge stays 2;
  5. read the messages and confirm badge 0; and
  6. 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.

  1. Keep the current pinned OpenClaw 2026.7.1 image/build and exact connector 0.5.1 archive.
  2. Keep a consistent pre-upgrade copy of every state database and sidecar.
  3. If the 8.1 gateway fails before state migration, restore the prior image and 0.5.1 artifact.
  4. 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.
  5. Restore connector configuration only from the known-good SecretRef-bearing copy; never synthesize a config from logs.
  6. Verify 7.1, 0.5.1, the Zulip monitor, exact-once journal recovery, owner DM, cron/tasks, and APNs behavior after rollback.
  7. 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:

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

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.